Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add script to merge and split coins #255

Merged
merged 54 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c6b7044
feat: add manage-coins script
npty Jun 3, 2024
67e25cb
feat: complete merge coins
npty Jun 4, 2024
7f9a837
feat: add split coins
npty Jun 4, 2024
11b8667
feat: group coins-related functions into class
npty Jun 4, 2024
3700c2d
chore: fix not get all coins
npty Jun 4, 2024
1ffbfd8
chore: simply return instead of exit program
npty Jun 4, 2024
b8d8c3b
chore: fix can't split all coins including gas token
npty Jun 4, 2024
9e94132
chore: fix lint
npty Jun 4, 2024
10bda3b
feat: add transfer option
npty Jun 5, 2024
2ca5c07
chore: fix lint
npty Jun 5, 2024
c60959f
chore: remove receipt
npty Jun 5, 2024
7549c0a
chore: update coinType ternary condition
npty Jun 6, 2024
318a3ed
chore: update program name and description
npty Jun 6, 2024
969e4c1
chore: update console.log to printInfo
npty Jun 6, 2024
511a61a
chore: refactor mergeCoin
npty Jun 6, 2024
14ab718
chore: use validateParameters
npty Jun 6, 2024
4775434
chore: export coinId in utils and use validateParameters
npty Jun 6, 2024
a832bb8
chore: split sui coin if coin type is not specified
npty Jun 6, 2024
b9ed184
chore: accept sui amount in full denom
npty Jun 6, 2024
08271f6
Merge branch 'main' into feat/script-merge-and-split-coins
npty Jun 6, 2024
5951312
chore: remove newline from printInfo
npty Jun 7, 2024
c707c77
chore: replace remaining console.log
npty Jun 7, 2024
3b8aa17
chore: print message when no coins to merge
npty Jun 7, 2024
74c3106
chore: remove doSplitCoins
npty Jun 7, 2024
09fd172
chore: remove doMergeCoin
npty Jun 7, 2024
a8557d7
Merge branch 'feat/script-merge-and-split-coins' of github.com:axelar…
npty Jun 7, 2024
e4c7552
chore: rename functions for consistency
npty Jun 7, 2024
638616c
chore: export isGasToken to utils
npty Jun 7, 2024
72ceefa
chore: refactor splitCoins
npty Jun 7, 2024
256f271
chore: refactor to use subcommands
npty Jun 7, 2024
580a22f
chore: reorder programs
npty Jun 7, 2024
78dc4fa
Merge branch 'main' into feat/script-merge-and-split-coins
blockchainguyy Jun 19, 2024
1b6e324
chore: add a util function for iterating over pagination response
npty Jun 21, 2024
2e09354
Merge branch 'feat/script-merge-and-split-coins' of github.com:axelar…
npty Jun 21, 2024
30cf4c4
chore: fix split coins
npty Jun 21, 2024
4576195
chore: fix object selection to split coin
npty Jun 21, 2024
862a837
Merge branch 'main' into feat/script-merge-and-split-coins
npty Jun 25, 2024
72d5614
Merge branch 'main' into feat/script-merge-and-split-coins
npty Jul 2, 2024
42a0a08
chore: replace signAndBroadcast
npty Jul 2, 2024
c7a4cc3
Merge branch 'main' into feat/script-merge-and-split-coins
npty Aug 1, 2024
25dc2f6
chore: refactor
npty Aug 2, 2024
7ad46d8
chore: adjust params
npty Aug 2, 2024
a5a63f0
chore: remove evm utils
npty Aug 2, 2024
657c6fc
chore: use addOptionsToCommands
npty Aug 2, 2024
3cf8a56
chore: update readme
npty Aug 2, 2024
f83fe44
chore: reword readme
npty Aug 2, 2024
344dd54
Merge branch 'main' into feat/script-merge-and-split-coins
blockchainguyy Aug 5, 2024
92255c2
chore: prettier
npty Aug 5, 2024
1be3d6d
Merge branch 'feat/script-merge-and-split-coins' of github.com:axelar…
npty Aug 5, 2024
6211df4
Merge branch 'main' into feat/script-merge-and-split-coins
npty Aug 13, 2024
51ad53e
Merge branch 'main' into feat/script-merge-and-split-coins
npty Aug 21, 2024
ff2576d
chore: update imports
npty Aug 21, 2024
9c7c9a8
chore: prettier
npty Aug 21, 2024
f1a5231
Merge branch 'main' into feat/script-merge-and-split-coins
npty Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions sui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,29 @@ node sui/transfer-object.js --objectId <object id to be transferred> --recipient

