diff --git a/packages/bitcore-client/bin/wallet b/packages/bitcore-client/bin/wallet index 1e7b69c33ec..88c948160e1 100755 --- a/packages/bitcore-client/bin/wallet +++ b/packages/bitcore-client/bin/wallet @@ -19,7 +19,8 @@ program .command('paypro', 'pay using payment protocol').alias('p') .command('send', 'simple send from wallet to an address').alias('s') .command('sign', 'sign a transaction') - .command('token', 'add an ERC20 token to an eth wallet') + .command('token', 'add an ERC20 token to an EVM wallet') + .command('token-rm', 'remove an ERC20 token from an EVM wallet') .command('flags', 'check or set wallet flags (XRP only)') .command('storage', 'storage util for wallets') .command('sign-message', 'sign a message with an address') diff --git a/packages/bitcore-client/bin/wallet-token b/packages/bitcore-client/bin/wallet-token index e6cd904924d..fb6ad2eb450 100755 --- a/packages/bitcore-client/bin/wallet-token +++ b/packages/bitcore-client/bin/wallet-token @@ -8,7 +8,7 @@ program .version(require('../package.json').version) .requiredOption('--name ', 'REQUIRED - Wallet name') .requiredOption('--contractAddress ', 'REQUIRED - Token contract address') - .option('--tokenName ', 'optional - Custom name for token') + .option('--tokenName ', 'optional - Custom name for token (default - contract\'s ticker)') .option('--storageType ', 'optional - Name of the database to use (Mongo | Level | TextFile)') .option('--path ', 'optional - Custom wallet storage path') .parse(process.argv); @@ -19,8 +19,8 @@ const main = async () => { const { name, path, contractAddress, storageType, tokenName } = program.opts(); try { wallet = await Wallet.loadWallet({ name, path, storageType }); - if (!['MATIC', 'ETH', 'ARB', 'OP', 'BASE'].includes(wallet.chain)) { - throw new Error('Cannot add token to non-ETH wallet.'); + if (!wallet.isEvmChain()) { + throw new Error('Cannot add token to non-EVM wallet.'); } const token = await wallet.getToken(contractAddress); const tokenObj = { diff --git a/packages/bitcore-client/bin/wallet-token-rm b/packages/bitcore-client/bin/wallet-token-rm new file mode 100755 index 00000000000..e1d9fcee2ed --- /dev/null +++ b/packages/bitcore-client/bin/wallet-token-rm @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +const program = require('commander'); +const promptly = require('promptly'); +const { Wallet } = require('../ts_build/src/wallet'); + +program + .version(require('../package.json').version) + .requiredOption('--name ', 'REQUIRED - Wallet name') + .requiredOption('--tokenName ', 'REQUIRED - Name of token to remove') + .option('--storageType ', 'optional - Name of the database to use (Mongo | Level | TextFile)') + .option('--path ', 'optional - Custom wallet storage path') + .parse(process.argv); + +let wallet; + +const main = async () => { + const { name, path, tokenName, storageType } = program.opts(); + try { + wallet = await Wallet.loadWallet({ name, path, storageType }); + if (!wallet.isEvmChain()) { + throw new Error('Cannot remove token from non-EVM wallet.'); + } + await wallet.rmToken({ tokenName }); + console.log(`Successfully removed ${tokenName}`); + } catch (e) { + console.error(e); + } +}; + +main() + .catch(console.error) + .finally(() => wallet?.storage?.close()); diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index e83143d4811..365c6d359ff 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -277,6 +277,14 @@ export class Wallet { return ['BTC', 'BCH', 'DOGE', 'LTC'].includes(this.chain?.toUpperCase() || 'BTC'); } + /** + * Is this wallet EVM compatible? + * @returns {Boolean} + */ + isEvmChain() { + return ['ETH', 'MATIC', 'ARB', 'OP', 'BASE'].includes(this.chain?.toUpperCase()); + } + lock() { this.unlocked = undefined; return this; @@ -442,6 +450,17 @@ export class Wallet { await this.saveWallet(); } + async rmToken({ tokenName }) { + if (!this.tokens) { + return; + } + this.tokens = this.tokens.filter(tok => + (tok.name && tok.name !== tokenName) || + /* legacy object */ (!tok.name && tok.symbol !== tokenName) + ); + await this.saveWallet(); + } + async newTx(params: { utxos?: any[]; recipients: { address: string; amount: number }[]; diff --git a/packages/bitcore-client/test/unit/wallet.test.ts b/packages/bitcore-client/test/unit/wallet.test.ts index dad197ee1c5..d038f3e2526 100644 --- a/packages/bitcore-client/test/unit/wallet.test.ts +++ b/packages/bitcore-client/test/unit/wallet.test.ts @@ -473,5 +473,79 @@ describe('Wallet', function() { } }); }); + + describe('rmToken', function() { + walletName = 'BitcoreClientTestRmToken'; + const usdcLegacyObj = { + symbol: 'USDC', + address: '0x123', + decimals: '6', + }; + + const usdcObj = { + symbol: 'USDC', + address: '0xabc', + decimals: '6', + name: 'USDCn' + }; + + const daiObj = { + symbol: 'DAI', + address: '0x1a2b3c', + decimals: '6', + name: 'DAIn' + }; + + beforeEach(async function() { + wallet = await Wallet.create({ + chain: 'ETH', + network: 'mainnet', + name: walletName, + phrase: 'snap impact summer because must pipe weasel gorilla actor acid web whip', + password: 'abc123', + lite: false, + storageType, + baseUrl + }); + + wallet.tokens = [ + usdcLegacyObj, + usdcObj, + daiObj + ]; + }); + + it('should remove a legacy token object', function() { + wallet.rmToken({ tokenName: 'USDC' }); + wallet.tokens.length.should.equal(2); + wallet.tokens.filter(t => t.symbol === 'USDC').length.should.equal(1); + wallet.tokens.filter(t => t.symbol === 'USDC')[0].should.deep.equal(usdcObj); + }); + + it('should remove a token object', function() { + wallet.rmToken({ tokenName: 'USDCn' }); + wallet.tokens.length.should.equal(2); + wallet.tokens.filter(t => t.symbol === 'USDC').length.should.equal(1); + wallet.tokens.filter(t => t.symbol === 'USDC')[0].should.deep.equal(usdcLegacyObj); + }); + + it('should remove the correct token object regardless of order', function() { + wallet.tokens = [ + usdcObj, + daiObj, + usdcLegacyObj // this should be ordered after usdcObj + ]; + + wallet.rmToken({ tokenName: 'USDC' }); + wallet.tokens.length.should.equal(2); + wallet.tokens.filter(t => t.symbol === 'USDC').length.should.equal(1); + wallet.tokens.filter(t => t.symbol === 'USDC')[0].should.deep.equal(usdcObj); + }); + + it('should not remove any unmatched token object', function() { + wallet.rmToken({ tokenName: 'BOGUS' }); + wallet.tokens.length.should.equal(3); + }); + }); });