Skip to content

Commit 6b58624

Browse files
Merge branch 'master' into rel/latest-back-merge
2 parents 0d0f6c8 + 5f5a391 commit 6b58624

File tree

25 files changed

+630
-105
lines changed

25 files changed

+630
-105
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2542,7 +2542,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
25422542
!txParams?.recipients &&
25432543
!(
25442544
txParams.prebuildTx?.consolidateId ||
2545-
(txParams.type && ['acceleration', 'fillNonce', 'transferToken'].includes(txParams.type))
2545+
(txParams.type && ['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(txParams.type))
25462546
)
25472547
) {
25482548
throw new Error(`missing txParams`);

modules/abstract-utxo/src/sign.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function signAndVerifyPsbt(
6464
signerKeychain: utxolib.BIP32Interface,
6565
{
6666
isLastSignature,
67+
/** deprecated */
6768
allowNonSegwitSigningWithoutPrevTx,
6869
}: { isLastSignature: boolean; allowNonSegwitSigningWithoutPrevTx?: boolean }
6970
): utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint> {
@@ -85,11 +86,7 @@ export function signAndVerifyPsbt(
8586
}
8687

8788
try {
88-
utxolib.bitgo.withUnsafeNonSegwit(
89-
psbt,
90-
() => psbt.signInputHD(inputIndex, signerKeychain),
91-
!!allowNonSegwitSigningWithoutPrevTx
92-
);
89+
psbt.signInputHD(inputIndex, signerKeychain);
9390
debug('Successfully signed input %d of %d', inputIndex + 1, psbt.data.inputs.length);
9491
} catch (e) {
9592
return new InputSigningError<bigint>(inputIndex, { id: outputId }, e);
@@ -111,13 +108,7 @@ export function signAndVerifyPsbt(
111108

112109
const outputId = outputIds[inputIndex];
113110
try {
114-
if (
115-
!utxolib.bitgo.withUnsafeNonSegwit(
116-
psbt,
117-
() => psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain),
118-
!!allowNonSegwitSigningWithoutPrevTx
119-
)
120-
) {
111+
if (!psbt.validateSignaturesOfInputHD(inputIndex, signerKeychain)) {
121112
return new InputSigningError(inputIndex, { id: outputId }, new Error(`invalid signature`));
122113
}
123114
} catch (e) {

modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export async function signTransaction<TNumber extends number | bigint>(
2727
txInfo: { unspents?: utxolib.bitgo.Unspent<TNumber>[] } | undefined;
2828
isLastSignature: boolean;
2929
signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined;
30+
/** deprecated */
3031
allowNonSegwitSigningWithoutPrevTx: boolean;
3132
pubs: string[] | undefined;
3233
cosignerPub: string | undefined;
@@ -47,19 +48,11 @@ export async function signTransaction<TNumber extends number | bigint>(
4748
isLastSignature = params.isLastSignature;
4849
}
4950

50-
const setSignerMusigNonceWithOverride = (
51-
psbt: utxolib.bitgo.UtxoPsbt,
52-
signerKeychain: utxolib.BIP32Interface,
53-
nonSegwitOverride: boolean
54-
) => {
55-
utxolib.bitgo.withUnsafeNonSegwit(psbt, () => psbt.setAllInputsMusig2NonceHD(signerKeychain), nonSegwitOverride);
56-
};
57-
5851
if (tx instanceof bitgo.UtxoPsbt && isTxWithKeyPathSpendInput) {
5952
switch (params.signingStep) {
6053
case 'signerNonce':
6154
assert(signerKeychain);
62-
setSignerMusigNonceWithOverride(tx, signerKeychain, params.allowNonSegwitSigningWithoutPrevTx);
55+
tx.setAllInputsMusig2NonceHD(signerKeychain);
6356
PSBT_CACHE.set(tx.getUnsignedTx().getId(), tx);
6457
return { txHex: tx.toHex() };
6558
case 'cosignerNonce':
@@ -80,7 +73,7 @@ export async function signTransaction<TNumber extends number | bigint>(
8073
// this instance is not an external signer
8174
assert(params.walletId, 'walletId is required for MuSig2 bitgo nonce');
8275
assert(signerKeychain);
83-
setSignerMusigNonceWithOverride(tx, signerKeychain, params.allowNonSegwitSigningWithoutPrevTx);
76+
tx.setAllInputsMusig2NonceHD(signerKeychain);
8477
const response = await coin.signPsbt(tx.toHex(), params.walletId);
8578
tx.combine(bitgo.createPsbtFromHex(response.psbt, coin.network));
8679
break;
@@ -102,7 +95,6 @@ export async function signTransaction<TNumber extends number | bigint>(
10295
assert(signerKeychain);
10396
signedTransaction = signAndVerifyPsbt(tx, signerKeychain, {
10497
isLastSignature,
105-
allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx,
10698
});
10799
} else {
108100
if (tx.ins.length !== params.txInfo?.unspents?.length) {

modules/bitgo/test/v2/unit/wallet.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3180,6 +3180,69 @@ describe('V2 Wallet:', function () {
31803180
intent.intentType.should.equal('fillNonce');
31813181
});
31823182

3183+
it('populate intent should return valid eth tokenApproval intent', async function () {
3184+
const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
3185+
const feeOptions = {
3186+
maxFeePerGas: 3000000000,
3187+
maxPriorityFeePerGas: 2000000000,
3188+
};
3189+
const tokenName = 'usdc';
3190+
3191+
const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
3192+
reqId,
3193+
intentType: 'tokenApproval',
3194+
tokenName,
3195+
feeOptions,
3196+
});
3197+
3198+
intent.should.have.property('recipients', undefined);
3199+
intent.feeOptions!.should.deepEqual(feeOptions);
3200+
intent.tokenName!.should.equal(tokenName);
3201+
intent.intentType.should.equal('tokenApproval');
3202+
});
3203+
3204+
it('populate intent should return valid polygon tokenApproval intent', async function () {
3205+
const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('tpolygon'));
3206+
const feeOptions = {
3207+
maxFeePerGas: 3000000000,
3208+
maxPriorityFeePerGas: 2000000000,
3209+
};
3210+
const tokenName = 'usdt';
3211+
3212+
const intent = mpcUtils.populateIntent(bitgo.coin('tpolygon'), {
3213+
reqId,
3214+
intentType: 'tokenApproval',
3215+
tokenName,
3216+
feeOptions,
3217+
});
3218+
3219+
intent.should.have.property('recipients', undefined);
3220+
intent.feeOptions!.should.deepEqual(feeOptions);
3221+
intent.tokenName!.should.equal(tokenName);
3222+
intent.intentType.should.equal('tokenApproval');
3223+
});
3224+
3225+
it('populate intent should return valid bsc tokenApproval intent', async function () {
3226+
const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('tbsc'));
3227+
const feeOptions = {
3228+
maxFeePerGas: 3000000000,
3229+
maxPriorityFeePerGas: 2000000000,
3230+
};
3231+
const tokenName = 'busd';
3232+
3233+
const intent = mpcUtils.populateIntent(bitgo.coin('tbsc'), {
3234+
reqId,
3235+
intentType: 'tokenApproval',
3236+
tokenName,
3237+
feeOptions,
3238+
});
3239+
3240+
intent.should.have.property('recipients', undefined);
3241+
intent.feeOptions!.should.deepEqual(feeOptions);
3242+
intent.tokenName!.should.equal(tokenName);
3243+
intent.intentType.should.equal('tokenApproval');
3244+
});
3245+
31833246
it('should populate intent with custodianTransactionId', async function () {
31843247
const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
31853248
const feeOptions = {
@@ -4942,4 +5005,99 @@ describe('V2 Wallet:', function () {
49425005
.should.be.rejectedWith('invalid argument for amount - Integer greater than zero or numeric string expected');
49435006
});
49445007
});
5008+
5009+
describe('Token Approval', function () {
5010+
let ethWallet;
5011+
const tokenName = 'usdc';
5012+
const walletPassphrase = 'test_passphrase';
5013+
5014+
before(async function () {
5015+
const walletData = {
5016+
id: '598f606cd8fc24710d2ebadb1d9459bb',
5017+
coin: 'teth',
5018+
keys: [
5019+
'598f606cd8fc24710d2ebad89dce86c2',
5020+
'598f606cc8e43aef09fcb785221d9dd2',
5021+
'5935d59cf660764331bafcade1855fd7',
5022+
],
5023+
};
5024+
ethWallet = new Wallet(bitgo, bitgo.coin('teth'), walletData);
5025+
});
5026+
5027+
it('should successfully approve ERC20 token', async function () {
5028+
const mockTokenApprovalBuild = {
5029+
txPrebuild: {
5030+
txHex: '0x1234567890abcdef',
5031+
txInfo: {
5032+
gasPrice: 20000000000,
5033+
gasLimit: 60000,
5034+
nonce: 1,
5035+
},
5036+
},
5037+
coin: 'teth',
5038+
tokenName: tokenName,
5039+
};
5040+
5041+
const mockSignedTransaction = {
5042+
txHex: '0xabcdef1234567890',
5043+
halfSigned: {
5044+
txHex: '0xabcdef1234567890',
5045+
},
5046+
};
5047+
5048+
const mockSentTransaction = {
5049+
hash: '0x9876543210fedcba',
5050+
status: 'signed',
5051+
};
5052+
5053+
// Mock the token approval build endpoint
5054+
const buildScope = nock(bgUrl)
5055+
.post(`/api/v2/teth/wallet/${ethWallet.id()}/token/approval/build`)
5056+
.reply(200, mockTokenApprovalBuild);
5057+
5058+
// Mock getKeychainsAndValidatePassphrase
5059+
sinon.stub(ethWallet, 'getKeychainsAndValidatePassphrase').resolves([
5060+
{
5061+
id: '598f606cd8fc24710d2ebad89dce86c2',
5062+
prv: 'test_private_key',
5063+
},
5064+
]);
5065+
5066+
// Mock signTransaction
5067+
sinon.stub(ethWallet, 'signTransaction').resolves(mockSignedTransaction);
5068+
5069+
// Mock sendTransaction
5070+
sinon.stub(ethWallet, 'sendTransaction').resolves(mockSentTransaction);
5071+
5072+
const result = await ethWallet.approveErc20Token(walletPassphrase, tokenName);
5073+
5074+
result.should.equal(mockSentTransaction);
5075+
buildScope.isDone().should.be.true();
5076+
});
5077+
5078+
it('should handle token approval build failure', async function () {
5079+
const buildScope = nock(bgUrl)
5080+
.post(`/api/v2/teth/wallet/${ethWallet.id()}/token/approval/build`)
5081+
.reply(400, { error: 'Invalid token name' });
5082+
5083+
sinon.stub(ethWallet, 'getKeychainsAndValidatePassphrase').resolves([
5084+
{
5085+
id: '598f606cd8fc24710d2ebad89dce86c2',
5086+
prv: 'test_private_key',
5087+
},
5088+
]);
5089+
5090+
await ethWallet.approveErc20Token(walletPassphrase, 'invalid_token').should.be.rejectedWith('Invalid token name');
5091+
5092+
buildScope.isDone().should.be.true();
5093+
});
5094+
5095+
it('should validate required parameters for approveErc20Token', async function () {
5096+
await ethWallet.approveErc20Token('', tokenName).should.be.rejectedWith();
5097+
});
5098+
5099+
afterEach(function () {
5100+
sinon.restore();
5101+
});
5102+
});
49455103
});

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,17 @@ export class BitGoAPI implements BitGoBase {
258258
}
259259
});
260260

261+
if (params.evm) {
262+
const evmConfig = common.Environments[env]['evm'] || {};
263+
Object.keys(params.evm).forEach((key) => {
264+
if (params.evm?.[key] && params.evm[key]['apiToken']) {
265+
evmConfig[key] = evmConfig[key] || {};
266+
evmConfig[key]['apiToken'] = params.evm[key]['apiToken'];
267+
}
268+
});
269+
common.Environments[env]['evm'] = evmConfig;
270+
}
271+
261272
common.setNetwork(common.Environments[env].network);
262273

263274
this._baseApiUrl = this._baseUrl + '/api/v1';

modules/sdk-api/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export interface BitGoAPIOptions {
4646
validate?: boolean;
4747
cookiesPropagationEnabled?: boolean;
4848
getAdditionalHeadersCb?: AdditionalHeadersCallback;
49+
evm?: {
50+
[key: string]: {
51+
baseUrl: string;
52+
apiToken?: string;
53+
};
54+
};
4955
}
5056

5157
export interface AccessTokenOptions {

modules/sdk-core/src/bitgo/utils/mpcUtils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export abstract class MpcUtils {
117117
populateIntent(baseCoin: IBaseCoin, params: PrebuildTransactionWithIntentOptions): PopulatedIntent {
118118
const chain = this.baseCoin.getChain();
119119

120-
if (!['acceleration', 'fillNonce', 'transferToken'].includes(params.intentType)) {
120+
if (!['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(params.intentType)) {
121121
assert(params.recipients, `'recipients' is a required parameter for ${params.intentType} intent`);
122122
}
123123
const intentRecipients = params.recipients?.map((recipient) => {
@@ -153,6 +153,7 @@ export abstract class MpcUtils {
153153
comment: params.comment,
154154
nonce: params.nonce,
155155
recipients: intentRecipients,
156+
tokenName: params.tokenName,
156157
};
157158

158159
if (baseCoin.getFamily() === 'eth' || baseCoin.getFamily() === 'polygon' || baseCoin.getFamily() === 'bsc') {
@@ -177,6 +178,12 @@ export abstract class MpcUtils {
177178
receiveAddress: params.receiveAddress,
178179
feeOptions: params.feeOptions,
179180
};
181+
case 'tokenApproval':
182+
return {
183+
...baseIntent,
184+
tokenName: params.tokenName,
185+
feeOptions: params.feeOptions,
186+
};
180187
default:
181188
throw new Error(`Unsupported intent type ${params.intentType}`);
182189
}

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export interface PopulatedIntent extends PopulatedIntentBase {
256256
receiveAddress?: string;
257257
custodianTransactionId?: string;
258258
custodianMessageId?: string;
259+
tokenName?: string;
259260
}
260261

261262
export type TxRequestState =

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3355,6 +3355,17 @@ export class Wallet implements IWallet {
33553355
params.preview
33563356
);
33573357
break;
3358+
case 'tokenApproval':
3359+
txRequest = await this.tssUtils!.prebuildTxWithIntent(
3360+
{
3361+
reqId,
3362+
intentType: 'tokenApproval',
3363+
tokenName: params.tokenName,
3364+
},
3365+
apiVersion,
3366+
params.preview
3367+
);
3368+
break;
33583369
default:
33593370
throw new Error(`transaction type not supported: ${params.type}`);
33603371
}

0 commit comments

Comments
 (0)