Skip to content

Commit

Permalink
[TypeScript SDK] Implement execute move call (MystenLabs#2353)
Browse files Browse the repository at this point in the history
  • Loading branch information
666lcz authored May 31, 2022
1 parent ca6b06a commit 46c36d5
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 18 deletions.
31 changes: 27 additions & 4 deletions sdk/typescript/src/index.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Generated type guards for "index.ts".
* WARNING: Do not manually change this file.
*/
import { Ed25519KeypairData, Keypair, PublicKeyInitData, PublicKeyData, TransferCoinTransaction, TxnDataSerializer, TransactionDigest, SuiAddress, ObjectOwner, SuiObjectRef, SuiObjectInfo, ObjectContentFields, MovePackageContent, SuiData, SuiMoveObject, SuiMovePackage, SuiObject, ObjectStatus, ObjectType, GetOwnedObjectsResponse, GetObjectDataResponse, ObjectDigest, ObjectId, SequenceNumber, TransferCoin, RawAuthoritySignInfo, TransactionKindName, SuiTransactionKind, TransactionData, EpochId, AuthorityQuorumSignInfo, CertifiedTransaction, GasCostSummary, ExecutionStatusType, ExecutionStatus, OwnedObjectRef, TransactionEffects, TransactionEffectsResponse, GatewayTxSeqNumber, GetTxnDigestsResponse, Event, MoveCall, SuiJsonValue, EmptySignInfo, AuthorityName, AuthoritySignature, TransactionBytes, SplitCoinResponse, TransactionResponse } from "./index";
import { Ed25519KeypairData, Keypair, PublicKeyInitData, PublicKeyData, TransferCoinTransaction, MoveCallTransaction, TxnDataSerializer, TransactionDigest, SuiAddress, ObjectOwner, SuiObjectRef, SuiObjectInfo, ObjectContentFields, MovePackageContent, SuiData, SuiMoveObject, SuiMovePackage, SuiObject, ObjectStatus, ObjectType, GetOwnedObjectsResponse, GetObjectDataResponse, ObjectDigest, ObjectId, SequenceNumber, TransferCoin, RawAuthoritySignInfo, TransactionKindName, SuiTransactionKind, TransactionData, EpochId, AuthorityQuorumSignInfo, CertifiedTransaction, GasCostSummary, ExecutionStatusType, ExecutionStatus, OwnedObjectRef, TransactionEffects, TransactionEffectsResponse, GatewayTxSeqNumber, GetTxnDigestsResponse, Event, MoveCall, SuiJsonValue, EmptySignInfo, AuthorityName, AuthoritySignature, TransactionBytes, SplitCoinResponse, TransactionResponse } from "./index";
import { BN } from "bn.js";

export function isEd25519KeypairData(obj: any, _argumentName?: string): obj is Ed25519KeypairData {
Expand Down Expand Up @@ -56,20 +56,43 @@ export function isTransferCoinTransaction(obj: any, _argumentName?: string): obj
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
isTransactionDigest(obj.signer) as boolean &&
isTransactionDigest(obj.objectId) as boolean &&
isTransactionDigest(obj.gasPayment) as boolean &&
(typeof obj.gasPayment === "undefined" ||
isTransactionDigest(obj.gasPayment) as boolean) &&
isSequenceNumber(obj.gasBudget) as boolean &&
isTransactionDigest(obj.recipient) as boolean
)
}

export function isMoveCallTransaction(obj: any, _argumentName?: string): obj is MoveCallTransaction {
return (
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
isTransactionDigest(obj.packageObjectId) as boolean &&
isTransactionDigest(obj.module) as boolean &&
isTransactionDigest(obj.function) as boolean &&
Array.isArray(obj.typeArguments) &&
obj.typeArguments.every((e: any) =>
isTransactionDigest(e) as boolean
) &&
Array.isArray(obj.arguments) &&
obj.arguments.every((e: any) =>
isSuiJsonValue(e) as boolean
) &&
(typeof obj.gasPayment === "undefined" ||
isTransactionDigest(obj.gasPayment) as boolean) &&
isSequenceNumber(obj.gasBudget) as boolean
)
}

export function isTxnDataSerializer(obj: any, _argumentName?: string): obj is TxnDataSerializer {
return (
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
typeof obj.newTransferCoin === "function"
typeof obj.newTransferCoin === "function" &&
typeof obj.newMoveCall === "function"
)
}

