Skip to content

Commit

Permalink
Add typescript sdk support
Browse files Browse the repository at this point in the history
  • Loading branch information
tzakian committed Sep 26, 2023
1 parent d4c8549 commit 7ebccba
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 1 deletion.
6 changes: 5 additions & 1 deletion sdk/typescript/src/bcs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export type SharedObjectRef = {
/**
* An object argument.
*/
export type ObjectArg = { ImmOrOwned: SuiObjectRef } | { Shared: SharedObjectRef };
export type ObjectArg =
| { ImmOrOwned: SuiObjectRef }
| { Shared: SharedObjectRef }
| { Receiving: SuiObjectRef };

/**
* A pure argument.
Expand Down Expand Up @@ -132,6 +135,7 @@ const BCS_SPEC: TypeSchema = {
ObjectArg: {
ImmOrOwned: 'SuiObjectRef',
Shared: 'SharedObjectRef',
Receiving: 'SuiObjectRef',
},
CallArg: {
Pure: [VECTOR, BCS.U8],
Expand Down
17 changes: 17 additions & 0 deletions sdk/typescript/src/builder/Inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ObjectArg = union([
mutable: boolean(),
}),
}),
object({ Receiving: SuiObjectRef }),
]);

export const PureCallArg = object({ Pure: array(integer()) });
Expand Down Expand Up @@ -61,6 +62,17 @@ export const Inputs = {
},
};
},
ReceivingRef({ objectId, digest, version }: SuiObjectRef): ObjectCallArg {
return {
Object: {
Receiving: {
digest,
version,
objectId: normalizeSuiAddress(objectId),
},
},
};
},
};

export function getIdFromCallArg(arg: string | ObjectCallArg) {
Expand All @@ -70,6 +82,11 @@ export function getIdFromCallArg(arg: string | ObjectCallArg) {
if ('ImmOrOwned' in arg.Object) {
return normalizeSuiAddress(arg.Object.ImmOrOwned.objectId);
}

if ('Receiving' in arg.Object) {
return normalizeSuiAddress(arg.Object.Receiving.objectId);
}

return normalizeSuiAddress(arg.Object.Shared.objectId);
}

Expand Down
22 changes: 22 additions & 0 deletions sdk/typescript/src/builder/TransactionBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ function createTransactionResult(index: number): TransactionResult {
}) as TransactionResult;
}

function isReceivingType(normalizedType: SuiMoveNormalizedType): boolean {
const tag = extractStructTag(normalizedType);
if (tag) {
return (
tag.Struct.address === '0x2' &&
tag.Struct.module === 'transfer' &&
tag.Struct.name === 'Receiving'
);
}
return false;
}

function expectClient(options: BuildOptions): SuiClient {
if (!options.client && !options.provider) {
throw new Error(
Expand Down Expand Up @@ -302,6 +314,14 @@ export class TransactionBlock {
return this.object(Inputs.ObjectRef(...args));
}

/**
* Add a new receiving input to the transaction using the fully-resolved object reference.
* If you only have an object ID, use `builder.object(id)` instead.
*/
receivingRef(...args: Parameters<(typeof Inputs)['ReceivingRef']>) {
return this.object(Inputs.ReceivingRef(...args));
}

/**
* Add a new shared object input to the transaction using the fully-resolved shared object reference.
* If you only have an object ID, use `builder.object(id)` instead.
Expand Down Expand Up @@ -706,6 +726,8 @@ export class TransactionBlock {
initialSharedVersion,
mutable,
});
} else if (normalizedType && isReceivingType(normalizedType)) {
input.value = Inputs.ReceivingRef(getObjectReference(object)!);
} else {
input.value = Inputs.ObjectRef(getObjectReference(object as SuiObjectResponse)!);
}
Expand Down
37 changes: 37 additions & 0 deletions sdk/typescript/src/builder/__tests__/Transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ it('can construct and serialize an empty tranaction', () => {
expect(() => tx.serialize()).not.toThrow();
});

it('can construct a receiving transaction argument', () => {
const tx = new TransactionBlock();
tx.object(Inputs.ReceivingRef(ref()));
expect(() => tx.serialize()).not.toThrow();
});

it('receiving transaction argument different from object argument', () => {
const oref = ref();
const rtx = new TransactionBlock();
rtx.object(Inputs.ReceivingRef(oref));
const otx = new TransactionBlock();
otx.object(Inputs.ObjectRef(oref));
expect(() => rtx.serialize()).not.toThrow();
expect(() => otx.serialize()).not.toThrow();
expect(otx.serialize()).not.toEqual(rtx.serialize());
});

it('can be serialized and deserialized to the same values', () => {
const tx = new TransactionBlock();
tx.add(Transactions.SplitCoins(tx.gas, [tx.pure(100)]));
Expand Down Expand Up @@ -109,6 +126,26 @@ describe('offline build', () => {
await tx.build();
});

it('uses a receiving argument', async () => {
const tx = setup();
tx.object(Inputs.ObjectRef(ref()));
const coin = tx.add(Transactions.SplitCoins(tx.gas, [tx.pure(100)]));
tx.add(Transactions.MergeCoins(tx.gas, [coin, tx.object(Inputs.ObjectRef(ref()))]));
tx.add(
Transactions.MoveCall({
target: '0x2::devnet_nft::mint',
typeArguments: [],
arguments: [tx.object(Inputs.ObjectRef(ref())), tx.object(Inputs.ReceivingRef(ref()))],
}),
);

const bytes = await tx.build();
const tx2 = TransactionBlock.from(bytes);
const bytes2 = await tx2.build();

expect(bytes).toEqual(bytes2);
});

it('builds a more complex interaction', async () => {
const tx = setup();
const coin = tx.add(Transactions.SplitCoins(tx.gas, [tx.pure(100)]));
Expand Down
7 changes: 7 additions & 0 deletions sdk/typescript/src/types/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export const SuiCallArg = union([
initialSharedVersion: string(),
mutable: boolean(),
}),
object({
type: literal('object'),
objectType: literal('receiving'),
objectId: string(),
version: string(),
digest: string(),
}),
]);
export type SuiCallArg = Infer<typeof SuiCallArg>;

Expand Down
9 changes: 9 additions & 0 deletions sdk/typescript/test/e2e/data/tto/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "tto"
version = "0.0.1"

[dependencies]
Sui = { local = "../../../../../../crates/sui-framework/packages/sui-framework" }

[addresses]
tto = "0x0"
46 changes: 46 additions & 0 deletions sdk/typescript/test/e2e/data/tto/sources/tto1.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module tto::tto {
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer::{Self, Receiving};

struct A has key, store {
id: UID,
}

struct B has key, store {
id: UID,
}

public fun start(ctx: &mut TxContext) {
let a = A { id: object::new(ctx) };
let a_address = object::id_address(&a);
let b = B { id: object::new(ctx) };
let c = B { id: object::new(ctx) };
transfer::share_object(c);
transfer::public_transfer(a, tx_context::sender(ctx));
transfer::public_transfer(b, a_address);
}

public entry fun receiver(parent: &mut A, x: Receiving<B>) {
let b = transfer::receive(&mut parent.id, x);
transfer::public_transfer(b, @tto);
}

public entry fun deleter(parent: &mut A, x: Receiving<B>) {
let B { id } = transfer::receive(&mut parent.id, x);
object::delete(id);
}

public fun return_(parent: &mut A, x: Receiving<B>): B {
transfer::receive(&mut parent.id, x)
}

public entry fun delete_(b: B) {
let B { id } = b;
object::delete(id);
}

public fun invalid_call_immut_ref(_parent: &mut A, _x: &Receiving<B>) { }
public fun invalid_call_mut_ref(_parent: &mut A, _x: &mut Receiving<B>) { }
public fun dropper(_parent: &mut A, _x: Receiving<B>) { }
}
146 changes: 146 additions & 0 deletions sdk/typescript/test/e2e/receive-object.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect, beforeAll, beforeEach } from 'vitest';

import { setup, TestToolbox, publishPackage } from './utils/setup';
import { OwnedObjectRef, SuiClient } from '../../src/client';
import { TransactionBlock } from '../../src/builder';
import type { Keypair } from '../../src/cryptography';

function getOwnerAddress(o: OwnedObjectRef): string | undefined {
// const owner = getObjectOwner(o);
if (typeof o.owner == 'object' && 'AddressOwner' in o.owner) {
return o.owner.AddressOwner;
} else {
return undefined;
}
}

describe('Transfer to Object', () => {
let toolbox: TestToolbox;
let packageId: string;
let parentObjectId: OwnedObjectRef;
let receiveObjectId: OwnedObjectRef;
let sharedObjectId: string;

beforeAll(async () => {
const packagePath = __dirname + '/./data/tto';
({ packageId } = await publishPackage(packagePath));
});

beforeEach(async () => {
toolbox = await setup();
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::start`,
typeArguments: [],
arguments: [],
});
const x = await validateTransaction(toolbox.client, toolbox.keypair, tx);
const y = (x.effects?.created)!.map((o) => getOwnerAddress(o))!;
receiveObjectId = (x.effects?.created)!.filter(
(o) => !y.includes(o.reference.objectId) && getOwnerAddress(o) !== undefined,
)[0];
parentObjectId = (x.effects?.created)!.filter(
(o) => y.includes(o.reference.objectId) && getOwnerAddress(o) !== undefined,
)[0];
const sharedObject = (x.effects?.created)!.filter((o) => getOwnerAddress(o) === undefined)[0];
sharedObjectId = sharedObject.reference.objectId;
});

it('Basic Receive: receive and then transfer', async () => {
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::receiver`,
typeArguments: [],
arguments: [
tx.object(parentObjectId.reference.objectId),
tx.object(receiveObjectId.reference.objectId),
],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});

it('Basic Receive: receive and then delete', async () => {
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::deleter`,
typeArguments: [],
arguments: [
tx.object(parentObjectId.reference.objectId),
tx.object(receiveObjectId.reference.objectId),
],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});

it('receive + return, then delete', async () => {
const tx = new TransactionBlock();
const b = tx.moveCall({
target: `${packageId}::tto::return_`,
typeArguments: [],
arguments: [
tx.object(parentObjectId.reference.objectId),
tx.object(receiveObjectId.reference.objectId),
],
});
tx.moveCall({
target: `${packageId}::tto::delete_`,
typeArguments: [],
arguments: [b],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});

it('Basic Receive: &Receiving arg type', async () => {
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::invalid_call_immut_ref`,
typeArguments: [],
arguments: [
tx.object(parentObjectId.reference.objectId),
tx.object(receiveObjectId.reference.objectId),
],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});

it('Basic Receive: &mut Receiving arg type', async () => {
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::invalid_call_mut_ref`,
typeArguments: [],
arguments: [
tx.object(parentObjectId.reference.objectId),
tx.object(receiveObjectId.reference.objectId),
],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});

it.fails('Trying to pass shared object as receiving argument', async () => {
const tx = new TransactionBlock();
tx.moveCall({
target: `${packageId}::tto::receiver`,
typeArguments: [],
arguments: [tx.object(parentObjectId.reference.objectId), tx.object(sharedObjectId)],
});
await validateTransaction(toolbox.client, toolbox.keypair, tx);
});
});

async function validateTransaction(client: SuiClient, signer: Keypair, tx: TransactionBlock) {
tx.setSenderIfNotSet(signer.getPublicKey().toSuiAddress());
const localDigest = await tx.getDigest({ client });
const result = await client.signAndExecuteTransactionBlock({
signer,
transactionBlock: tx,
options: {
showEffects: true,
},
});
expect(localDigest).toEqual(result.digest);
expect(result.effects?.status.status).toEqual('success');
return result;
}

0 comments on commit 7ebccba

Please sign in to comment.