Skip to content

Commit b98851f

Browse files
feat: add approveToken function for erc20 sendMany
TICKET: COIN-2785
1 parent 9f0b266 commit b98851f

File tree

4 files changed

+262
-0
lines changed

4 files changed

+262
-0
lines changed

examples/ts/eth/approve-erc20token.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Approve an ERC20 token for use with a batcher contract at BitGo.
3+
*
4+
* Copyright 2023, BitGo, Inc. All Rights Reserved.
5+
*/
6+
import { BitGoAPI } from '@bitgo/sdk-api';
7+
import { Topeth } from '@bitgo/sdk-coin-opeth';
8+
require('dotenv').config({ path: '../../../.env' });
9+
10+
const bitgo = new BitGoAPI({
11+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
12+
env: 'test',
13+
});
14+
15+
const coin = 'topeth';
16+
bitgo.register(coin, Topeth.createInstance);
17+
18+
const walletId = process.env.TESTNET_ETH_WALLET_ID;
19+
const walletPassphrase = process.env.TESTNET_ETH_WALLET_PASSPHRASE;
20+
const tokenName = 'topeth:terc18dp'; // Replace with the token you want to approve
21+
22+
async function main() {
23+
if (!walletId) {
24+
throw new Error('Please set TESTNET_ETH_WALLET_ID environment variable');
25+
}
26+
if (!walletPassphrase) {
27+
throw new Error('Please set TESTNET_ETH_WALLET_PASSPHRASE environment variable');
28+
}
29+
30+
const walletInstance = await bitgo.coin(coin).wallets().get({ id: walletId });
31+
32+
console.log('Wallet ID:', walletInstance.id());
33+
console.log('Current Receive Address:', walletInstance.receiveAddress());
34+
35+
console.log(`Approving token ${tokenName} for use with batcher contract...`);
36+
37+
try {
38+
const approvalTransaction = await walletInstance.approveErc20Token(walletPassphrase, tokenName);
39+
40+
console.log('Token Approval Transaction:', JSON.stringify(approvalTransaction, null, 4));
41+
console.log('Transaction ID:', approvalTransaction.txid);
42+
console.log('Status:', approvalTransaction.status);
43+
} catch (e) {
44+
console.error('Error approving token:', e.message);
45+
if (e.stack) {
46+
console.error(e.stack);
47+
}
48+
}
49+
}
50+
51+
main().catch((e) => console.error(e));

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

