@mysten/sui
Version:
Sui TypeScript API
560 lines (506 loc) • 15.7 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { fromBase64, toBase64 } from '@mysten/bcs';
import type { bcs } from '../bcs/index.js';
import { TransactionDataBuilder } from '../transactions/TransactionData.js';
import type {
CallArg,
Command as BcsCommand,
TransactionData,
} from '../transactions/data/internal.js';
import type { Transaction as GrpcTransaction } from '../grpc/proto/sui/rpc/v2/transaction.js';
import {
Transaction as GrpcTransactionType,
TransactionExpiration_TransactionExpirationKind,
} from '../grpc/proto/sui/rpc/v2/transaction.js';
import type { ObjectReference } from '../grpc/proto/sui/rpc/v2/object_reference.js';
import type { Input } from '../grpc/proto/sui/rpc/v2/input.js';
import { FundsWithdrawal_Source, Input_InputKind } from '../grpc/proto/sui/rpc/v2/input.js';
import type { Command } from '../grpc/proto/sui/rpc/v2/transaction.js';
import type { Argument } from '../grpc/proto/sui/rpc/v2/argument.js';
import { Argument_ArgumentKind } from '../grpc/proto/sui/rpc/v2/argument.js';
import { Transaction } from '../transactions/Transaction.js';
/**
* Converts CallArg (TypeScript internal format) to gRPC Input format
*/
function callArgToGrpcInput(arg: CallArg): Input {
switch (arg.$kind) {
case 'Pure':
// Pure.bytes is a base64-encoded string that needs to be decoded
return {
kind: Input_InputKind.PURE,
pure: fromBase64(arg.Pure.bytes),
};
case 'Object':
if (arg.Object.$kind === 'ImmOrOwnedObject') {
return {
kind: Input_InputKind.IMMUTABLE_OR_OWNED,
objectId: arg.Object.ImmOrOwnedObject.objectId,
version: BigInt(arg.Object.ImmOrOwnedObject.version),
digest: arg.Object.ImmOrOwnedObject.digest,
};
} else if (arg.Object.$kind === 'SharedObject') {
return {
kind: Input_InputKind.SHARED,
objectId: arg.Object.SharedObject.objectId,
version: BigInt(arg.Object.SharedObject.initialSharedVersion),
mutable: arg.Object.SharedObject.mutable,
};
} else if (arg.Object.$kind === 'Receiving') {
return {
kind: Input_InputKind.RECEIVING,
objectId: arg.Object.Receiving.objectId,
version: BigInt(arg.Object.Receiving.version),
digest: arg.Object.Receiving.digest,
};
}
throw new Error(`Unknown Object kind: ${JSON.stringify(arg.Object)}`);
case 'UnresolvedObject':
const unresolved = arg.UnresolvedObject;
return {
objectId: unresolved.objectId,
version: unresolved.version
? BigInt(unresolved.version)
: unresolved.initialSharedVersion
? BigInt(unresolved.initialSharedVersion)
: undefined,
digest: unresolved.digest ?? undefined,
mutable: unresolved.mutable ?? undefined,
};
case 'UnresolvedPure':
throw new Error('UnresolvedPure arguments must be resolved before converting to gRPC format');
case 'FundsWithdrawal': {
const withdrawal = arg.FundsWithdrawal;
return {
kind: Input_InputKind.FUNDS_WITHDRAWAL,
fundsWithdrawal: {
amount:
withdrawal.reservation.$kind === 'MaxAmountU64'
? BigInt(withdrawal.reservation.MaxAmountU64)
: undefined,
coinType: withdrawal.typeArg.$kind === 'Balance' ? withdrawal.typeArg.Balance : undefined,
source:
withdrawal.withdrawFrom.$kind === 'Sponsor'
? FundsWithdrawal_Source.SPONSOR
: FundsWithdrawal_Source.SENDER,
},
};
}
default:
throw new Error(`Unknown CallArg kind: ${JSON.stringify(arg)}`);
}
}
/**
* Converts a TypeScript Argument to gRPC Argument
*/
function tsArgumentToGrpcArgument(arg: typeof bcs.Argument.$inferInput): Argument {
if ('GasCoin' in arg) {
return { kind: Argument_ArgumentKind.GAS };
} else if ('Input' in arg) {
return { kind: Argument_ArgumentKind.INPUT, input: arg.Input };
} else if ('Result' in arg) {
return { kind: Argument_ArgumentKind.RESULT, result: arg.Result };
} else if ('NestedResult' in arg) {
return {
kind: Argument_ArgumentKind.RESULT,
result: arg.NestedResult[0],
subresult: arg.NestedResult[1],
};
}
throw new Error(`Unknown Argument: ${JSON.stringify(arg)}`);
}
/**
* Converts TypeScript Command to gRPC Command
*/
function tsCommandToGrpcCommand(cmd: BcsCommand): Command {
switch (cmd.$kind) {
case 'MoveCall':
return {
command: {
oneofKind: 'moveCall',
moveCall: {
package: cmd.MoveCall.package,
module: cmd.MoveCall.module,
function: cmd.MoveCall.function,
typeArguments: cmd.MoveCall.typeArguments,
arguments: cmd.MoveCall.arguments.map(tsArgumentToGrpcArgument),
},
},
};
case 'TransferObjects':
return {
command: {
oneofKind: 'transferObjects',
transferObjects: {
objects: cmd.TransferObjects.objects.map(tsArgumentToGrpcArgument),
address: tsArgumentToGrpcArgument(cmd.TransferObjects.address),
},
},
};
case 'SplitCoins':
return {
command: {
oneofKind: 'splitCoins',
splitCoins: {
coin: tsArgumentToGrpcArgument(cmd.SplitCoins.coin),
amounts: cmd.SplitCoins.amounts.map(tsArgumentToGrpcArgument),
},
},
};
case 'MergeCoins':
return {
command: {
oneofKind: 'mergeCoins',
mergeCoins: {
coin: tsArgumentToGrpcArgument(cmd.MergeCoins.destination),
coinsToMerge: cmd.MergeCoins.sources.map(tsArgumentToGrpcArgument),
},
},
};
case 'Publish':
return {
command: {
oneofKind: 'publish',
publish: {
// modules are base64-encoded strings in internal format
modules: cmd.Publish.modules.map((m) => fromBase64(m)),
dependencies: cmd.Publish.dependencies,
},
},
};
case 'MakeMoveVec':
return {
command: {
oneofKind: 'makeMoveVector',
makeMoveVector: {
elementType: cmd.MakeMoveVec.type ?? undefined,
elements: cmd.MakeMoveVec.elements.map(tsArgumentToGrpcArgument),
},
},
};
case 'Upgrade':
return {
command: {
oneofKind: 'upgrade',
upgrade: {
// modules are base64-encoded strings in internal format
modules: cmd.Upgrade.modules.map((m) => fromBase64(m)),
dependencies: cmd.Upgrade.dependencies,
package: cmd.Upgrade.package,
ticket: tsArgumentToGrpcArgument(cmd.Upgrade.ticket),
},
},
};
default:
throw new Error(`Unknown Command kind: ${JSON.stringify(cmd)}`);
}
}
export function transactionDataToGrpcTransaction(data: TransactionData): GrpcTransaction {
const grpcInputs = data.inputs.map(callArgToGrpcInput);
const grpcCommands = data.commands.map(tsCommandToGrpcCommand);
const transaction: GrpcTransaction = {
version: 1,
kind: {
data: {
oneofKind: 'programmableTransaction',
programmableTransaction: {
inputs: grpcInputs,
commands: grpcCommands,
},
},
},
};
if (data.sender) {
transaction.sender = data.sender;
}
const gasOwner = data.gasData.owner ?? data.sender;
transaction.gasPayment = {
objects: data.gasData.payment
? data.gasData.payment.map((ref) => ({
objectId: ref.objectId,
version: BigInt(ref.version),
digest: ref.digest,
}))
: [],
price: data.gasData.price ? BigInt(data.gasData.price) : undefined,
budget: data.gasData.budget ? BigInt(data.gasData.budget) : undefined,
};
if (gasOwner) {
transaction.gasPayment.owner = gasOwner;
}
if (data.expiration) {
if ('None' in data.expiration) {
transaction.expiration = {
kind: TransactionExpiration_TransactionExpirationKind.NONE,
};
} else if (data.expiration.$kind === 'Epoch') {
transaction.expiration = {
kind: TransactionExpiration_TransactionExpirationKind.EPOCH,
epoch: BigInt(data.expiration.Epoch),
};
} else if (data.expiration.$kind === 'ValidDuring') {
const validDuring = data.expiration.ValidDuring;
transaction.expiration = {
kind: TransactionExpiration_TransactionExpirationKind.VALID_DURING,
minEpoch: validDuring.minEpoch != null ? BigInt(validDuring.minEpoch) : undefined,
epoch: validDuring.maxEpoch != null ? BigInt(validDuring.maxEpoch) : undefined,
chain: validDuring.chain,
nonce: validDuring.nonce,
};
}
}
return transaction;
}
export function applyGrpcResolvedTransaction(
transactionData: TransactionDataBuilder,
resolvedTransaction: GrpcTransaction,
options?: { onlyTransactionKind?: boolean },
): void {
const resolved = grpcTransactionToTransactionData(resolvedTransaction);
if (options?.onlyTransactionKind) {
transactionData.applyResolvedData({
...resolved,
gasData: {
budget: null,
owner: null,
payment: null,
price: null,
},
expiration: null,
});
} else {
transactionData.applyResolvedData(resolved);
}
}
/**
* Converts an array of ObjectReferences from gRPC format to TypeScript SuiObjectRef format
*/
export function grpcObjectReferencesToBcs(refs: ObjectReference[]): {
objectId: string;
version: string;
digest: string;
}[] {
return refs.map((ref) => ({
objectId: ref.objectId!,
version: ref.version?.toString()!,
digest: ref.digest!,
}));
}
export function transactionToGrpcTransaction(transaction: Transaction) {
const snapshot = transaction.getData();
if (!snapshot.sender) {
snapshot.sender = '0x0000000000000000000000000000000000000000000000000000000000000000';
}
return transactionDataToGrpcTransaction(snapshot);
}
export function transactionToGrpcJson(transaction: Transaction): unknown {
const grpcTransaction = transactionToGrpcTransaction(transaction);
return GrpcTransactionType.toJson(grpcTransaction);
}
function grpcInputToCallArg(input: Input): CallArg {
switch (input.kind) {
case Input_InputKind.PURE:
return {
$kind: 'Pure',
Pure: { bytes: toBase64(input.pure!) },
};
case Input_InputKind.IMMUTABLE_OR_OWNED:
return {
$kind: 'Object',
Object: {
$kind: 'ImmOrOwnedObject',
ImmOrOwnedObject: {
objectId: input.objectId!,
version: input.version!.toString(),
digest: input.digest!,
},
},
};
case Input_InputKind.SHARED:
return {
$kind: 'Object',
Object: {
$kind: 'SharedObject',
SharedObject: {
objectId: input.objectId!,
initialSharedVersion: input.version!.toString(),
mutable: input.mutable ?? false,
},
},
};
case Input_InputKind.RECEIVING:
return {
$kind: 'Object',
Object: {
$kind: 'Receiving',
Receiving: {
objectId: input.objectId!,
version: input.version!.toString(),
digest: input.digest!,
},
},
};
case Input_InputKind.FUNDS_WITHDRAWAL:
return {
$kind: 'FundsWithdrawal',
FundsWithdrawal: {
reservation: {
$kind: 'MaxAmountU64' as const,
MaxAmountU64: input.fundsWithdrawal?.amount?.toString() ?? '0',
},
typeArg: {
$kind: 'Balance' as const,
Balance: input.fundsWithdrawal?.coinType ?? '0x2::sui::SUI',
},
withdrawFrom:
input.fundsWithdrawal?.source === FundsWithdrawal_Source.SPONSOR
? { $kind: 'Sponsor' as const, Sponsor: true as const }
: { $kind: 'Sender' as const, Sender: true as const },
},
};
default:
throw new Error(`Unknown Input kind: ${JSON.stringify(input)}`);
}
}
function grpcArgumentToTsArgument(
arg: Argument,
):
| { $kind: 'GasCoin'; GasCoin: true }
| { $kind: 'Input'; Input: number }
| { $kind: 'Result'; Result: number }
| { $kind: 'NestedResult'; NestedResult: [number, number] } {
switch (arg.kind) {
case Argument_ArgumentKind.GAS:
return { $kind: 'GasCoin', GasCoin: true };
case Argument_ArgumentKind.INPUT:
return { $kind: 'Input', Input: arg.input! };
case Argument_ArgumentKind.RESULT:
if (arg.subresult != null) {
return { $kind: 'NestedResult', NestedResult: [arg.result!, arg.subresult] };
}
return { $kind: 'Result', Result: arg.result! };
default:
throw new Error(`Unknown Argument kind: ${JSON.stringify(arg)}`);
}
}
function grpcCommandToTsCommand(cmd: Command): BcsCommand {
const command = cmd.command;
if (!command) {
throw new Error('Command is missing');
}
switch (command.oneofKind) {
case 'moveCall':
return {
$kind: 'MoveCall',
MoveCall: {
package: command.moveCall.package!,
module: command.moveCall.module!,
function: command.moveCall.function!,
typeArguments: command.moveCall.typeArguments ?? [],
arguments: command.moveCall.arguments.map(grpcArgumentToTsArgument),
},
};
case 'transferObjects':
return {
$kind: 'TransferObjects',
TransferObjects: {
objects: command.transferObjects.objects.map(grpcArgumentToTsArgument),
address: grpcArgumentToTsArgument(command.transferObjects.address!),
},
};
case 'splitCoins':
return {
$kind: 'SplitCoins',
SplitCoins: {
coin: grpcArgumentToTsArgument(command.splitCoins.coin!),
amounts: command.splitCoins.amounts.map(grpcArgumentToTsArgument),
},
};
case 'mergeCoins':
return {
$kind: 'MergeCoins',
MergeCoins: {
destination: grpcArgumentToTsArgument(command.mergeCoins.coin!),
sources: command.mergeCoins.coinsToMerge.map(grpcArgumentToTsArgument),
},
};
case 'publish':
return {
$kind: 'Publish',
Publish: {
modules: command.publish.modules.map((m) => toBase64(m)),
dependencies: command.publish.dependencies ?? [],
},
};
case 'makeMoveVector':
return {
$kind: 'MakeMoveVec',
MakeMoveVec: {
type: command.makeMoveVector.elementType ?? null,
elements: command.makeMoveVector.elements.map(grpcArgumentToTsArgument),
},
};
case 'upgrade':
return {
$kind: 'Upgrade',
Upgrade: {
modules: command.upgrade.modules.map((m) => toBase64(m)),
dependencies: command.upgrade.dependencies ?? [],
package: command.upgrade.package!,
ticket: grpcArgumentToTsArgument(command.upgrade.ticket!),
},
};
default:
throw new Error(`Unknown Command kind: ${JSON.stringify(command)}`);
}
}
export function grpcTransactionToTransactionData(grpcTx: GrpcTransaction): TransactionData {
const programmableTx = grpcTx.kind?.data;
if (programmableTx?.oneofKind !== 'programmableTransaction') {
throw new Error('Only programmable transactions are supported');
}
const inputs = programmableTx.programmableTransaction.inputs.map(grpcInputToCallArg);
const commands = programmableTx.programmableTransaction.commands.map(grpcCommandToTsCommand);
let expiration: TransactionData['expiration'] = null;
if (grpcTx.expiration) {
switch (grpcTx.expiration.kind) {
case TransactionExpiration_TransactionExpirationKind.NONE:
expiration = { $kind: 'None', None: true };
break;
case TransactionExpiration_TransactionExpirationKind.EPOCH:
expiration = { $kind: 'Epoch', Epoch: grpcTx.expiration.epoch!.toString() };
break;
case TransactionExpiration_TransactionExpirationKind.VALID_DURING:
expiration = {
$kind: 'ValidDuring',
ValidDuring: {
minEpoch: grpcTx.expiration.minEpoch?.toString() ?? null,
maxEpoch: grpcTx.expiration.epoch?.toString() ?? null,
minTimestamp: null,
maxTimestamp: null,
chain: grpcTx.expiration.chain ?? '',
nonce: grpcTx.expiration.nonce ?? 0,
},
};
break;
}
}
return {
version: 2 as const,
sender: grpcTx.sender ?? null,
expiration,
gasData: {
budget: grpcTx.gasPayment?.budget?.toString() ?? null,
owner: grpcTx.gasPayment?.owner ?? null,
payment:
grpcTx.gasPayment?.objects?.map((obj) => ({
objectId: obj.objectId!,
version: obj.version!.toString(),
digest: obj.digest!,
})) ?? null,
price: grpcTx.gasPayment?.price?.toString() ?? null,
},
inputs,
commands,
};
}