feat: initial debug endpoint support for sending multisig transactions

This commit is contained in:
Matthew Little
2020-08-20 09:28:29 -06:00
parent a4661486af
commit d12ba53fb0
6 changed files with 923 additions and 131 deletions

16
.vscode/launch.json vendored
View File

@@ -30,22 +30,6 @@
"TS_NODE_SKIP_IGNORE": "true"
}
},
{
"type": "node",
"request": "launch",
"name": "Launch: db-memory",
"runtimeArgs": ["-r", "ts-node/register/transpile-only", "-r", "tsconfig-paths/register"],
"args": ["${workspaceFolder}/src/index.ts"],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"preLaunchTask": "stacks-node:deploy-dev",
"postDebugTask": "stacks-node:stop-dev",
"env": {
"STACKS_BLOCKCHAIN_API_DB": "memory",
"NODE_ENV": "development",
"TS_NODE_SKIP_IGNORE": "true",
}
},
{
"type": "node",
"request": "launch",

760
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -85,7 +85,7 @@
"@apidevtools/json-schema-ref-parser": "^9.0.1",
"@awaitjs/express": "^0.5.1",
"@blockstack/stacks-blockchain-api-types": "file:docs",
"@blockstack/stacks-transactions": "github:blockstack/stacks-transactions-js#953d438ba8bc7380408666ae768533b1678fa9e9",
"@blockstack/stacks-transactions": "github:blockstack/stacks-transactions-js#4042320fbe3b1b8c2d03596804a0d3e8b9405ea1",
"@types/ws": "^7.2.5",
"big-integer": "^1.6.48",
"bitcoinjs-lib": "^5.1.7",

View File

@@ -12,6 +12,17 @@ import {
StacksTestnet,
getAddressFromPrivateKey,
sponsorTransaction,
makeUnsignedSTXTokenTransfer,
TransactionSigner,
createStacksPrivateKey,
pubKeyfromPrivKey,
publicKeyToString,
addressFromPublicKeys,
AddressHashMode,
createStacksPublicKey,
TransactionVersion,
AddressVersion,
addressToString,
} from '@blockstack/stacks-transactions';
import { SampleContracts } from '../../sample-data/broadcast-contract-default';
import { DataStore, DbFaucetRequestCurrency } from '../../datastore/common';
@@ -40,6 +51,20 @@ export const testnetKeys: { secretKey: string; stacksAddress: string }[] = [
},
];
export const testnetKeyMap: Record<
string,
{ address: string; secretKey: string; pubKey: string }
> = Object.fromEntries(
testnetKeys.map(t => [
t.stacksAddress,
{
address: t.stacksAddress,
secretKey: t.secretKey,
pubKey: publicKeyToString(pubKeyfromPrivKey(t.secretKey)),
},
])
);
export function GetStacksTestnetNetwork() {
const stacksNetwork = new StacksTestnet();
stacksNetwork.coreApiUrl = `http://${getCoreNodeEndpoint()}`;
@@ -59,6 +84,227 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
return submitResult;
}
const tokenTransferFromMultisigHtml = `
<style>
* { font-family: "Lucida Console", Monaco, monospace; }
input, select {
display: block;
width: 100%;
margin-bottom: 10;
}
</style>
<form action="" method="post">
<label for="signers">Signers</label>
<select name="signers" id="signers" multiple>
${testnetKeys
.map(k => `<option value="${k.stacksAddress}">${k.stacksAddress}</option>`)
.join('\n')}
</select>
<label for="signatures_required">Signatures required</label>
<input type="number" id="signatures_required" name="signatures_required" value="1">
<label for="recipient_address">Recipient address</label>
<input list="recipient_addresses" name="recipient_address" value="${
testnetKeys[1].stacksAddress
}">
<datalist id="recipient_addresses">
${testnetKeys.map(k => '<option value="' + k.stacksAddress + '">').join('\n')}
</datalist>
<label for="stx_amount">uSTX amount</label>
<input type="number" id="stx_amount" name="stx_amount" value="100">
<label for="memo">Memo</label>
<input type="text" id="memo" name="memo" value="hello" maxlength="34">
<input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
<label for="sponsored">Create sponsored transaction</label>
<input type="submit" value="Submit">
</form>
`;
router.getAsync('/broadcast/token-transfer-from-multisig', (req, res) => {
res.set('Content-Type', 'text/html').send(tokenTransferFromMultisigHtml);
});
router.postAsync('/broadcast/token-transfer-from-multisig', async (req, res) => {
const { signers, signatures_required, recipient_address, stx_amount, memo } = req.body as {
signers: string[];
signatures_required: string;
recipient_address: string;
stx_amount: string;
memo: string;
};
const sponsored = !!req.body.sponsored;
const signerPubKeys = signers.map(addr => testnetKeyMap[addr].pubKey);
const signerPrivateKeys = signers.map(addr => testnetKeyMap[addr].secretKey);
const transferTx1 = await makeSTXTokenTransfer({
recipient: recipient_address,
amount: new BN(stx_amount),
memo: memo,
network: stacksNetwork,
sponsored: sponsored,
numSignatures: parseInt(signatures_required),
// TODO: should this field be named `signerPublicKeys`?
publicKeys: signerPubKeys,
// TODO: should this field be named `signerPrivateKeys`?
signerKeys: signerPrivateKeys,
});
const transferTx = await makeUnsignedSTXTokenTransfer({
recipient: recipient_address,
amount: new BN(stx_amount),
memo: memo,
network: stacksNetwork,
numSignatures: signers.length,
publicKeys: signerPubKeys,
sponsored: sponsored,
});
const signer = new TransactionSigner(transferTx);
signerPrivateKeys.forEach(signerKey => {
signer.signOrigin(createStacksPrivateKey(signerKey));
});
// signer.appendOrigin(origin_key);
let serialized: Buffer;
let expectedTxId: string;
if (sponsored) {
const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
const sponsoredTx = await sponsorTransaction({
network: stacksNetwork,
transaction: transferTx,
sponsorPrivateKey: sponsorKey,
});
serialized = sponsoredTx.serialize();
expectedTxId = sponsoredTx.txid();
} else {
serialized = transferTx.serialize();
expectedTxId = transferTx.txid();
}
const { txId } = await sendCoreTx(serialized);
if (txId !== '0x' + expectedTxId) {
throw new Error(`Expected ${expectedTxId}, core ${txId}`);
}
res
.set('Content-Type', 'text/html')
.send(
tokenTransferFromMultisigHtml +
'<h3>Broadcasted transaction:</h3>' +
`<a href="/extended/v1/tx/${txId}">${txId}</a>`
);
});
const tokenTransferMultisigHtml = `
<style>
* { font-family: "Lucida Console", Monaco, monospace; }
input, select {
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="recipient_addresses">Recipient addresses</label>
<select name="recipient_addresses" id="recipient_addresses" multiple>
${testnetKeys
.map(k => `<option value="${k.stacksAddress}">${k.stacksAddress}</option>`)
.join('\n')}
</select>
<label for="signatures_required">Signatures required</label>
<input type="number" id="signatures_required" name="signatures_required" value="1">
<label for="stx_amount">uSTX amount</label>
<input type="number" id="stx_amount" name="stx_amount" value="100">
<label for="memo">Memo</label>
<input type="text" id="memo" name="memo" value="hello" maxlength="34">
<input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
<label for="sponsored">Create sponsored transaction</label>
<input type="submit" value="Submit">
</form>
`;
router.getAsync('/broadcast/token-transfer-multisig', (req, res) => {
res.set('Content-Type', 'text/html').send(tokenTransferMultisigHtml);
});
router.postAsync('/broadcast/token-transfer-multisig', async (req, res) => {
const { origin_key, recipient_addresses, signatures_required, stx_amount, memo } = req.body as {
origin_key: string;
recipient_addresses: string[];
signatures_required: string;
stx_amount: string;
memo: string;
};
const sponsored = !!req.body.sponsored;
const recipientPubKeys = recipient_addresses
.map(s => testnetKeyMap[s].pubKey)
.map(k => createStacksPublicKey(k));
const sigRequired = parseInt(signatures_required);
const recipientAddress = addressToString(
addressFromPublicKeys(
stacksNetwork.version === TransactionVersion.Testnet
? AddressVersion.TestnetMultiSig
: AddressVersion.MainnetMultiSig,
AddressHashMode.SerializeP2SH,
sigRequired,
recipientPubKeys
)
);
const transferTx = await makeSTXTokenTransfer({
recipient: recipientAddress,
amount: new BN(stx_amount),
memo: memo,
network: stacksNetwork,
senderKey: origin_key,
sponsored: sponsored,
});
let serialized: Buffer;
let expectedTxId: string;
if (sponsored) {
const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
const sponsoredTx = await sponsorTransaction({
network: stacksNetwork,
transaction: transferTx,
sponsorPrivateKey: sponsorKey,
});
serialized = sponsoredTx.serialize();
expectedTxId = sponsoredTx.txid();
} else {
serialized = transferTx.serialize();
expectedTxId = transferTx.txid();
}
const { txId } = await sendCoreTx(serialized);
if (txId !== '0x' + expectedTxId) {
throw new Error(`Expected ${expectedTxId}, core ${txId}`);
}
res
.set('Content-Type', 'text/html')
.send(
tokenTransferMultisigHtml +
'<h3>Broadcasted transaction:</h3>' +
`<a href="/extended/v1/tx/${txId}">${txId}</a>`
);
});
const tokenTransferHtml = `
<style>
* { font-family: "Lucida Console", Monaco, monospace; }
@@ -114,6 +360,7 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
});
let serialized: Buffer;
let expectedTxId: string;
if (sponsored) {
const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
const sponsoredTx = await sponsorTransaction({
@@ -122,11 +369,16 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
sponsorPrivateKey: sponsorKey,
});
serialized = sponsoredTx.serialize();
expectedTxId = sponsoredTx.txid();
} else {
serialized = transferTx.serialize();
expectedTxId = transferTx.txid();
}
const { txId } = await sendCoreTx(serialized);
if (txId !== '0x' + expectedTxId) {
throw new Error(`Expected ${expectedTxId}, core ${txId}`);
}
res
.set('Content-Type', 'text/html')
.send(
@@ -136,15 +388,6 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
);
});
router.postAsync('/v2/transactions', async (req, res) => {
const data: Buffer = req.body;
const { txId } = await sendCoreTx(data);
res.json({
success: true,
txId,
});
});
const contractDeployHtml = `
<style>
* { font-family: "Lucida Console", Monaco, monospace; }
@@ -202,6 +445,7 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
});
let serializedTx: Buffer;
let expectedTxId: string;
if (sponsored) {
const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
const sponsoredTx = await sponsorTransaction({
@@ -210,12 +454,17 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
sponsorPrivateKey: sponsorKey,
});
serializedTx = sponsoredTx.serialize();
expectedTxId = sponsoredTx.txid();
} else {
serializedTx = contractDeployTx.serialize();
expectedTxId = contractDeployTx.txid();
}
const contractId = senderAddress + '.' + contract_name;
const { txId } = await sendCoreTx(serializedTx);
if (txId !== '0x' + expectedTxId) {
throw new Error(`Expected ${expectedTxId}, core ${txId}`);
}
res
.set('Content-Type', 'text/html')
.send(
@@ -359,6 +608,7 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
});
let serialized: Buffer;
let expectedTxId: string;
if (sponsored) {
const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
const sponsoredTx = await sponsorTransaction({
@@ -367,11 +617,16 @@ export function createDebugRouter(db: DataStore): RouterWithAsync {
sponsorPrivateKey: sponsorKey,
});
serialized = sponsoredTx.serialize();
expectedTxId = sponsoredTx.txid();
} else {
serialized = contractCallTx.serialize();
expectedTxId = contractCallTx.txid();
}
const { txId } = await sendCoreTx(serialized);
if (txId !== '0x' + expectedTxId) {
throw new Error(`Expected ${expectedTxId}, core ${txId}`);
}
res
.set('Content-Type', 'text/html')
.send('<h3>Broadcasted transaction:</h3>' + `<a href="/extended/v1/tx/${txId}">${txId}</a>`);

View File

@@ -1621,6 +1621,7 @@ describe('api tests', () => {
contractName: 'hello-world',
codeBody: '()',
fee: new BN(200),
nonce: new BN(0),
senderKey: 'b8d99fd45da58038d630d9855d3ca2466e8e0f89d3894c4724f0efc9ff4b51f001',
postConditions: [],
});

View File

@@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": false,
"resolveJsonModule": true,
"baseUrl": ".",
"skipLibCheck": false,
"skipLibCheck": true,
"paths": {
"*": ["src/@types/*"]
}