Skip to content

Commit d4a5538

Browse files
007harshmahajanshashankms288
authored andcommitted
feat(polygon): added support for mpcv2 in recovery
Ticket: WIN-4617 Merge pull request #5877 from BitGo/BTC-1963-create-invoice chore: adding create-invoice example for custodial lightning TICKET: WIN-4617
1 parent 2b45af1 commit d4a5538

File tree

4 files changed

+255
-49
lines changed

4 files changed

+255
-49
lines changed

modules/sdk-coin-polygon/src/lib/transactionBuilder.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TransactionBuilder as EthLikeTransactionBuilder } from '@bitgo/abstract-eth';
2-
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { BuildTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
33
import { BaseCoin as CoinConfig } from '@bitgo/statics';
44

55
import { getCommon } from './utils';
@@ -8,6 +8,7 @@ import { Transaction, TransferBuilder } from './';
88

99
export class TransactionBuilder extends EthLikeTransactionBuilder {
1010
protected _transfer: TransferBuilder;
11+
private _signatures: { publicKey: string; signature: string }[] = [];
1112

1213
constructor(_coinConfig: Readonly<CoinConfig>) {
1314
super(_coinConfig);
@@ -27,6 +28,16 @@ export class TransactionBuilder extends EthLikeTransactionBuilder {
2728
return this._transfer;
2829
}
2930

31+
/**
32+
* Add a signature to the transaction
33+
* @param publicKey - The public key associated with the signature
34+
* @param signature - The signature to add
35+
*/
36+
addSignature(publicKey: PublicKey, signature: Buffer): void {
37+
// Method updated
38+
this._signatures.push({ publicKey: publicKey.toString(), signature: signature.toString('hex') });
39+
}
40+
3041
/** @inheritdoc */
3142
public coinUsesNonPackedEncodingForTxData(): boolean {
3243
// This is because the contracts which have been deployed for

modules/sdk-coin-polygon/src/polygon.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,26 @@ import { AbstractEthLikeNewCoins, recoveryBlockchainExplorerQuery } from '@bitgo
55
import { BaseCoin, BitGoBase, common, MPCAlgorithm, MultisigType, multisigTypes } from '@bitgo/sdk-core';
66
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
77
import { TransactionBuilder } from './lib';
8+
import {
9+
UnsignedSweepTxMPCv2,
10+
RecoverOptions,
11+
OfflineVaultTxInfo,
12+
} from '../../abstract-eth/src/abstractEthLikeNewCoins';
813

914
export class Polygon extends AbstractEthLikeNewCoins {
1015
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
1116
super(bitgo, staticsCoin);
1217
}
1318

19+
/**
20+
* Builds an unsigned sweep transaction for TSS
21+
* @param params - Recovery options
22+
* @returns {Promise<OfflineVaultTxInfo | UnsignedSweepTxMPCv2>}
23+
*/
24+
protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise<OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
25+
return this.buildUnsignedSweepTxnMPCv2(params);
26+
}
27+
1428
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
1529
return new Polygon(bitgo, staticsCoin);
1630
}

modules/sdk-coin-polygon/test/fixtures/polygon.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,84 @@ export const getBalanceResponse = {
3636
result: '9999999999999999928',
3737
};
3838

39+
// New mock data functions for recovery and sweep
40+
const TEST_POLYGON_WALLET_FIRST_ADDRESS = '0xbd475757237154b522333fca4c565bc789b46193';
41+
const TEST_RECOVERY_PASSCODE = 'Bondiola1234#';
42+
43+
export function getNonBitGoRecoveryForHotWalletsMPCv2(intendedChain = 'tpolygon'): any {
44+
return {
45+
userKey:
46+
'{"iv":"SXEAqti+qKFZwMmrKHaKdA==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' +
47+
':"ccm","adata":"","cipher":"aes","salt":"cI/PRDlqFjQ=","ct":"3S+T9khsU9aTlL\n' +
48+
'MUG3EgO65f2DAwktCgun7K/ct2XjTT2BJNrX1aOrF08jtMjRfbZpGqQm0t5O9NDD1jgs+F+mL1y\n' +
49+
'+uMabkpWaKS3L2VpKDE/ENeCZDR632FrqUwnRYCuHYkdUhdG3AeUBTxHCQFnZB5VTqMEEfYI3KB\n' +
50+
'zsuVSUPiUcvn0+UxCU2dXAUI7pINB3+d8/wAdKgRnCsjuggnEduzy9xLLiPb4sfNSOrB7HW9Ia/\n' +
51+
'JsgGZsn4tr744sGgZGUGW5ORfPYtXuRgvA/4dhjKeW0bnAzijtkQeo0M9jkNlR2+JF1fxBkyDYC\n' +
52+
'xvEYMbU/DwRxDIqmvjtHbMgcsrvATi24RaWrCv4Bh37qlPy7ZzfEIAAD8Gbj7U076a8VNeKh0cD\n' +
53+
'YiqrP3QxjqA58EEq2jluILAjD9KurB6gf7Y0bQxH9ew09Iaa+ABLy3EUQHwA3DwJ3Z2t3l5TT0y\n' +
54+
'O3C/kriclH75TlRlHvRWeSEzhWQMnVw0LAzTQTag4LrDaLCbjbk42oQXi6xvmeabOvgOLvfUh33\n' +
55+
'g24hVOqQWKIMjReH6db0xjBmuB8d4JdOi7/sgEc1gzSsgqtYTothtozP2FeJQtzOIvmC7MpIZf4\n' +
56+
'Uex/mXXsbazxfB09DwNqzV949KncW++dLScJ4SfzRhxLqHVY26EUGXbny0mQF1AljAGhiDVhjch\n' +
57+
'Sr6caTYVUytsDB98WLpg8e9v3lTrKi7r6oiHkB5Pztf7+0lcCHy8Ypmbg3cQxnQtloNno08whyJ\n' +
58+
'SDoTMbd6yp190AQs2qFzwhdboDp68DtkmLhPj3+HCqG9hJB1ZvXMml/ms+eKbiC5Hzy3dihz1xZ\n' +
59+
'wxXtBH244XHUsxBAL1bII9RDJCn9nVR5I2UhsjLUcSS9fmthDSYEh0LN/4LO1U5IEOtA8nh72fZ\n' +
60+
'y7ARkaccSHszKTiiGYbTY2JxnvQcZPrfup6YXWf8YNHxlL3Lh+LRrn++raj/Hq1RzmxcHON42Wn\n' +
61+
'7+1VB7134m7lQQNyWy9Jm7r4EWPgWDon6opzeCMnKgedFJUSRG0hqQJTXDxv9c5EQxp8Q7vuMzN\n' +
62+
'sd42g0axdNCDGfY+kHWdSkSdZK0hCaey0Qo="}\n',
63+
backupKey:
64+
'{"iv":"KBdFO8unJEB/e3tSaMEE0g==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' +
65+
':"ccm","adata":"","cipher":"aes","salt":"RTDMflomOaU=","ct":"lywTUj0TKnSEpc\n' +
66+
'vkMEpQ5GdEVtYX4GLm/ZKUyVpLekLYyNpudEZgMsZI7azkMElFJPu5aElLWPEGFauvr5X0gI6ia\n' +
67+
'8dpBejad98ce/0Mwf7PjxrXrKWZ7SEqHfPULHRtH7DG+Caa8jNBfx1Y3jPbeG03FMwg/npFbKYu\n' +
68+
'JeyqH10VZ2ZIgKSZtAAYstij41oOTFFwm8Ro99mTKC16hV/e+EaZR8VoWgpz66TYe78jVNoOBf0\n' +
69+
'3sGotz90VDa2VW3HXZxgRnsI6uxKFjr/LerjT0wKYF/ksxznzZtYFz7Q03/rv+9SgqkyfpGFWCU\n' +
70+
'CptSYMExnJXU+DGxHLVjmiksSa8vVATQAP4VT+dNZrVUA04WxzFZonl1Bh9ghZcfSXqrjfmwiLf\n' +
71+
'2+7kKOsWF3aGeQg/WUPcpBDdIYoB9u/FXP6ess50+uL8Mp4yeRN4V3XveinJ9UWmXMFfVK1h/jx\n' +
72+
'7+40+TlGiJl4AF1RlHK6Y+co7QUoahUtKQ+Ar2fXtEyoBydG5ziu47qHamok/99PdAjg57wCqzD\n' +
73+
'w3qFJrqoZUEXFfolZVX/UMKjzrRR/9ErhRN3zZceDuwdrc2sSVMi7aYgLEGjzN4I8yvgnKBDE7a\n' +
74+
'5drOHlcQDRvokhCC4tAsAF5VrxoQW0MwHzkAMXI+EY65z7IWmT4l3Cym1OyujERoBfj0MwmyUn7\n' +
75+
'SzfNgOisI+98r7gjxyrW94nXsR9H70j9TvJRgKydopmW+8HcYpqaJaflV11bcvzu1Ne7x2Xjifc\n' +
76+
'xlPi5f3p8IUIQfPH9wxVVOTlJV+rF1hEm3h4ZNMTUzc4Pym+RLIJ8nTNGZYZ0o5DtTFNxfyt8cK\n' +
77+
'CFrzdnB7zvFoXDpHio+x69Xvaejebql0cegvmaXbg0tRFsx9aNQjD8J32fVTLGzFvN+rQ3juYmn\n' +
78+
'PsNkcrKmx+bO453eOzZs28NN4XcjY4tE6DmCfFFdyfMkmnEOknWH8Iao+ALoRMopghTV4lnNGwQ\n' +
79+
'+VEJNbggLVfyiCNnQ4U2r08yOYpx1K6OQk8sUxtRzdYVcvBHtR9hfM5GUf2JIbpbBcVEd3a/0Lp\n' +
80+
'KqJARe6lmklikH7QdaqH095tQkw2mVQZpMw="}\n',
81+
bitgoKey:
82+
'02e75778dbb3988061f438b08d2eaf8d1bd3e6decc01b57ae35da45ba902eee1b34da5234e4d65c94fa4f8c3ac1313ca3f1f13f1262545d714249c349895eb9c01',
83+
walletPassphrase: TEST_RECOVERY_PASSCODE,
84+
walletContractAddress: TEST_POLYGON_WALLET_FIRST_ADDRESS,
85+
bitgoFeeAddress: '0xbd475757237154b522333fca4c565bc789b46193',
86+
recoveryDestination: '0xd5adde17fed8baed3f32b84af05b8f2816f7b560',
87+
bitgoDestinationAddress: '0xe5986ce4490deb67d2950562ceb930ddf9be7a14',
88+
eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 },
89+
gasLimit: 500000,
90+
intendedChain: intendedChain,
91+
};
92+
}
93+
94+
export function getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2(intendedChain = 'tpolygon'): any {
95+
const address = '0x3685e9831699279942c1190edbf2c33c2c3c156b';
96+
return {
97+
recoveryDestination: '0xd5adde17fed8baed3f32b84af05b8f2816f7b560',
98+
bitgoDestinationAddress: '0xe5986ce4490deb67d2950562ceb930ddf9be7a14',
99+
walletContractAddress: TEST_POLYGON_WALLET_FIRST_ADDRESS,
100+
eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 },
101+
gasLimit: 500000,
102+
intendedChain: intendedChain,
103+
address: address,
104+
amount: '100000000000000000', // 0.1 MATIC
105+
commonKeyChain:
106+
'03a139ba3bf49de33675ea869baaab8f3225afffc96835c30db401c53b335476dddf0ad919ad8cfa92967a20879753a42fd3df1d6eb0824f0294a7838faa80e963',
107+
};
108+
}
109+
110+
export function getInvalidNonBitGoRecoveryParams(): any {
111+
return {
112+
...getNonBitGoRecoveryForHotWalletsMPCv2(),
113+
userKey: 'invalidUserKey',
114+
};
115+
}
116+
39117
export const getContractCallRequest = {
40118
module: 'proxy',
41119
action: 'eth_call',

modules/sdk-coin-polygon/test/unit/polygon.ts

Lines changed: 151 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import nock from 'nock';
77
import * as secp256k1 from 'secp256k1';
88
import * as should from 'should';
99
import { Polygon, Tpolygon, TransactionBuilder, TransferBuilder } from '../../src';
10+
import { AbstractEthLikeNewCoins, UnsignedSweepTxMPCv2, OfflineVaultTxInfo, optionalDeps } from '@bitgo/abstract-eth';
1011
import { getBuilder } from '../getBuilder';
1112
import * as mockData from '../fixtures/polygon';
12-
import { OfflineVaultTxInfo, optionalDeps } from '@bitgo/abstract-eth';
1313
import * as sjcl from '@bitgo/sjcl';
14+
import assert from 'assert';
1415

1516
nock.enableNetConnect();
1617

@@ -756,53 +757,6 @@ describe('Polygon', function () {
756757
recovery.should.have.property('tx');
757758
});
758759

759-
it('should construct an unsigned sweep tx with TSS', async function () {
760-
const backupKeyAddress = '0xe7406dc43d13f698fb41a345c7783d39a4c2d191';
761-
nock(baseUrl)
762-
.get('/api')
763-
.query(mockData.getTxListRequest(backupKeyAddress))
764-
.reply(200, mockData.getTxListResponse);
765-
nock(baseUrl)
766-
.get('/api')
767-
.query(mockData.getBalanceRequest(backupKeyAddress))
768-
.reply(200, mockData.getBalanceResponse);
769-
770-
const basecoin = bitgo.coin('tpolygon') as Polygon;
771-
772-
const userKey = '03f8606a595917de4cf2244e27b7fba172505469392ad385d2dd2b3588a6bb878c';
773-
const backupKey = '03f8606a595917de4cf2244e27b7fba172505469392ad385d2dd2b3588a6bb878c';
774-
775-
const recoveryParams = {
776-
userKey: userKey,
777-
backupKey: backupKey,
778-
walletContractAddress: '0xe7406dc43d13f698fb41a345c7783d39a4c2d191',
779-
recoveryDestination: '0xac05da78464520aa7c9d4c19bd7a440b111b3054',
780-
walletPassphrase: TestBitGo.V2.TEST_RECOVERY_PASSCODE,
781-
isTss: true,
782-
gasPrice: 20000000000,
783-
gasLimit: 500000,
784-
replayProtectionOptions: {
785-
chain: 80002,
786-
hardfork: 'london',
787-
},
788-
};
789-
790-
const transaction = (await basecoin.recover(recoveryParams)) as OfflineVaultTxInfo;
791-
should.exist(transaction);
792-
transaction.should.have.property('tx');
793-
transaction.should.have.property('expireTime');
794-
transaction.should.have.property('gasLimit');
795-
transaction.gasLimit.should.equal('500000');
796-
transaction.should.have.property('gasPrice');
797-
transaction.gasPrice.should.equal('20000000000');
798-
transaction.should.have.property('recipient');
799-
const recipient = (transaction as any).recipient as Recipient;
800-
recipient.should.have.property('address');
801-
recipient.address.should.equal('0xac05da78464520aa7c9d4c19bd7a440b111b3054');
802-
recipient.should.have.property('amount');
803-
recipient.amount.should.equal('9989999999999999928');
804-
});
805-
806760
it('should be able to second sign', async function () {
807761
const walletContractAddress = TestBitGo.V2.TEST_ETH_WALLET_FIRST_ADDRESS as string;
808762
const backupKeyAddress = '0x4f2c4830cc37f2785c646f89ded8a919219fa0e9';
@@ -1212,4 +1166,153 @@ describe('Polygon', function () {
12121166
transaction.halfSigned?.should.have.property('recipients');
12131167
});
12141168
});
1169+
1170+
describe('Non-BitGo Recovery for Hot Wallets (MPCv2)', function () {
1171+
const baseUrl = 'https://api-amoy.polygonscan.com';
1172+
1173+
it('should build a recovery transaction for MPCv2 kind of hot wallets', async function () {
1174+
nock(baseUrl)
1175+
.get('/api')
1176+
.query(mockData.getTxListRequest(mockData.getNonBitGoRecoveryForHotWalletsMPCv2().bitgoFeeAddress))
1177+
.reply(200, mockData.getTxListResponse);
1178+
1179+
nock(baseUrl)
1180+
.get('/api')
1181+
.query(mockData.getBalanceRequest(mockData.getNonBitGoRecoveryForHotWalletsMPCv2().bitgoFeeAddress))
1182+
.reply(200, mockData.getBalanceResponse);
1183+
1184+
nock(baseUrl)
1185+
.get('/api')
1186+
.query(mockData.getBalanceRequest(mockData.getNonBitGoRecoveryForHotWalletsMPCv2().walletContractAddress))
1187+
.reply(200, mockData.getBalanceResponse);
1188+
1189+
const params = mockData.getNonBitGoRecoveryForHotWalletsMPCv2();
1190+
1191+
const transaction = await (basecoin as AbstractEthLikeNewCoins).recover({
1192+
userKey: params.userKey,
1193+
backupKey: params.backupKey,
1194+
walletPassphrase: params.walletPassphrase,
1195+
walletContractAddress: params.walletContractAddress,
1196+
bitgoFeeAddress: params.bitgoFeeAddress,
1197+
recoveryDestination: params.recoveryDestination,
1198+
eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 },
1199+
gasLimit: 500000,
1200+
isTss: true,
1201+
bitgoDestinationAddress: params.bitgoDestinationAddress,
1202+
replayProtectionOptions: { chain: 80002, hardfork: 'london' },
1203+
intendedChain: params.intendedChain,
1204+
});
1205+
should.exist(transaction);
1206+
transaction.should.have.property('tx');
1207+
});
1208+
it('should throw an error for invalid user key', async function () {
1209+
const params = mockData.getInvalidNonBitGoRecoveryParams();
1210+
1211+
await assert.rejects(
1212+
async () => {
1213+
await (basecoin as AbstractEthLikeNewCoins).recover({
1214+
userKey: params.userKey,
1215+
backupKey: params.backupKey,
1216+
walletPassphrase: params.walletPassphrase,
1217+
walletContractAddress: params.walletContractAddress,
1218+
bitgoFeeAddress: params.bitgoFeeAddress,
1219+
recoveryDestination: params.recoveryDestination,
1220+
eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 },
1221+
gasLimit: 500000,
1222+
bitgoDestinationAddress: params.bitgoDestinationAddress,
1223+
intendedChain: params.intendedChain,
1224+
});
1225+
},
1226+
Error,
1227+
'user key is invalid'
1228+
);
1229+
});
1230+
});
1231+
1232+
describe('Build Unsigned Sweep for Self-Custody Cold Wallets (MPCv2)', function () {
1233+
const baseUrl = 'https://api-amoy.polygonscan.com';
1234+
1235+
it('should generate an unsigned sweep without derivation seed', async function () {
1236+
nock(baseUrl)
1237+
.get('/api')
1238+
.query(mockData.getTxListRequest(mockData.getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2().address))
1239+
.reply(200, mockData.getTxListResponse);
1240+
1241+
nock(baseUrl)
1242+
.get('/api')
1243+
.query(mockData.getBalanceRequest(mockData.getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2().address))
1244+
.reply(200, mockData.getBalanceResponse);
1245+
1246+
nock(baseUrl)
1247+
.get('/api')
1248+
.query(
1249+
mockData.getBalanceRequest(
1250+
mockData.getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2().walletContractAddress
1251+
)
1252+
)
1253+
.reply(200, mockData.getBalanceResponse);
1254+
1255+
const params = mockData.getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2();
1256+
const sweepResult = await (basecoin as AbstractEthLikeNewCoins).recover({
1257+
userKey: params.commonKeyChain,
1258+
backupKey: params.commonKeyChain,
1259+
recoveryDestination: params.recoveryDestination,
1260+
gasLimit: 200000,
1261+
eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 },
1262+
walletContractAddress: params.walletContractAddress,
1263+
isTss: true,
1264+
replayProtectionOptions: {
1265+
chain: '137',
1266+
hardfork: 'london',
1267+
},
1268+
});
1269+
1270+
should.exist(sweepResult);
1271+
const output = sweepResult as UnsignedSweepTxMPCv2;
1272+
output.should.have.property('txRequests');
1273+
output.txRequests.should.have.length(1);
1274+
output.txRequests[0].should.have.property('transactions');
1275+
output.txRequests[0].transactions.should.have.length(1);
1276+
output.txRequests[0].should.have.property('walletCoin');
1277+
output.txRequests[0].transactions[0].should.have.property('unsignedTx');
1278+
output.txRequests[0].transactions[0].unsignedTx.should.have.property('serializedTxHex');
1279+
output.txRequests[0].transactions[0].unsignedTx.should.have.property('signableHex');
1280+
output.txRequests[0].transactions[0].unsignedTx.should.have.property('derivationPath');
1281+
output.txRequests[0].transactions[0].unsignedTx.should.have.property('feeInfo');
1282+
output.txRequests[0].transactions[0].unsignedTx.should.have.property('parsedTx');
1283+
const parsedTx = output.txRequests[0].transactions[0].unsignedTx.parsedTx as { spendAmount: string };
1284+
parsedTx.should.have.property('spendAmount');
1285+
(output.txRequests[0].transactions[0].unsignedTx.parsedTx as { outputs: any[] }).should.have.property('outputs');
1286+
});
1287+
1288+
it('should throw an error for invalid address', async function () {
1289+
const params = mockData.getBuildUnsignedSweepForSelfCustodyColdWalletsMPCv2();
1290+
params.recoveryDestination = 'invalidAddress';
1291+
1292+
params.userKey =
1293+
'0234eb39b22fed523ece7c78da29ba1f1de5b64a6e48013e0914de793bc1df0570e779de04758732734d97e54b782c8b336283811af6a2c57bd81438798e1c2446';
1294+
params.backupKey =
1295+
'0234eb39b22fed523ece7c78da29ba1f1de5b64a6e48013e0914de793bc1df0570e779de04758732734d97e54b782c8b336283811af6a2c57bd81438798e1c2446';
1296+
1297+
await assert.rejects(
1298+
async () => {
1299+
await (basecoin as AbstractEthLikeNewCoins).recover({
1300+
recoveryDestination: params.recoveryDestination,
1301+
gasLimit: 2000,
1302+
eip1559: { maxFeePerGas: 200, maxPriorityFeePerGas: 10000 },
1303+
userKey: params.userKey,
1304+
backupKey: params.backupKey,
1305+
walletContractAddress: params.walletContractAddress,
1306+
isTss: true,
1307+
replayProtectionOptions: {
1308+
chain: '137',
1309+
hardfork: 'london',
1310+
},
1311+
});
1312+
},
1313+
Error,
1314+
'Error: invalid address'
1315+
);
1316+
});
1317+
});
12151318
});

0 commit comments

Comments
 (0)