Skip to content

Commit 4b1adf2

Browse files
asimm241zone117x
authored andcommitted
fix: add support for multi signature in payloads endpoint
add test cases for multisignature for combine and payloads endpoint
1 parent 9ca1abf commit 4b1adf2

File tree

2 files changed

+264
-28
lines changed

2 files changed

+264
-28
lines changed

src/api/routes/rosetta/construction.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
import {
3434
UnsignedTokenTransferOptions,
3535
makeUnsignedSTXTokenTransfer,
36+
TokenTransferOptions,
37+
UnsignedMultiSigTokenTransferOptions,
3638
} from '@blockstack/stacks-transactions';
3739
import * as express from 'express';
3840
import { StacksCoreRpcClient } from '../../../core-rpc/client';
@@ -382,27 +384,54 @@ export function createRosettaConstructionRouter(db: DataStore): RouterWithAsync
382384
res.status(400).json(RosettaErrors.emptyPublicKey);
383385
return;
384386
}
387+
for (const key of publicKeys) {
388+
if (key.curve_type !== 'secp256k1') {
389+
res.status(400).json(RosettaErrors.invalidCurveType);
390+
return;
391+
}
392+
}
385393

386-
if (publicKeys[0].curve_type !== 'secp256k1') {
387-
res.status(400).json(RosettaErrors.invalidCurveType);
394+
const recipientAddress = options.token_transfer_recipient_address;
395+
if (!recipientAddress) {
396+
res.status(400).json(RosettaErrors.invalidRecipient);
388397
return;
389398
}
399+
const senderAddress = options.sender_address;
390400

391-
const recipientAddress = options.token_transfer_recipient_address
392-
? options.token_transfer_recipient_address
393-
: '';
394-
const senderAddress = options.sender_address ? options.sender_address : '';
401+
if (!senderAddress) {
402+
res.status(400).json(RosettaErrors.invalidSender);
403+
return;
404+
}
395405

396406
const accountInfo = await new StacksCoreRpcClient().getAccount(senderAddress);
397407

398-
const tokenTransferOptions: UnsignedTokenTransferOptions = {
399-
recipient: recipientAddress,
400-
amount: new BN(amount),
401-
fee: new BN(fees),
402-
publicKey: publicKeys[0].hex_bytes,
403-
network: GetStacksTestnetNetwork(),
404-
nonce: accountInfo.nonce ? new BN(accountInfo.nonce) : new BN(0),
405-
};
408+
let tokenTransferOptions: UnsignedTokenTransferOptions | UnsignedMultiSigTokenTransferOptions;
409+
410+
if (publicKeys.length > 1) {
411+
//multi signature
412+
const publicKeysStrings = publicKeys.map(key => {
413+
return key.hex_bytes;
414+
});
415+
tokenTransferOptions = {
416+
recipient: recipientAddress,
417+
amount: new BN(amount),
418+
fee: new BN(fees),
419+
publicKeys: publicKeysStrings,
420+
numSignatures: 2,
421+
network: GetStacksTestnetNetwork(),
422+
nonce: accountInfo.nonce ? new BN(accountInfo.nonce) : new BN(0),
423+
};
424+
} else {
425+
// signel signature
426+
tokenTransferOptions = {
427+
recipient: recipientAddress,
428+
amount: new BN(amount),
429+
fee: new BN(fees),
430+
publicKey: publicKeys[0].hex_bytes,
431+
network: GetStacksTestnetNetwork(),
432+
nonce: accountInfo.nonce ? new BN(accountInfo.nonce) : new BN(0),
433+
};
434+
}
406435

407436
const transaction = await makeUnsignedSTXTokenTransfer(tokenTransferOptions);
408437
const unsignedTransaction = transaction.serialize();

src/tests-rosetta/api.ts

Lines changed: 221 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {
1818
StacksTestnet,
1919
standardPrincipalCV,
2020
TransactionSigner,
21+
UnsignedMultiSigTokenTransferOptions,
2122
UnsignedTokenTransferOptions,
2223
} from '@blockstack/stacks-transactions';
2324
import * as BN from 'bn.js';
2425
import { getCoreNodeEndpoint, StacksCoreRpcClient } from '../core-rpc/client';
25-
import { bufferToHexPrefixString } from '../helpers';
26+
import { bufferToHexPrefixString, digestSha512_256 } from '../helpers';
2627
import {
2728
RosettaConstructionCombineRequest,
2829
RosettaConstructionCombineResponse,
@@ -39,8 +40,7 @@ import {
3940
} from '@blockstack/stacks-blockchain-api-types';
4041
import { RosettaConstants, RosettaErrors } from '../api/rosetta-constants';
4142
import { GetStacksTestnetNetwork, testnetKeys } from '../api/routes/debug';
42-
import { getSignature } from '../rosetta-helpers';
43-
import { cloneDeep } from '@blockstack/stacks-transactions/lib/utils';
43+
import { getOptionsFromOperations, getSignature } from '../rosetta-helpers';
4444

4545
describe('Rosetta API', () => {
4646
let db: PgDataStore;
@@ -1135,7 +1135,12 @@ describe('Rosetta API', () => {
11351135
expect(JSON.parse(result.text)).toEqual(expectedResponse);
11361136
});
11371137

1138-
test('payloads success', async () => {
1138+
test('payloads single sign success', async () => {
1139+
const publicKey = publicKeyToString(pubKeyfromPrivKey(testnetKeys[0].secretKey));
1140+
const sender = testnetKeys[0].stacksAddress;
1141+
const recipient = testnetKeys[1].stacksAddress;
1142+
const fee = '-180';
1143+
11391144
const request: RosettaConstructionPayloadsRequest = {
11401145
network_identifier: {
11411146
blockchain: 'stacks',
@@ -1151,11 +1156,11 @@ describe('Rosetta API', () => {
11511156
type: 'fee',
11521157
status: 'success',
11531158
account: {
1154-
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
1159+
address: sender,
11551160
metadata: {},
11561161
},
11571162
amount: {
1158-
value: '-180',
1163+
value: fee,
11591164
currency: {
11601165
symbol: 'STX',
11611166
decimals: 6,
@@ -1172,7 +1177,7 @@ describe('Rosetta API', () => {
11721177
type: 'token_transfer',
11731178
status: 'success',
11741179
account: {
1175-
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
1180+
address: sender,
11761181
metadata: {},
11771182
},
11781183
amount: {
@@ -1193,7 +1198,7 @@ describe('Rosetta API', () => {
11931198
type: 'token_transfer',
11941199
status: 'success',
11951200
account: {
1196-
address: 'STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0',
1201+
address: recipient,
11971202
metadata: {},
11981203
},
11991204
amount: {
@@ -1208,12 +1213,27 @@ describe('Rosetta API', () => {
12081213
],
12091214
public_keys: [
12101215
{
1211-
hex_bytes: '025c13b2fc2261956d8a4ad07d481b1a3b2cbf93a24f992249a61c3a1c4de79c51',
1216+
hex_bytes: publicKey,
12121217
curve_type: 'secp256k1',
12131218
},
12141219
],
12151220
};
12161221

1222+
const accountInfo = await new StacksCoreRpcClient().getAccount(sender);
1223+
1224+
const tokenTransferOptions: UnsignedTokenTransferOptions = {
1225+
recipient: recipient,
1226+
amount: new BN('500000'),
1227+
fee: new BN(fee),
1228+
publicKey: publicKey,
1229+
network: GetStacksTestnetNetwork(),
1230+
nonce: accountInfo.nonce ? new BN(accountInfo.nonce) : new BN(0),
1231+
};
1232+
1233+
const transaction = await makeUnsignedSTXTokenTransfer(tokenTransferOptions);
1234+
const unsignedTransaction = transaction.serialize();
1235+
const hexBytes = digestSha512_256(unsignedTransaction).toString('hex');
1236+
12171237
const result = await supertest(api.server)
12181238
.post(`/rosetta/v1/construction/payloads`)
12191239
.send(request);
@@ -1222,12 +1242,138 @@ describe('Rosetta API', () => {
12221242
expect(result.type).toBe('application/json');
12231243

12241244
const expectedResponse = {
1225-
unsigned_transaction:
1226-
'80800000000400539886f96611ba3ba6cef9618f8c78118b37c5be000000000000000000000000000000b400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003020000000000051a1ae3f911d8f1d46d7416bfbe4b593fd41eac19cb000000000007a12000000000000000000000000000000000000000000000000000000000000000000000',
1245+
unsigned_transaction: unsignedTransaction.toString('hex'),
12271246
payloads: [
12281247
{
1229-
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
1230-
hex_bytes: '0xf1e432494d509577c5468a8cad70d957942e2671f299340a20f65992a4bfa221',
1248+
address: sender,
1249+
hex_bytes: '0x' + hexBytes,
1250+
signature_type: 'ecdsa',
1251+
},
1252+
],
1253+
};
1254+
1255+
expect(JSON.parse(result.text)).toEqual(expectedResponse);
1256+
});
1257+
1258+
test('payloads multi sign success', async () => {
1259+
const publicKey1 = publicKeyToString(pubKeyfromPrivKey(testnetKeys[0].secretKey));
1260+
const publicKey2 = publicKeyToString(pubKeyfromPrivKey(testnetKeys[1].secretKey));
1261+
1262+
const sender = testnetKeys[0].stacksAddress;
1263+
const recipient = testnetKeys[1].stacksAddress;
1264+
const fee = '-180';
1265+
1266+
const request: RosettaConstructionPayloadsRequest = {
1267+
network_identifier: {
1268+
blockchain: 'stacks',
1269+
network: 'testnet',
1270+
},
1271+
operations: [
1272+
{
1273+
operation_identifier: {
1274+
index: 0,
1275+
network_index: 0,
1276+
},
1277+
related_operations: [],
1278+
type: 'fee',
1279+
status: 'success',
1280+
account: {
1281+
address: sender,
1282+
metadata: {},
1283+
},
1284+
amount: {
1285+
value: fee,
1286+
currency: {
1287+
symbol: 'STX',
1288+
decimals: 6,
1289+
},
1290+
metadata: {},
1291+
},
1292+
},
1293+
{
1294+
operation_identifier: {
1295+
index: 1,
1296+
network_index: 0,
1297+
},
1298+
related_operations: [],
1299+
type: 'token_transfer',
1300+
status: 'success',
1301+
account: {
1302+
address: sender,
1303+
metadata: {},
1304+
},
1305+
amount: {
1306+
value: '-500000',
1307+
currency: {
1308+
symbol: 'STX',
1309+
decimals: 6,
1310+
},
1311+
metadata: {},
1312+
},
1313+
},
1314+
{
1315+
operation_identifier: {
1316+
index: 2,
1317+
network_index: 0,
1318+
},
1319+
related_operations: [],
1320+
type: 'token_transfer',
1321+
status: 'success',
1322+
account: {
1323+
address: recipient,
1324+
metadata: {},
1325+
},
1326+
amount: {
1327+
value: '500000',
1328+
currency: {
1329+
symbol: 'STX',
1330+
decimals: 6,
1331+
},
1332+
metadata: {},
1333+
},
1334+
},
1335+
],
1336+
public_keys: [
1337+
{
1338+
hex_bytes: publicKey1,
1339+
curve_type: 'secp256k1',
1340+
},
1341+
{
1342+
hex_bytes: publicKey2,
1343+
curve_type: 'secp256k1',
1344+
},
1345+
],
1346+
};
1347+
1348+
const accountInfo = await new StacksCoreRpcClient().getAccount(sender);
1349+
1350+
const tokenTransferOptions: UnsignedMultiSigTokenTransferOptions = {
1351+
recipient: recipient,
1352+
amount: new BN('500000'),
1353+
fee: new BN(fee),
1354+
publicKeys: [publicKey1, publicKey2],
1355+
numSignatures: 2,
1356+
network: GetStacksTestnetNetwork(),
1357+
nonce: accountInfo.nonce ? new BN(accountInfo.nonce) : new BN(0),
1358+
};
1359+
1360+
const transaction = await makeUnsignedSTXTokenTransfer(tokenTransferOptions);
1361+
const unsignedTransaction = transaction.serialize();
1362+
const hexBytes = digestSha512_256(unsignedTransaction).toString('hex');
1363+
1364+
const result = await supertest(api.server)
1365+
.post(`/rosetta/v1/construction/payloads`)
1366+
.send(request);
1367+
1368+
expect(result.status).toBe(200);
1369+
expect(result.type).toBe('application/json');
1370+
1371+
const expectedResponse = {
1372+
unsigned_transaction: unsignedTransaction.toString('hex'),
1373+
payloads: [
1374+
{
1375+
address: sender,
1376+
hex_bytes: '0x' + hexBytes,
12311377
signature_type: 'ecdsa',
12321378
},
12331379
],
@@ -1412,7 +1558,7 @@ describe('Rosetta API', () => {
14121558
expect(JSON.parse(result.text)).toEqual(expectedResponse);
14131559
});
14141560

1415-
test('combine success', async () => {
1561+
test('combine single sign success', async () => {
14161562
const publicKey = publicKeyToString(pubKeyfromPrivKey(testnetKeys[0].secretKey));
14171563

14181564
const txOptions: UnsignedTokenTransferOptions = {
@@ -1471,6 +1617,67 @@ describe('Rosetta API', () => {
14711617
expect(JSON.parse(result.text)).toEqual(expectedResponse);
14721618
});
14731619

1620+
test('combine multi sign success', async () => {
1621+
const publicKey1 = publicKeyToString(pubKeyfromPrivKey(testnetKeys[0].secretKey));
1622+
const publicKey2 = publicKeyToString(pubKeyfromPrivKey(testnetKeys[1].secretKey));
1623+
1624+
const txOptions: UnsignedMultiSigTokenTransferOptions = {
1625+
publicKeys: [publicKey1, publicKey2],
1626+
numSignatures: 2,
1627+
recipient: standardPrincipalCV(testnetKeys[1].stacksAddress),
1628+
amount: new BigNum(12345),
1629+
network: GetStacksTestnetNetwork(),
1630+
memo: 'test memo',
1631+
nonce: new BigNum(0),
1632+
fee: new BigNum(200),
1633+
};
1634+
1635+
const unsignedTransaction = await makeUnsignedSTXTokenTransfer(txOptions);
1636+
const unsignedSerializedTx = unsignedTransaction.serialize().toString('hex');
1637+
1638+
const signer = new TransactionSigner(unsignedTransaction);
1639+
signer.signOrigin(createStacksPrivateKey(testnetKeys[0].secretKey));
1640+
const signedSerializedTx = unsignedTransaction.serialize().toString('hex');
1641+
1642+
const signature = getSignature(unsignedTransaction);
1643+
if (!signature) return;
1644+
1645+
const request: RosettaConstructionCombineRequest = {
1646+
network_identifier: {
1647+
blockchain: 'stacks',
1648+
network: 'testnet',
1649+
},
1650+
unsigned_transaction: unsignedSerializedTx,
1651+
signatures: [
1652+
{
1653+
signing_payload: {
1654+
hex_bytes: signature.data,
1655+
signature_type: 'ecdsa',
1656+
},
1657+
public_key: {
1658+
hex_bytes: publicKey1,
1659+
curve_type: 'secp256k1',
1660+
},
1661+
signature_type: 'ecdsa',
1662+
hex_bytes: signature.data,
1663+
},
1664+
],
1665+
};
1666+
1667+
const result = await supertest(api.server)
1668+
.post(`/rosetta/v1/construction/combine`)
1669+
.send(request);
1670+
1671+
expect(result.status).toBe(200);
1672+
expect(result.type).toBe('application/json');
1673+
1674+
const expectedResponse: RosettaConstructionCombineResponse = {
1675+
signed_transaction: signedSerializedTx,
1676+
};
1677+
1678+
expect(JSON.parse(result.text)).toEqual(expectedResponse);
1679+
});
1680+
14741681
test('combine invalid transaction', async () => {
14751682
const request: RosettaConstructionCombineRequest = {
14761683
network_identifier: {

0 commit comments

Comments
 (0)