node sui/transfer-object.js --contractName <Can be checked from config> --objectName <picked from config> --recipient <recipient address>
```

## Coins Management

List of coins in the wallet:

```bash
node sui/tokens.js list
```

Merge the coins:

```bash
node sui/tokens.js merge --coin-type <coin type to merge>
```

If coin type is not provided, it will merge all the coins.

Split the coins:

```bash
node sui/tokens.js split --amount <amount> --coin-type <coin type to split> --transfer <recipient address>
```

Note:
- If coin type is not provided, it will split all the coins.
- If transfer address is not provided, it will split the coins in the same wallet. Otherwise, it will transfer the splitted coins to the provided address.
219 changes: 219 additions & 0 deletions sui/tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const { Transaction } = require('@mysten/sui/transactions');
const { Command } = require('commander');
const { addBaseOptions, parseSuiUnitAmount, addOptionsToCommands } = require('./cli-utils');
const { loadConfig, saveConfig, printInfo, printError } = require('../common/');
const { broadcast, getWallet } = require('./sign-utils');
const { suiCoinId, isGasToken, paginateAll } = require('./utils');
const {
utils: { formatUnits },
} = require('ethers');

class CoinManager {
static async getAllCoins(client, account) {
const coinTypeToCoins = {};

try {
// Fetch all coins using pagination
const coins = await paginateAll(client, 'getAllCoins', { owner: account });

// Iterate over each coin and organize them by coin type
for (const coin of coins) {
const coinsByType = coinTypeToCoins[coin.coinType] || {
data: [],
totalBalance: 0n,
};

coinsByType.data.push(coin);
coinsByType.totalBalance += BigInt(coin.balance);

coinTypeToCoins[coin.coinType] = coinsByType;
}
} catch (e) {
printError('Failed to fetch coins', e.message);
}

return coinTypeToCoins;
}

static async printCoins(client, coinTypeToCoins) {
for (const coinType in coinTypeToCoins) {
const coins = coinTypeToCoins[coinType];

const metadata = await client.getCoinMetadata({
coinType,
});

if (!metadata) {
printError('No metadata found for', coinType);
process.exit(0);
}

printInfo('Coin Type', coinType);
printInfo('Total Balance', `${formatUnits(coins.totalBalance.toString(), metadata.decimals).toString()}`);
printInfo('Total Objects', coins.data.length);

for (const coin of coins.data) {
printInfo(`- ${formatUnits(coin.balance, metadata.decimals)}`);
}
}
}

static async splitCoins(tx, client, coinTypeToCoins, walletAddress, args, options) {
const coinType = options.coinType || suiCoinId;
const splitAmount = args.splitAmount;

const metadata = await client.getCoinMetadata({
coinType,
});

if (!metadata) {
printError('No metadata found for', coinType);
process.exit(0);
}

const objectToSplit = isGasToken(coinType)
? tx.gas
: coinTypeToCoins[coinType].data.find((coinObject) => BigInt(coinObject.balance) >= splitAmount)?.coinObjectId;

if (!objectToSplit) {
printError('No coin object found with enough balance to split');
process.exit(0);
}

const [coin] = tx.splitCoins(objectToSplit, [splitAmount]);

printInfo('Split Coins', coinType);
printInfo('Split Amount', `${formatUnits(splitAmount, metadata.decimals).toString()}`);

if (options.transfer) {
tx.transferObjects([coin], options.transfer);
printInfo('Transfer Coins to', options.transfer);
} else {
tx.transferObjects([coin], walletAddress);
}

// The transaction will fail if the gas budget is not set for splitting coins transaction
tx.setGasBudget(1e7);
}

static async mergeCoins(tx, coinTypeToCoins, options) {
const coinTypes = options.coinType ? [options.coinType] : Object.keys(coinTypeToCoins);

let merged = false;

for (const coinType of coinTypes) {
const coins = coinTypeToCoins[coinType];

if (!coins) {
throw new Error(`No coins found for coin type ${coinType}`);
}

const coinObjectIds = coins.data.map((coin) => coin.coinObjectId);

// If the first coin is a gas token, remove it from the list. Otherwise, the merge will fail.
if (isGasToken(coins.data[0].coinType)) {
coinObjectIds.shift();
}

if (coinObjectIds.length < 2) {
// Need at least 2 coins to merge
continue;
}

const firstCoin = coinObjectIds.shift();
const remainingCoins = coinObjectIds.map((id) => tx.object(id));

tx.mergeCoins(firstCoin, remainingCoins);
merged = true;

printInfo('Merge Coins', coins.data[0].coinType);
}

return merged;
}
}

async function processSplitCommand(keypair, client, args, options) {
printInfo('Action', 'Split Coins');

const coinTypeToCoins = await CoinManager.getAllCoins(client, keypair.toSuiAddress());

const tx = new Transaction();

await CoinManager.splitCoins(tx, client, coinTypeToCoins, keypair.toSuiAddress(), args, options);

const receipt = await broadcast(client, keypair, tx);

printInfo('Splitted Coins', receipt.digest);
}

async function processMergeCommand(keypair, client, args, options) {
printInfo('Action', 'Merge Coins');
const coinTypeToCoins = await CoinManager.getAllCoins(client, keypair.toSuiAddress());

const tx = new Transaction();
const hasMerged = await CoinManager.mergeCoins(tx, coinTypeToCoins, options);

if (!hasMerged) {
printInfo('No coins to merge');
return;
}

const receipt = await broadcast(client, keypair, tx);

printInfo('Merged Coins', receipt.digest);
}

async function processListCommand(keypair, client, args, options) {
printInfo('Action', 'List Coins');
printInfo('Wallet Address', keypair.toSuiAddress());

const coinTypeToCoins = await CoinManager.getAllCoins(client, keypair.toSuiAddress());
await CoinManager.printCoins(client, coinTypeToCoins);
}

async function mainProcessor(options, processor, args = {}) {
const config = loadConfig(options.env);
const [keypair, client] = getWallet(config.sui, options);
await processor(keypair, client, args, options);
saveConfig(config, options.env);
npty marked this conversation as resolved.
Show resolved Hide resolved
}

if (require.main === module) {
// Main program
const program = new Command('tokens').description('Merge, split, and list coins.');

// Sub-programs
const mergeProgram = new Command('merge').description('Merge all coins into a single object');
const splitProgram = new Command('split').description(
'Split coins into a new object. If no coin type is specified, SUI coins will be used by default.',
);
const listProgram = new Command('list').description('List all coins and balances');

// Define options, arguments, and actions for each sub-program
mergeProgram.option('--coin-type <coinType>', 'Coin type to merge').action((options) => {
mainProcessor(options, processMergeCommand);
});

splitProgram
.argument('<amount>', 'Amount should be in the full coin unit (e.g. 1.5 for 1_500_000_000 coins)', parseSuiUnitAmount)
.option('--transfer <recipientAddress>', 'Used with split command to transfer the split coins to the recipient address')
.option('--coin-type <coinType>', 'Coin type to split')
.action((splitAmount, options) => {
mainProcessor(options, processSplitCommand, { splitAmount });
});

listProgram.action((options) => {
mainProcessor(options, processListCommand);
});

// Add sub-programs to the main program
program.addCommand(mergeProgram);
program.addCommand(splitProgram);
program.addCommand(listProgram);

// Add base options to all sub-programs
addOptionsToCommands(program, addBaseOptions);
npty marked this conversation as resolved.
Show resolved Hide resolved

program.parse();
}
31 changes: 30 additions & 1 deletion sui/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { singletonStruct, itsStruct, squidStruct } = require('./types-utils');

const suiPackageAddress = '0x2';
const suiClockAddress = '0x6';
const suiCoinId = '0x2::sui::SUI';

const getAmplifierSigners = async (config, chain) => {
const client = await CosmWasmClient.connect(config.axelar.rpc);
Expand Down Expand Up @@ -145,10 +146,38 @@ const getSigners = async (keypair, config, chain, options) => {
return getAmplifierSigners(config, chain);
};

const isGasToken = (coinType) => {
return coinType === suiCoinId;
};

const paginateAll = async (client, paginatedFn, params, pageLimit = 100) => {
let cursor;
let response = await client[paginatedFn]({
...params,
cursor,
limit: pageLimit,
});
const items = response.data;

while (response.hasNextPage) {
response = await client[paginatedFn]({
...params,
cursor: response.nextCursor,
limit: pageLimit,
});
items.push(...response.data);
}

return items;
};

module.exports = {
suiCoinId,
getAmplifierSigners,
isGasToken,
paginateAll,
suiPackageAddress,
suiClockAddress,
getAmplifierSigners,
getBcsBytesByObjectId,
deployPackage,
findPublishedObject,
Expand Down
Loading