+165
Original file line numberDiff line numberDiff line change
@@ -4681,4 +4681,169 @@ describe('V2 Wallet:', function () {
46814681
await adaWallet.sendMany(sendManyParams).should.be.resolved();
46824682
});
46834683
});
4684+
4685+
describe('ERC20 Token Approval', function () {
4686+
let wallet;
4687+
const walletId = '5b34252f1bf349930e34020a00000000';
4688+
let topethCoin;
4689+
const tokenName = 'topeth:terc18dp';
4690+
const coin = 'topeth';
4691+
4692+
beforeEach(function () {
4693+
topethCoin = bitgo.coin('topeth');
4694+
wallet = new Wallet(bitgo, topethCoin, {
4695+
id: walletId,
4696+
coin: coin,
4697+
keys: ['keyid1', 'keyid2', 'keyid3'],
4698+
});
4699+
});
4700+
4701+
afterEach(function () {
4702+
sinon.restore();
4703+
});
4704+
4705+
it('should successfully approve a token', async function () {
4706+
const walletPassphrase = 'password123';
4707+
const expectedApprovalBuild = {
4708+
txHex: '0x123456',
4709+
feeInfo: {
4710+
fee: '1000000000',
4711+
},
4712+
};
4713+
4714+
const expectedSignedTx = {
4715+
txHex: '0x123456signed',
4716+
};
4717+
4718+
const expectedSendResult = {
4719+
txid: '0xabcdef',
4720+
tx: '0x123456signed',
4721+
status: 'signed',
4722+
};
4723+
4724+
// Mock the token approval build API
4725+
const ethUrl = common.Environments[bitgo.getEnv()].uri;
4726+
nock(ethUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);
4727+
4728+
// Mock the getKeychainsAndValidatePassphrase method
4729+
sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
4730+
{
4731+
id: 'keyid1',
4732+
pub: 'pub1',
4733+
encryptedPrv: 'encryptedPrv',
4734+
},
4735+
]);
4736+
4737+
// Mock the sign transaction method
4738+
sinon.stub(wallet, 'signTransaction').resolves(expectedSignedTx);
4739+
4740+
// Mock the send transaction method
4741+
sinon.stub(wallet, 'sendTransaction').resolves(expectedSendResult);
4742+
4743+
const result = await wallet.approveToken(walletPassphrase, tokenName);
4744+
4745+
should.exist(result);
4746+
result.should.deepEqual(expectedSendResult);
4747+
4748+
// Verify the parameters passed to signTransaction
4749+
const signParams = wallet.signTransaction.firstCall.args[0];
4750+
signParams.should.have.property('txPrebuild', expectedApprovalBuild);
4751+
signParams.should.have.property('keychain');
4752+
signParams.should.have.property('walletPassphrase', walletPassphrase);
4753+
});
4754+
4755+
it('should handle token approval build API errors', async function () {
4756+
const walletPassphrase = 'password123';
4757+
const errorMessage = 'token approval build failed';
4758+
4759+
// Mock the token approval build API to return an error
4760+
nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).replyWithError(errorMessage);
4761+
4762+
await wallet.approveToken(walletPassphrase, tokenName).should.be.rejectedWith(errorMessage);
4763+
});
4764+
4765+
it('should handle wallet passphrase validation errors', async function () {
4766+
const walletPassphrase = 'wrong-password';
4767+
const expectedApprovalBuild = {
4768+
txHex: '0x123456',
4769+
feeInfo: {
4770+
fee: '1000000000',
4771+
},
4772+
};
4773+
4774+
// Mock the token approval build API
4775+
nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);
4776+
4777+
// Mock the getKeychainsAndValidatePassphrase method to throw an error
4778+
const error = new Error('unable to decrypt keychain with the given wallet passphrase');
4779+
error.name = 'wallet_passphrase_incorrect';
4780+
sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').rejects(error);
4781+
4782+
await wallet
4783+
.approveToken(walletPassphrase, tokenName)
4784+
.should.be.rejectedWith('unable to decrypt keychain with the given wallet passphrase');
4785+
});
4786+
4787+
it('should handle signing errors', async function () {
4788+
const walletPassphrase = 'password123';
4789+
const expectedApprovalBuild = {
4790+
txHex: '0x123456',
4791+
feeInfo: {
4792+
fee: '1000000000',
4793+
},
4794+
};
4795+
const signingError = new Error('signing failed');
4796+
4797+
// Mock the token approval build API
4798+
nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);
4799+
4800+
// Mock the getKeychainsAndValidatePassphrase method
4801+
sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
4802+
{
4803+
id: 'keyid1',
4804+
pub: 'pub1',
4805+
encryptedPrv: 'encryptedPrv',
4806+
},
4807+
]);
4808+
4809+
// Mock the sign transaction method to throw an error
4810+
sinon.stub(wallet, 'signTransaction').rejects(signingError);
4811+
4812+
await wallet.approveToken(walletPassphrase, tokenName).should.be.rejectedWith(signingError);
4813+
});
4814+
4815+
it('should handle send transaction errors', async function () {
4816+
const walletPassphrase = 'password123';
4817+
const expectedApprovalBuild = {
4818+
txHex: '0x123456',
4819+
feeInfo: {
4820+
fee: '1000000000',
4821+
},
4822+
};
4823+
const expectedSignedTx = {
4824+
txHex: '0x123456signed',
4825+
};
4826+
const sendError = new Error('send failed');
4827+
4828+
// Mock the token approval build API
4829+
nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);
4830+
4831+
// Mock the getKeychainsAndValidatePassphrase method
4832+
sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
4833+
{
4834+
id: 'keyid1',
4835+
pub: 'pub1',
4836+
encryptedPrv: 'encryptedPrv',
4837+
},
4838+
]);
4839+
4840+
// Mock the sign transaction method
4841+
sinon.stub(wallet, 'signTransaction').resolves(expectedSignedTx);
4842+
4843+
// Mock the send transaction method to throw an error
4844+
sinon.stub(wallet, 'sendTransaction').rejects(sendError);
4845+
4846+
await wallet.approveToken(walletPassphrase, tokenName).should.be.rejectedWith(sendError);
4847+
});
4848+
});
46844849
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { SerializedNtilde } from '../../account-lib/mpc/tss/ecdsa/types';
3232
import { IAddressBook } from '../address-book';
3333
import { WalletUser } from '@bitgo/public-types';
34+
import { SubmitTransactionResponse } from '../inscriptionBuilder';
3435

