Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
23 changes: 22 additions & 1 deletion sui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ ts-node sui/deploy-contract.js deploy ITS
ts-node sui/its.js interchain-transfer <coin-package-address> <coin-package-name> <coin-mod-name> <coin-object-id> <interchain-token-id> destination-chain <destination-chain-name> destination-address <receiving-address> amount 1 --env <your-env> --signatureScheme <your-signature-scheme>
```


- Example Command:

```bash
ts-node sui/its.js interchain-transfer 0x5d693cebdbba9fdcc8a5990858998a2f7bee87ef2d537e9dd4588a30ec615ad7 my_custom_coin MY_CUSTOM_COIN 0xa8c34124a6d103214dbec1cfdc9a7505eac9b8a68c73c10a2d0b6f42ff5f3af4 0x3630dbd78a65b5b70745574d94268a71c142076543fabb71d30d9d315fdf87f4 ethereum-sepolia 0xc5DcAC3e02f878FE995BF71b1Ef05153b71da8BE 1 --env testnet --signatureScheme ed25519
```


Example Response:

````bash
Expand Down Expand Up @@ -553,6 +553,27 @@ Restore a coin's TreasuryCap to ITS after calling remove-treasury-cap, giving mi
ts-node sui/its restore-treasury-cap [options] <symbol>
```

### Mint Coin

Mint new Sui Coin

Command:
```bash
ts-node sui/its.js mint-coins <coin-package-id> <coin-package-name> <coin-mod-name> <amount> <receiver> <env> <signature-scheme>
```


Example:
```bash
ts-node sui/its.js mint-coins 0xe3521d94addba8d1405abf057a897abceedfc973c6c7016fe4e9baaafc14723b my_custom_coin MY_CUSTOM_COIN 1 0xa46ed4032af9ae9c8412dc8294eb9b3ed43277f7222591da331707f747b38bd9 -e testnet --signatureScheme ed25519
```

Example Response:
```bash
💰 my token balance 1
New coin object id: 0x7eb1b5a01b679380d9fc0553293d81acbfc7f9485c10f01b92cf38dfb5b76b92
```

## Sui Contract Verification

