mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-04-29 05:15:32 +08:00
feat: add deploy smart contract debug API endpoint
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1955,6 +1955,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"escape-goat": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz",
|
||||
"integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw=="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"escape-goat": "^3.0.0",
|
||||
"express": "^4.17.1",
|
||||
"node-pg-migrate": "^4.2.3",
|
||||
"pg": "^7.18.2",
|
||||
|
||||
@@ -3,10 +3,12 @@ import * as path from 'path';
|
||||
import * as express from 'express';
|
||||
import * as BN from 'bn.js';
|
||||
import { addAsync } from '@awaitjs/express';
|
||||
import { makeSTXTokenTransfer, TransactionVersion, Address } from '@blockstack/stacks-transactions/src';
|
||||
import { htmlEscape } from 'escape-goat';
|
||||
import { makeSTXTokenTransfer, TransactionVersion, makeSmartContractDeploy } from '@blockstack/stacks-transactions/src';
|
||||
import { BufferReader } from '../../binary-reader';
|
||||
import { readTransaction } from '../../p2p/tx';
|
||||
import { txidFromData } from '@blockstack/stacks-transactions/src/utils';
|
||||
import * as BroadcastContractDefault from '../../sample-data/broadcast-contract-default.json';
|
||||
|
||||
const testnetKeys: { secretKey: string; publicKey: string; stacksAddress: string }[] = [
|
||||
{
|
||||
@@ -103,6 +105,66 @@ export function createDebugRouter(): express.Router {
|
||||
.send(tokenTransferHtml + '<h3>Broadcasted transaction:</h3>' + `<a href="/tx/${txId}">${txId}</a>`);
|
||||
});
|
||||
|
||||
const contractDeployHtml = `
|
||||
<style>
|
||||
input, textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 10;
|
||||
}
|
||||
</style>
|
||||
<form action="" method="post">
|
||||
<label for="origin_key">Sender key</label>
|
||||
<input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
|
||||
<datalist id="origin_keys">
|
||||
${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
|
||||
</datalist>
|
||||
|
||||
<label for="fee_rate">uSTX tx fee</label>
|
||||
<input type="number" id="fee_rate" name="fee_rate" value="9">
|
||||
|
||||
<label for="nonce">Nonce</label>
|
||||
<input type="number" id="nonce" name="nonce" value="0">
|
||||
|
||||
<label for="contract_name">Contract name</label>
|
||||
<input type="text" id="contract_name" name="contract_name" value="${htmlEscape(
|
||||
BroadcastContractDefault.contract_name
|
||||
)}" pattern="^[a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*$|^[-+=/*]$|^[<>]=?$" maxlength="128">
|
||||
|
||||
<label for="source_code">Contract Clarity source code</label>
|
||||
<textarea id="source_code" name="source_code" rows="52">${htmlEscape(
|
||||
BroadcastContractDefault.contract_source
|
||||
)}</textarea>
|
||||
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
`;
|
||||
|
||||
router.getAsync('/broadcast/contract-deploy', (req, res) => {
|
||||
res.set('Content-Type', 'text/html').send(contractDeployHtml);
|
||||
});
|
||||
|
||||
router.postAsync('/broadcast/contract-deploy', (req, res) => {
|
||||
const { origin_key, contract_name, source_code, fee_rate, nonce } = req.body;
|
||||
|
||||
const normalized_contract_source = (source_code as string).replace(/\r/g, '').replace(/\t/g, ' ');
|
||||
const deployTx = makeSmartContractDeploy(contract_name, normalized_contract_source, new BN(fee_rate), origin_key, {
|
||||
nonce: new BN(nonce),
|
||||
version: TransactionVersion.Testnet,
|
||||
});
|
||||
const serialized = deployTx.serialize();
|
||||
const mempoolPath = process.env['STACKS_CORE_MEMPOOL_PATH'];
|
||||
if (!mempoolPath) {
|
||||
throw new Error('STACKS_CORE_MEMPOOL_PATH not specified');
|
||||
}
|
||||
const txBinPath = path.join(mempoolPath, `tx_${Date.now()}.bin`);
|
||||
fs.writeFileSync(txBinPath, serialized);
|
||||
const txId = '0x' + txidFromData(serialized);
|
||||
res
|
||||
.set('Content-Type', 'text/html')
|
||||
.send(contractDeployHtml + '<h3>Broadcasted transaction:</h3>' + `<a href="/tx/${txId}">${txId}</a>`);
|
||||
});
|
||||
|
||||
const txWatchHtml = `
|
||||
<script>
|
||||
const sse = new EventSource('/tx/stream?protocol=eventsource');
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CoreNodeParsedTxMessage } from '../event-stream/core-node-message';
|
||||
import { TransactionAuthTypeID, TransactionPayloadTypeID } from '../p2p/tx';
|
||||
import { c32address } from 'c32check';
|
||||
import { NotImplementedError } from '../errors';
|
||||
import { Address } from '@blockstack/stacks-transactions/src';
|
||||
|
||||
export interface DbBlock {
|
||||
block_hash: string;
|
||||
@@ -197,6 +198,16 @@ export function createDbTxFromCoreMsg(msg: CoreNodeParsedTxMessage): DbTx {
|
||||
dbTx.token_transfer_memo = rawTx.payload.memo;
|
||||
break;
|
||||
}
|
||||
case TransactionPayloadTypeID.SmartContract: {
|
||||
const sender_address = Address.fromHashMode(
|
||||
rawTx.auth.originCondition.hashMode as number,
|
||||
rawTx.version as number,
|
||||
rawTx.auth.originCondition.signer.toString('hex')
|
||||
).toString();
|
||||
dbTx.smart_contract_contract_id = sender_address + '.' + rawTx.payload.name;
|
||||
dbTx.smart_contract_source_code = rawTx.payload.codeBody;
|
||||
break;
|
||||
}
|
||||
case TransactionPayloadTypeID.ContractCall: {
|
||||
const contractAddress = c32address(rawTx.payload.address.version, rawTx.payload.address.bytes.toString('hex'));
|
||||
dbTx.contract_call_contract_id = `${contractAddress}.${rawTx.payload.contractName}`;
|
||||
|
||||
5
src/sample-data/broadcast-contract-default.json
Normal file
5
src/sample-data/broadcast-contract-default.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"contract_name": "hello-world-contract",
|
||||
"contract_source": "(define-constant sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)\n(define-constant recipient 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G)\n\n(define-fungible-token novel-token-19)\n(define-public (test-transfer-ft)\n (begin\n (unwrap-panic (ft-transfer? novel-token-19 u10 sender recipient))\n (ok u1)))\n(define-public (test-mint-ft)\n (begin\n (unwrap-panic (ft-mint? novel-token-19 u11 recipient))\n (ok u1)))\n(begin (ft-mint? novel-token-19 u12 sender))\n\n(define-non-fungible-token hello-nft uint)\n(define-public (test-transfer-nft)\n (begin\n (unwrap-panic (nft-transfer? hello-nft u1 sender recipient))\n (ok u1)))\n(define-public (test-mint-nft)\n (begin\n (unwrap-panic (nft-mint? hello-nft u2 recipient))\n (ok u2)))\n(begin (nft-mint? hello-nft u1 sender))\n\n(define-public (test-emit-event)\n (begin\n (print \"Event! Hello world\")\n (ok u1)))\n\n(define-public (test-event-types)\n (begin\n (test-emit-event)\n (test-mint-ft)\n (test-transfer-ft)\n (test-mint-nft)\n (test-transfer-nft)\n (unwrap-panic (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))\n (unwrap-panic (stx-burn? u20 tx-sender))\n (ok 'true)))\n\n(define-map store ((key (buff 32))) ((value (buff 32))))\n(define-public (get-value (key (buff 32)))\n (begin\n (match (map-get? store ((key key)))\n entry (ok (get value entry))\n (err 0))))\n(define-public (set-value (key (buff 32)) (value (buff 32)))\n (begin\n (map-set store ((key key)) ((value value)))\n (ok 'true)))"
|
||||
|
||||
}
|
||||
@@ -10,11 +10,14 @@
|
||||
"sourceRoot": ".",
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": false,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"migrations/**/*",
|
||||
"tests/**/*"
|
||||
"tests/**/*",
|
||||
],
|
||||
"exclude": ["lib"]
|
||||
"exclude": [
|
||||
"lib",
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user