Expand Down
5 changes: 3 additions & 2 deletions sdk/typescript/src/signers/raw-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { Ed25519Keypair } from '../cryptography/ed25519-keypair';
import { Provider } from '../providers/provider';
import { Base64DataBuffer } from '../serialization/base64';
import { SuiAddress } from '../types';
import { SignaturePubkeyPair } from './signer';
import { SignerWithProvider } from './signer-with-provider';
import { TxnDataSerializer } from './txn-data-serializers/txn-data-serializer';
Expand All @@ -20,8 +21,8 @@ export class RawSigner extends SignerWithProvider {
this.keypair = keypair;
}

async getAddress(): Promise<string> {
throw this.keypair.getPublicKey().toSuiAddress();
async getAddress(): Promise<SuiAddress> {
return this.keypair.getPublicKey().toSuiAddress();
}

async signData(data: Base64DataBuffer): Promise<SignaturePubkeyPair> {
Expand Down
25 changes: 22 additions & 3 deletions sdk/typescript/src/signers/signer-with-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { JsonRpcProvider } from '../providers/json-rpc-provider';
import { Provider } from '../providers/provider';
import { VoidProvider } from '../providers/void-provider';
import { Base64DataBuffer } from '../serialization/base64';
import { TransactionResponse } from '../types';
import { SuiAddress, TransactionResponse } from '../types';
import { SignaturePubkeyPair, Signer } from './signer';
import { RpcTxnDataSerializer } from './txn-data-serializers/rpc-txn-data-serializer';
import {
MoveCallTransaction,
TransferCoinTransaction,
TxnDataSerializer,
} from './txn-data-serializers/txn-data-serializer';
Expand All @@ -23,7 +24,7 @@ export abstract class SignerWithProvider implements Signer {
// Sub-classes MUST implement these

// Returns the checksum address
abstract getAddress(): Promise<string>;
abstract getAddress(): Promise<SuiAddress>;

/**
* Returns the signature for the data and the public key of the signer
Expand Down Expand Up @@ -68,7 +69,25 @@ export abstract class SignerWithProvider implements Signer {
async transferCoin(
transaction: TransferCoinTransaction
): Promise<TransactionResponse> {
const txBytes = await this.serializer.newTransferCoin(transaction);
const signerAddress = await this.getAddress();
const txBytes = await this.serializer.newTransferCoin(
signerAddress,
transaction
);
return await this.signAndExecuteTransaction(txBytes);
}

/**
* Serialize and Sign a `MoveCall` transaction and submit to the Gateway for execution
*/
async executeMoveCall(
transaction: MoveCallTransaction
): Promise<TransactionResponse> {
const signerAddress = await this.getAddress();
const txBytes = await this.serializer.newMoveCall(
signerAddress,
transaction
);
return await this.signAndExecuteTransaction(txBytes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import { isTransactionBytes } from '../../index.guard';
import { JsonRpcClient } from '../../rpc/client';
import { Base64DataBuffer } from '../../serialization/base64';
import { SuiAddress } from '../../types';
import {
MoveCallTransaction,
TransferCoinTransaction,
TxnDataSerializer,
} from './txn-data-serializer';
Expand All @@ -28,16 +30,44 @@ export class RpcTxnDataSerializer implements TxnDataSerializer {
this.client = new JsonRpcClient(endpoint);
}

async newTransferCoin(t: TransferCoinTransaction): Promise<Base64DataBuffer> {
async newTransferCoin(
signerAddress: SuiAddress,
t: TransferCoinTransaction
): Promise<Base64DataBuffer> {
try {
const resp = await this.client.requestWithType(
'sui_transferCoin',
[t.signer, t.objectId, t.gasPayment, t.gasBudget, t.recipient],
[signerAddress, t.objectId, t.gasPayment, t.gasBudget, t.recipient],
isTransactionBytes
);
return new Base64DataBuffer(resp.txBytes);
} catch (err) {
throw new Error(`Error transferring coin: ${err}`);
throw new Error(`Error transferring coin: ${err} with args ${t}`);
}
}

async newMoveCall(
signerAddress: SuiAddress,
t: MoveCallTransaction
): Promise<Base64DataBuffer> {
try {
const resp = await this.client.requestWithType(
'sui_moveCall',
[
signerAddress,
t.packageObjectId,
t.module,
t.function,
t.typeArguments,
t.arguments,
t.gasPayment,
t.gasBudget,
],
isTransactionBytes
);
return new Base64DataBuffer(resp.txBytes);
} catch (err) {
throw new Error(`Error executing a move call: ${err} with args ${t}`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,42 @@
// SPDX-License-Identifier: Apache-2.0

import { Base64DataBuffer } from '../../serialization/base64';
import { ObjectId, SuiAddress } from '../../types';
import { ObjectId, SuiAddress, SuiJsonValue } from '../../types';

///////////////////////////////
// Exported Types
export interface TransferCoinTransaction {
signer: SuiAddress;
objectId: ObjectId;
gasPayment: ObjectId;
gasPayment?: ObjectId;
gasBudget: number;
recipient: SuiAddress;
}

export interface MoveCallTransaction {
packageObjectId: ObjectId;
module: string;
function: string;
typeArguments: string[];
arguments: SuiJsonValue[];
gasPayment?: ObjectId;
gasBudget: number;
}

///////////////////////////////
// Exported Abstracts
/**
* Serializes a transaction to a string that can be signed by a `Signer`.
*/
export interface TxnDataSerializer {
newTransferCoin(txn: TransferCoinTransaction): Promise<Base64DataBuffer>;
newTransferCoin(
signerAddress: SuiAddress,
txn: TransferCoinTransaction
): Promise<Base64DataBuffer>;

newMoveCall(
signerAddress: SuiAddress,
txn: MoveCallTransaction
): Promise<Base64DataBuffer>;

// TODO: add more interface methods
// TODO: add `newSplitCoin` and `newMergeCoin`
}
65 changes: 64 additions & 1 deletion wallet/src/ui/app/redux/slices/sui-objects/Coin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@

import { isSuiMoveObject } from '@mysten/sui.js';

import type { SuiObject, SuiMoveObject } from '@mysten/sui.js';
import type {
ObjectId,
SuiObject,
SuiMoveObject,
TransactionResponse,
RawSigner,
SuiAddress,
} from '@mysten/sui.js';

const COIN_TYPE = '0x2::Coin::Coin';
const COIN_TYPE_ARG_REGEX = /^0x2::Coin::Coin<(.+)>$/;
Expand All @@ -26,4 +33,60 @@ export class Coin {
public static getBalance(obj: SuiMoveObject) {
return BigInt(obj.fields.balance);
}

public static getID(obj: SuiMoveObject): ObjectId {
return obj.fields.id.id;
}

/**
* Transfer `amount` of Coin<T> to `recipient`.
*
* @param signer A signer with connection to the gateway:e.g., new RawSigner(keypair, new JsonRpcProvider(endpoint))
* @param coins A list of Coins owned by the signer with the same generic type(e.g., 0x2::Sui::Sui)
* @param amount The amount to be transfer
* @param recipient The sui address of the recipient
*/
public static async transferCoin(
signer: RawSigner,
coins: SuiMoveObject[],
amount: BigInt,
recipient: SuiAddress
): Promise<TransactionResponse> {
if (coins.length < 2) {
throw new Error(`Not enough coins to transfer`);
}
const coin = await Coin.selectCoin(coins, amount);
return await signer.transferCoin({
objectId: coin,
gasBudget: 1000,
recipient: recipient,
});
}

private static async selectCoin(
coins: SuiMoveObject[],
amount: BigInt
): Promise<ObjectId> {
const coin = await Coin.selectCoinForSplit(coins, amount);
// TODO: Split coin not implemented yet
return Coin.getID(coin);
}

private static async selectCoinForSplit(
coins: SuiMoveObject[],
amount: BigInt
): Promise<SuiMoveObject> {
// Sort coins by balance in an ascending order
coins.sort();

const coinWithSufficientBalance = coins.find(
(c) => Coin.getBalance(c) >= amount
);
if (coinWithSufficientBalance) {
return coinWithSufficientBalance;
}

// merge coins to have a coin with sufficient balance
throw new Error(`Merge coin Not implemented`);
}
}
33 changes: 33 additions & 0 deletions wallet/src/ui/app/redux/slices/sui-objects/NFT.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { TransactionResponse, RawSigner } from '@mysten/sui.js';

// TODO: Remove this after internal dogfooding
export class ExampleNFT {
/**
* Mint a Example NFT. The wallet address must own enough gas tokens to pay for the transaction.
*
* @param signer A signer with connection to the gateway:e.g., new RawSigner(keypair, new JsonRpcProvider(endpoint))
*/
public static async mintExampleNFT(
signer: RawSigner,
name?: string,
description?: string,
imageUrl?: string
): Promise<TransactionResponse> {
return await signer.executeMoveCall({
packageObjectId: '0x2',
module: 'DevNetNFT',
function: 'mint',
typeArguments: [],
arguments: [
name || 'Example NFT',
description || 'An NFT created by Sui Wallet',
imageUrl ||
'ipfs://bafkreibngqhl3gaa7daob4i2vccziay2jjlp435cf66vhono7nrvww53ty',
],
gasBudget: 10000,
});
}
}

0 comments on commit 46c36d5

Please sign in to comment.