This script generates a `verification` folder inside the `move` directory, which contains ZIP files for each contract to be used for verification. Before zipping, a `deps` subdirectory is added to each contract, and the local dependency paths in the `Move.toml` file are updated to reference the `deps` folder.
Expand Down
68 changes: 61 additions & 7 deletions sui/its.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
validateParameters,
isValidNumber,
validateDestinationChain,
estimateITSFee,
} = require('../common/utils');
const {
addBaseOptions,
Expand All @@ -33,7 +34,7 @@ const chalk = require('chalk');
const {
utils: { arrayify, parseUnits },
} = require('hardhat').ethers;
const { checkIfCoinExists, checkIfCoinIsMinted } = require('./utils/token-utils');
const { checkIfCoinExists, checkIfSenderHasSufficientBalance } = require('./utils/token-utils');

async function setFlowLimits(keypair, client, config, contracts, args, options) {
let [tokenIds, flowLimits] = args;
Expand Down Expand Up @@ -605,7 +606,7 @@ async function deployRemoteCoin(keypair, client, config, contracts, args, option
typeArguments: [coinType],
});

console.log('🚀 Deploying remote interchain coin...');
printInfo('🚀 Deploying remote interchain token....');

const unitAmountGas = parseUnits('1', 9).toBigInt();

Expand Down Expand Up @@ -730,9 +731,6 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti

const coinType = `${coinPackageId}::${coinPackageName}::${coinModName}`;

await checkIfCoinExists(client, coinPackageId, coinType);
await checkIfCoinIsMinted(client, coinObjectId, coinType);

const tokenIdObj = await txBuilder.moveCall({
target: `${itsConfig.address}::token_id::from_u256`,
arguments: [tokenId],
Expand All @@ -743,6 +741,9 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti
arguments: [],
});

await checkIfCoinExists(client, coinPackageId, coinType);
await checkIfSenderHasSufficientBalance(client, walletAddress, coinType, coinObjectId, amount);

const [coinsToSend] = tx.splitCoins(coinObjectId, [amount]);

const prepareInterchainTransferTicket = await txBuilder.moveCall({
Expand All @@ -757,9 +758,9 @@ async function interchainTransfer(keypair, client, config, contracts, args, opti
arguments: [itsConfig.objects.InterchainTokenService, prepareInterchainTransferTicket, suiClockAddress],
});

const unitAmountGas = parseUnits('1', 9).toBigInt();
const gasValue = await estimateITSFee(config.chains['sui'], destinationChain, options.env, 'InterchainTransfer', 'auto', config.axelar);

const [gas] = tx.splitCoins(tx.gas, [unitAmountGas]);
const [gas] = tx.splitCoins(tx.gas, [gasValue]);

await txBuilder.moveCall({
target: `${contracts.GasService.address}::gas_service::pay_gas`,
Expand Down Expand Up @@ -823,6 +824,49 @@ async function checkVersionControl(keypair, client, config, contracts, args, opt
}
}

async function mintCoins(keypair, client, config, contracts, args, options) {
const [coinPackageId, coinPackageName, coinModName, amount, receiver] = args;

const walletAddress = keypair.toSuiAddress();

const coinType = `${coinPackageId}::${coinPackageName}::${coinModName}`;

await checkIfCoinExists(client, coinPackageId, coinType);

const { data } = await client.getOwnedObjects({
owner: walletAddress,
filter: { StructType: `0x2::coin::TreasuryCap<${coinType}>` },
options: { showType: true },
});

if (!Array.isArray(data) || data.length === 0) {
throw new Error('TreasuryCap object not found for the specified coin type.');
}

const treasury = data[0].data?.objectId ?? data[0].objectId;

const txBuilder = new TxBuilder(client);
await txBuilder.moveCall({
target: `${coinPackageId}::${coinPackageName}::mint`,
arguments: [treasury, amount, receiver],
});

const response = await broadcastFromTxBuilder(txBuilder, keypair, `Mint ${coinPackageId}`, options);

const balance = await client.getBalance({
owner: receiver,
coinType: `${coinPackageId}::${coinPackageName}::${coinModName}`,
});

printInfo('💰 receiver token balance', balance.totalBalance);

const coinChanged = response.objectChanges.find((c) => c.type === 'created');

printInfo('New coin object id:', coinChanged.objectId);

return [balance.totalBalance, coinChanged.objectId];
}

async function processCommand(command, config, chain, args, options) {
const [keypair, client] = getWallet(chain, options);

Expand Down Expand Up @@ -1013,6 +1057,14 @@ if (require.main === module) {
},
);

const mintCoinsProgram = new Command()
.name('mint-coins')
.command('mint-coins <coinPackageId> <coinPackageName> <coinModName> <amount> <receiver>')
.description('Mint coins for a given package on sui')
.action((coinPackageId, coinPackageName, coinModName, amount, receiver, options) => {
mainProcessor(mintCoins, options, [coinPackageId, coinPackageName, coinModName, amount, receiver], processCommand);
});

program.addCommand(setFlowLimitsProgram);
program.addCommand(addTrustedChainsProgram);
program.addCommand(removeTrustedChainsProgram);
Expand All @@ -1032,6 +1084,8 @@ if (require.main === module) {
program.addCommand(checkVersionControlProgram);
program.addCommand(interchainTransferProgram);

program.addCommand(mintCoinsProgram);

// finalize program
addOptionsToCommands(program, addBaseOptions, { offline: true });
program.parse();
Expand Down
22 changes: 15 additions & 7 deletions sui/utils/token-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,20 @@ async function checkIfCoinExists(client, coinPackageId, coinType) {
}
}

async function checkIfCoinIsMinted(client, coinObjectId, coinType) {
const coinObject = await client.getObject({ id: coinObjectId, options: { showType: true } });
const objectType = coinObject?.data?.type;
const expectedObjectType = `${SUI_PACKAGE_ID}::coin::Coin<${coinType}>`;
if (objectType !== expectedObjectType) {
throw new Error(`Invalid coin object type. Expected ${expectedObjectType}, got ${objectType || 'unknown'}`);
async function checkIfSenderHasSufficientBalance(client, walletAddress, coinType, coinObjectId, amount) {
const coins = await client.getCoins({
owner: walletAddress,
coinType,
});

const coin = coins.data.find((c) => c.coinObjectId === coinObjectId);
if (!coin) {
throw new Error(`Coin with ID ${coinObjectId} not found for owner ${walletAddress}`);
}

const balance = Number(coin.balance);
if (balance < amount) {
throw new Error(`User does not have sufficient balance. Expected ${amount}, got ${balance}`);
}
}

Expand All @@ -89,5 +97,5 @@ module.exports = {
createLockedCoinManagement,
saveTokenDeployment,
checkIfCoinExists,
checkIfCoinIsMinted,
checkIfSenderHasSufficientBalance,
};