3536
export interface MaximumSpendableOptions {
3637
minValue?: number | string;
@@ -899,4 +900,5 @@ export interface IWallet {
899900
fetchCrossChainUTXOs(params: FetchCrossChainUTXOsOptions): Promise<CrossChainUTXO[]>;
900901
getChallengesForEcdsaSigning(): Promise<WalletEcdsaChallenges>;
901902
getNftBalances(): Promise<NftBalance[]>;
903+
approveErc20Token(walletPassphrase: string, tokenName: string): Promise<SubmitTransactionResponse>;
902904
}

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

+44
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import { AddressBook, IAddressBook } from '../address-book';
112112
import { IRequestTracer } from '../../api';
113113
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
114114
import { getLightningAuthKey } from '../lightning/lightningWalletUtil';
115+
import { SubmitTransactionResponse } from '../inscriptionBuilder';
115116

116117
const debug = require('debug')('bitgo:v2:wallet');
117118

@@ -3739,4 +3740,47 @@ export class Wallet implements IWallet {
37393740
}
37403741
return keychains;
37413742
}
3743+
3744+
/**
3745+
* Approve token for use with a batcher contract
3746+
* This function builds, signs, and sends a token approval transaction
3747+
*
3748+
* @param {string} walletPassphrase - The passphrase to be used to decrypt the user key
3749+
* @param {string} tokenName - The name of the token to be approved
3750+
* @returns {Promise<any>} The transaction details
3751+
*/
3752+
async approveErc20Token(walletPassphrase: string, tokenName: string): Promise<SubmitTransactionResponse> {
3753+
const reqId = new RequestTracer();
3754+
this.bitgo.setRequestTracer(reqId);
3755+
3756+
let tokenApprovalBuild;
3757+
try {
3758+
const url = this.baseCoin.url(`/wallet/${this.id()}/token/approval/build`);
3759+
tokenApprovalBuild = await this.bitgo
3760+
.post(url)
3761+
.send({
3762+
tokenName: tokenName,
3763+
})
3764+
.result();
3765+
} catch (e) {
3766+
throw e;
3767+
}
3768+
3769+
const keychains = await this.getKeychainsAndValidatePassphrase({
3770+
reqId,
3771+
walletPassphrase,
3772+
});
3773+
3774+
const signingParams = {
3775+
txPrebuild: tokenApprovalBuild,
3776+
keychain: keychains[0],
3777+
walletPassphrase,
3778+
reqId,
3779+
};
3780+
3781+
const halfSignedTransaction = await this.signTransaction(signingParams);
3782+
const finalTxParams = _.extend({}, halfSignedTransaction);
3783+
3784+
return this.sendTransaction(finalTxParams, reqId);
3785+
}
37423786
}

0 commit comments

Comments
 (0)