Skip to content

Commit

Permalink
ts sdk: coin-with-balance intent create zero coin on zero amount (#20184
Browse files Browse the repository at this point in the history
)

## Description 

Change the `coinWithBalance` intent so that it creates a zero coin
instead of splitting when amount is 0. This fixes the edge case where
the user doesn't have a coin of required type in the wallet but the
amount is 0 anyways, thus avoids throwing the `Not enough coins of type
${coinType} to satisfy requested balance` error in this situation

cc @hayes-mysten 

## Test plan 

Added e2e tests

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
- [ ] REST API:

---------

Co-authored-by: hayes-mysten <[email protected]>
  • Loading branch information
kklas and hayes-mysten authored Nov 6, 2024
1 parent 27098db commit e7bc63e
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-dolphins-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/sui': patch
---

Allow 0 amounts with `coinWithBalance` intent when the wallet has no coin objects of the required type.
10 changes: 9 additions & 1 deletion sdk/typescript/src/transactions/intents/CoinWithBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function resolveCoinBalance(
if (command.$kind === '$Intent' && command.$Intent.name === COIN_WITH_BALANCE) {
const { type, balance } = parse(CoinWithBalanceData, command.$Intent.data);

if (type !== 'gas') {
if (type !== 'gas' && balance > 0n) {
coinTypes.add(type);
}

Expand Down Expand Up @@ -114,6 +114,14 @@ async function resolveCoinBalance(
balance: bigint;
};

if (balance === 0n) {
transactionData.replaceCommand(
index,
Commands.MoveCall({ target: '0x2::coin::zero', typeArguments: [type] }),
);
continue;
}

const commands = [];

if (!mergedCoins.has(type)) {
Expand Down
172 changes: 171 additions & 1 deletion sdk/typescript/test/e2e/coin-with-balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ describe('coinWithBalance', () => {
let publishToolbox: TestToolbox;
let packageId: string;
let testType: string;
let testTypeZero: string;

beforeAll(async () => {
[toolbox, publishToolbox] = await Promise.all([setup(), setup()]);
const packagePath = resolve(__dirname, './data/coin_metadata');
packageId = await publishToolbox.getPackage(packagePath);
testType = normalizeSuiAddress(packageId) + '::test::TEST';
testTypeZero = normalizeSuiAddress(packageId) + '::test_zero::TEST_ZERO';
});

it('works with sui', async () => {
Expand Down Expand Up @@ -322,6 +324,139 @@ describe('coinWithBalance', () => {
});
});

it('works with zero balance coin', async () => {
const tx = new Transaction();
const receiver = new Ed25519Keypair();

tx.transferObjects(
[
coinWithBalance({
type: testTypeZero,
balance: 0n,
}),
],
receiver.toSuiAddress(),
);
tx.setSender(publishToolbox.keypair.toSuiAddress());

expect(
JSON.parse(
await tx.toJSON({
supportedIntents: ['CoinWithBalance'],
}),
),
).toEqual({
expiration: null,
gasData: {
budget: null,
owner: null,
payment: null,
price: null,
},
inputs: [
{
Pure: {
bytes: toBase64(fromHex(receiver.toSuiAddress())),
},
},
],
sender: publishToolbox.keypair.toSuiAddress(),
commands: [
{
$Intent: {
data: {
balance: '0',
type: testTypeZero,
},
inputs: {},
name: 'CoinWithBalance',
},
},
{
TransferObjects: {
objects: [
{
Result: 0,
},
],
address: {
Input: 0,
},
},
},
],
version: 2,
});

expect(
JSON.parse(
await tx.toJSON({
supportedIntents: [],
client: publishToolbox.client,
}),
),
).toEqual({
expiration: null,
gasData: {
budget: null,
owner: null,
payment: null,
price: null,
},
inputs: [
{
Pure: {
bytes: toBase64(fromHex(receiver.toSuiAddress())),
},
},
],
sender: publishToolbox.keypair.toSuiAddress(),
commands: [
{
MoveCall: {
arguments: [],
function: 'zero',
module: 'coin',
package: '0x0000000000000000000000000000000000000000000000000000000000000002',
typeArguments: [testTypeZero],
},
},
{
TransferObjects: {
objects: [{ Result: 0 }],
address: {
Input: 0,
},
},
},
],
version: 2,
});

const { digest } = await toolbox.client.signAndExecuteTransaction({
transaction: tx,
signer: publishToolbox.keypair,
});

const result = await toolbox.client.waitForTransaction({
digest,
options: { showEffects: true, showBalanceChanges: true, showObjectChanges: true },
});

expect(result.effects?.status.status).toBe('success');
expect(
result.objectChanges?.filter((change) => {
if (change.type !== 'created') return false;
if (typeof change.owner !== 'object' || !('AddressOwner' in change.owner)) return false;

return (
change.objectType === `0x2::coin::Coin<${testTypeZero}>` &&
change.owner.AddressOwner === receiver.toSuiAddress()
);
}).length,
).toEqual(1);
});

it('works with multiple coins', async () => {
const tx = new Transaction();
const receiver = new Ed25519Keypair();
Expand All @@ -332,6 +467,7 @@ describe('coinWithBalance', () => {
coinWithBalance({ type: testType, balance: 2n }),
coinWithBalance({ type: 'gas', balance: 3n }),
coinWithBalance({ type: 'gas', balance: 4n }),
coinWithBalance({ type: testTypeZero, balance: 0n }),
],
receiver.toSuiAddress(),
);
Expand Down Expand Up @@ -401,6 +537,16 @@ describe('coinWithBalance', () => {
name: 'CoinWithBalance',
},
},
{
$Intent: {
data: {
balance: '0',
type: testTypeZero,
},
inputs: {},
name: 'CoinWithBalance',
},
},
{
TransferObjects: {
objects: [
Expand All @@ -416,6 +562,9 @@ describe('coinWithBalance', () => {
{
Result: 3,
},
{
Result: 4,
},
],
address: {
Input: 0,
Expand Down Expand Up @@ -523,13 +672,23 @@ describe('coinWithBalance', () => {
],
},
},
{
MoveCall: {
arguments: [],
function: 'zero',
module: 'coin',
package: '0x0000000000000000000000000000000000000000000000000000000000000002',
typeArguments: [testTypeZero],
},
},
{
TransferObjects: {
objects: [
{ NestedResult: [0, 0] },
{ NestedResult: [1, 0] },
{ NestedResult: [2, 0] },
{ NestedResult: [3, 0] },
{ Result: 4 },
],
address: {
Input: 0,
Expand All @@ -547,7 +706,7 @@ describe('coinWithBalance', () => {

const result = await toolbox.client.waitForTransaction({
digest,
options: { showEffects: true, showBalanceChanges: true },
options: { showEffects: true, showBalanceChanges: true, showObjectChanges: true },
});

expect(result.effects?.status.status).toBe('success');
Expand All @@ -574,5 +733,16 @@ describe('coinWithBalance', () => {
},
},
]);
expect(
result.objectChanges?.filter((change) => {
if (change.type !== 'created') return false;
if (typeof change.owner !== 'object' || !('AddressOwner' in change.owner)) return false;

return (
change.objectType === `0x2::coin::Coin<${testTypeZero}>` &&
change.owner.AddressOwner === receiver.toSuiAddress()
);
}).length,
).toEqual(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

module coin_metadata::test_zero;

use sui::coin;
use sui::url;

public struct TEST_ZERO has drop {}

fun init(witness: TEST_ZERO, ctx: &mut TxContext) {
let (treasury_cap, metadata) = coin::create_currency<TEST_ZERO>(
witness,
2,
b"TEST",
b"Test Coin",
b"Test coin metadata",
option::some(url::new_unsafe_from_bytes(b"http://sui.io")),
ctx,
);

transfer::public_share_object(metadata);
transfer::public_share_object(treasury_cap)
}

0 comments on commit e7bc63e

Please sign in to comment.