diff --git a/move/example/Move.lock b/move/example/Move.lock index 55895567..1e39f47c 100644 --- a/move/example/Move.lock +++ b/move/example/Move.lock @@ -2,11 +2,20 @@ [move] version = 2 -manifest_digest = "35EEA97D60D29F06D44ECBC878C919B5E79C2A94B14C542D6A429B33F9572E97" -deps_digest = "060AD7E57DFB13104F21BE5F5C3759D03F0553FC3229247D9A7A6B45F50D03A3" +manifest_digest = "196CD7C03594C5D0B7CCE594F4099D87F127C8F0EAB84C1570C4D2C874A28047" +deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" dependencies = [ { name = "AxelarGateway" }, { name = "GasService" }, + { name = "ITS" }, + { name = "Sui" }, +] + +[[move.package]] +name = "Abi" +source = { local = "../abi" } + +dependencies = [ { name = "Sui" }, ] @@ -26,6 +35,27 @@ dependencies = [ { name = "Sui" }, ] +[[move.package]] +name = "Governance" +source = { local = "../governance" } + +dependencies = [ + { name = "Abi" }, + { name = "AxelarGateway" }, + { name = "Sui" }, +] + +[[move.package]] +name = "ITS" +source = { local = "../its" } + +dependencies = [ + { name = "AxelarGateway" }, + { name = "GasService" }, + { name = "Governance" }, + { name = "Sui" }, +] + [[move.package]] name = "MoveStdlib" source = { git = "https://github.com/MystenLabs/sui.git", rev = "mainnet-v1.25.3", subdir = "crates/sui-framework/packages/move-stdlib" } @@ -39,6 +69,6 @@ dependencies = [ ] [move.toolchain-version] -compiler-version = "1.32.0" +compiler-version = "1.30.3" edition = "2024.beta" flavor = "sui" diff --git a/move/example/sources/its/its.move b/move/example/sources/its/its.move index 29ce85bb..84009516 100644 --- a/move/example/sources/its/its.move +++ b/move/example/sources/its/its.move @@ -1,4 +1,4 @@ -module example::its { +module example::its_example { use std::ascii; use std::ascii::{String}; use std::type_name; @@ -6,9 +6,10 @@ module example::its { use sui::event; use sui::address; use sui::hex; - use sui::coin::{Coin}; + use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata}; use sui::sui::SUI; use sui::clock::Clock; + use sui::url::Url; use axelar_gateway::channel::{Self, Channel, ApprovedMessage}; use axelar_gateway::discovery::{Self, RelayerDiscovery, Transaction}; @@ -18,10 +19,17 @@ module example::its { use its::service; use its::its::ITS; use its::token_id::TokenId; + use its::coin_management; + use its::coin_info; + + public struct ITS_EXAMPLE has drop {} public struct Singleton has key { id: UID, channel: Channel, + treasury_cap: Option>, + coin_metadata: Option>, + token_id: Option, } public struct Executed has copy, drop { @@ -31,15 +39,41 @@ module example::its { amount: u64, } - fun init(ctx: &mut TxContext) { + fun init(witness: ITS_EXAMPLE, ctx: &mut TxContext) { + let decimals: u8 = 8; + let symbol: vector = b"ITS"; + let name: vector = b"Test Coin"; + let description = b""; + let icon_url = option::none(); + let (treasury_cap, coin_metadata) = coin::create_currency( + witness, + decimals, + symbol, + name, + description, + icon_url, + ctx, + ); + let singletonId = object::new(ctx); let channel = channel::new(ctx); transfer::share_object(Singleton { id: singletonId, channel, + treasury_cap: option::some(treasury_cap), + coin_metadata: option::some(coin_metadata), + token_id: option::none(), }); } + public fun token_id(self: &Singleton): &TokenId { + self.token_id.borrow() + } + + public fun mint(self: &mut Singleton, amount: u64, ctx: &mut TxContext): Coin { + self.treasury_cap.borrow_mut().mint(amount, ctx) + } + public fun register_transaction(discovery: &mut RelayerDiscovery, singleton: &Singleton, its: &ITS) { let mut arguments = vector::empty>(); @@ -117,20 +151,29 @@ module example::its { ) } - public fun send_interchain_transfer( - singleton: &Singleton, + public fun register_coin(self: &mut Singleton, its: &mut ITS) { + let coin_info = coin_info::from_metadata(self.coin_metadata.extract(), 12); + let coin_management = coin_management::new_with_cap(self.treasury_cap.extract()); + + let token_id = service::register_coin(its, coin_info, coin_management); + + self.token_id.fill(token_id); + } + + public fun send_interchain_transfer( + self: &Singleton, its: &mut ITS, destination_chain: String, destination_address: vector, - token_id: TokenId, - coin: Coin, + coin: Coin, metadata: vector, gas_service: &mut GasService, gas: Coin, clock: &Clock, ctx: &mut TxContext, ) { - service::interchain_transfer( + let token_id = *self.token_id.borrow(); + service::interchain_transfer( its, token_id, coin, @@ -139,25 +182,25 @@ module example::its { metadata, gas_service, gas, - &singleton.channel, + &self.channel, clock, ctx, ); } - public fun execute_interchain_transfer( + public fun execute_interchain_transfer( approved_message: ApprovedMessage, singleton: &mut Singleton, its: &mut ITS, clock: &Clock, ctx: &mut TxContext - ): Coin { + ): Coin { let ( source_chain, source_address, data, coin, - ) = service::receive_interchain_transfer_with_data( + ) = service::receive_interchain_transfer_with_data( its, approved_message, &singleton.channel, diff --git a/move/governance/sources/governance/governance.move b/move/governance/sources/governance/governance.move index 04164c88..9ed4e68c 100644 --- a/move/governance/sources/governance/governance.move +++ b/move/governance/sources/governance/governance.move @@ -62,12 +62,10 @@ public fun is_governance( entry fun take_upgrade_cap(self: &mut Governance, upgrade_cap: UpgradeCap) { is_cap_new(&upgrade_cap); - self - .caps - .add( - object::id(&upgrade_cap), - upgrade_cap, - ) + self.caps.add( + object::id(&upgrade_cap), + upgrade_cap, + ) } public fun authorize_upgrade( diff --git a/move/its/sources/service.move b/move/its/sources/service.move index 51764921..805f863d 100644 --- a/move/its/sources/service.move +++ b/move/its/sources/service.move @@ -52,14 +52,16 @@ module its::service { public fun register_coin( self: &mut ITS, coin_info: CoinInfo, coin_management: CoinManagement - ) { + ): TokenId { let token_id = token_id::from_coin_data(&coin_info, &coin_management); self.add_registered_coin(token_id, coin_management, coin_info); event::emit(CoinRegistered { token_id - }) + }); + + token_id } public fun deploy_remote_interchain_token( diff --git a/test/its.js b/test/its.js new file mode 100644 index 00000000..3c88cf6d --- /dev/null +++ b/test/its.js @@ -0,0 +1,320 @@ +const { SuiClient, getFullnodeUrl } = require('@mysten/sui/client'); +const { Ed25519Keypair } = require('@mysten/sui/keypairs/ed25519'); +const { Secp256k1Keypair } = require('@mysten/sui/keypairs/secp256k1'); +const { requestSuiFromFaucetV0, getFaucetHost } = require('@mysten/sui/faucet'); +const { + publishPackage, + getRandomBytes32, + expectRevert, + expectEvent, + approveMessage, + hashMessage, + signMessage, + approveAndExecuteMessage, +} = require('./utils'); +const { TxBuilder } = require('../dist/tx-builder'); +const { + bcsStructs: { + gateway: { WeightedSigners, MessageToSign, Proof }, + }, +} = require('../dist/bcs'); +const { bcs } = require('@mysten/sui/bcs'); +const { arrayify, hexlify, keccak256, defaultAbiCoder } = require('ethers/lib/utils'); +const { expect } = require('chai'); + +const COMMAND_TYPE_ROTATE_SIGNERS = 1; +const clock = '0x6'; +const sui = '0x2'; +const MESSAGE_TYPE_SET_TRUSTED_ADDRESSES = BigInt(0x2af37a0d5d48850a855b1aaaf57f726c107eb99b40eabf4cc1ba30410cfa2f68); + +describe.only('ITS', () => { + let client; + const operator = Ed25519Keypair.fromSecretKey(arrayify(getRandomBytes32())); + const deployer = Ed25519Keypair.fromSecretKey(arrayify(getRandomBytes32())); + const keypair = Ed25519Keypair.fromSecretKey(arrayify(getRandomBytes32())); + const domainSeparator = getRandomBytes32(); + const network = process.env.NETWORK || 'localnet'; + let nonce = 0; + let packageId; + let gateway; + let discovery; + let its; + let axelarPackageId; + let gasService, gasServicePackageId; + let exampleId, singleton; + let governance; + const remoteChain = 'Remote Chain'; + const trustedAddress = 'Trusted Address'; + const gatewayInfo = {}; + + function calculateNextSigners() { + const signerKeys = [getRandomBytes32(), getRandomBytes32(), getRandomBytes32()]; + const pubKeys = signerKeys.map((key) => Secp256k1Keypair.fromSecretKey(arrayify(key)).getPublicKey().toRawBytes()); + const keys = signerKeys.map((key, index) => { + return { privKey: key, pubKey: pubKeys[index] }; + }); + keys.sort((key1, key2) => { + for (let i = 0; i < 33; i++) { + if (key1.pubKey[i] < key2.pubKey[i]) return -1; + if (key1.pubKey[i] > key2.pubKey[i]) return 1; + } + + return 0; + }); + gatewayInfo.signerKeys = keys.map((key) => key.privKey); + gatewayInfo.signers = { + signers: keys.map((key) => { + return { pub_key: key.pubKey, weight: 1 }; + }), + threshold: 2, + nonce: hexlify([++nonce]), + }; + } + + async function newExample() { + const result = await publishPackage(client, keypair, 'example'); + exampleId = result.packageId; + singleton = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${exampleId}::its_example::Singleton`, + ).objectId; + } + + async function sleep(ms = 1000) { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + + const minimumRotationDelay = 1000; + const previousSignersRetention = 15; + + before(async () => { + client = new SuiClient({ url: getFullnodeUrl(network) }); + + await Promise.all( + [operator, deployer, keypair].map((keypair) => + requestSuiFromFaucetV0({ + host: getFaucetHost(network), + recipient: keypair.toSuiAddress(), + }), + ), + ); + + let result = await publishPackage(client, deployer, 'axelar_gateway'); + axelarPackageId = result.packageId; + const creatorCap = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${axelarPackageId}::gateway::CreatorCap`, + ).objectId; + discovery = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${axelarPackageId}::discovery::RelayerDiscovery`, + ).objectId; + + result = await publishPackage(client, deployer, 'gas_service'); + gasServicePackageId = result.packageId; + gasService = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${gasServicePackageId}::gas_service::GasService`, + ).objectId; + + await publishPackage(client, deployer, 'abi'); + result = await publishPackage(client, deployer, 'governance'); + const governanceId = result.packageId; + const governanceUpgradeCap = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${sui}::package::UpgradeCap`, + ).objectId; + + result = await publishPackage(client, deployer, 'its'); + packageId = result.packageId; + its = result.publishTxn.objectChanges.find( + (change) => change.objectType === `${packageId}::its::ITS`, + ).objectId; + + calculateNextSigners(); + + const encodedSigners = WeightedSigners.serialize(gatewayInfo.signers).toBytes(); + let builder = new TxBuilder(client); + + const separator = await builder.moveCall({ + target: `${axelarPackageId}::bytes32::new`, + arguments: [domainSeparator], + }); + + await builder.moveCall({ + target: `${axelarPackageId}::gateway::setup`, + arguments: [ + creatorCap, + operator.toSuiAddress(), + separator, + minimumRotationDelay, + previousSignersRetention, + encodedSigners, + clock, + ], + }); + + const trustedSourceChain = 'Axelar'; + const trustedSourceAddress = 'Address'; + const messageType = 1234; + await builder.moveCall({ + target: `${governanceId}::governance::new`, + arguments: [ + trustedSourceChain, + trustedSourceAddress, + messageType, + governanceUpgradeCap, + ], + }) + + result = await builder.signAndExecute(deployer); + gateway = result.objectChanges.find((change) => change.objectType === `${axelarPackageId}::gateway::Gateway`).objectId; + governance = result.objectChanges.find((change) => change.objectType === `${governanceId}::governance::Governance`).objectId; + + gatewayInfo.gateway = gateway; + gatewayInfo.domainSeparator = domainSeparator; + gatewayInfo.packageId = axelarPackageId; + gatewayInfo.discovery = discovery; + + const itsData = await client.getObject({ + id: its, + options: { + showContent: true, + }, + }); + + const channelId = itsData.data.content.fields.channel.fields.id.id; + + const payload = defaultAbiCoder.encode(['uint256', 'bytes'], [ + MESSAGE_TYPE_SET_TRUSTED_ADDRESSES, + bcs.struct('Trusted Addresses', { + chain_names: bcs.vector(bcs.String), + trusted_addresses: bcs.vector(bcs.String), + }).serialize({ + chain_names: [remoteChain], + trusted_addresses: [trustedAddress], + }).toBytes(), + ]); + const message = { + source_chain: trustedSourceAddress, + message_id: 'Message Id 0', + source_address: trustedSourceAddress, + destination_id: channelId, + payload, + payload_hash: keccak256(payload), + }; + + await approveMessage(client, keypair, gatewayInfo, message); + builder = new TxBuilder(client); + + const approvedMessage = await builder.moveCall({ + target: `${axelarPackageId}::gateway::take_approved_message`, + arguments: [ + gateway, + message.source_chain, + message.message_id, + message.source_address, + message.destination_id, + message.payload, + ], + }); + + await builder.moveCall({ + target: `${packageId}::service::set_trusted_addresses`, + arguments: [ + its, + governance, + approvedMessage, + ], + }); + + await builder.signAndExecute(keypair); + }); + + describe('Token Registration', () => { + it('Should register a coin', async () => { + await newExample(); + + let builder = new TxBuilder(client); + + await builder.moveCall({ + target: `${exampleId}::its_example::register_coin`, + arguments: [ + singleton, + its, + ], + typeArguments: [], + }); + + await expectEvent(builder, keypair, { + type: `${packageId}::service::CoinRegistered` + }); + }); + }); + + describe('Its Example', () => { + it('Should register a coin', async () => { + await newExample(); + + let builder = new TxBuilder(client); + + await builder.moveCall({ + target: `${exampleId}::its_example::register_coin`, + arguments: [ + singleton, + its, + ], + typeArguments: [], + }); + + await expectEvent(builder, keypair, { + type: `${packageId}::service::CoinRegistered` + }); + }); + + it('Should send some tokens', async () => { + const amount = 1234; + await newExample(); + + let builder = new TxBuilder(client); + + const coin = await builder.moveCall({ + target: `${exampleId}::its_example::mint`, + arguments: [ + singleton, + amount, + ], + typeArguments: [], + }); + await builder.moveCall({ + target: `${exampleId}::its_example::register_coin`, + arguments: [ + singleton, + its, + ], + typeArguments: [], + }); + const gas = await builder.moveCall({ + target: `${sui}::coin::zero`, + arguments: [], + typeArguments: [ + `${sui}::sui::SUI` + ], + }); + await builder.moveCall({ + target: `${exampleId}::its_example::send_interchain_transfer`, + arguments: [ + singleton, + its, + 'Destination Chain', + '0x1234', + coin, + '0x', + gasService, + gas, + '0x6', + ], + typeArguments: [], + }); + + await expectEvent(builder, keypair, { + type: `${packageId}::service::CoinRegistered` + }); + }); + }); +}); diff --git a/test/utils.js b/test/utils.js index a1a31d7b..3a4e7d89 100644 --- a/test/utils.js +++ b/test/utils.js @@ -84,7 +84,7 @@ async function expectEvent(builder, keypair, eventData = {}) { expect(a).to.equal(b); } - + if(!eventData.arguments) return; for (const key of Object.keys(eventData.arguments)) { compare(event.parsedJson[key], eventData.arguments[key]); }