@mysten/sui
Version:
Sui TypeScript API(Work in Progress)
549 lines (505 loc) • 14.4 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { fromBase64, toBase64 } from '@mysten/bcs';
import type { GenericSchema, InferInput, InferOutput } from 'valibot';
import {
array,
bigint,
boolean,
check,
integer,
is,
lazy,
literal,
nullable,
nullish,
number,
object,
optional,
parse,
pipe,
string,
union,
unknown,
} from 'valibot';
import { TypeTagSerializer } from '../../bcs/index.js';
import type { StructTag as StructTagType, TypeTag as TypeTagType } from '../../bcs/types.js';
import { JsonU64, ObjectID, safeEnum, TransactionData } from './internal.js';
import type { Argument } from './internal.js';
export const ObjectRef = object({
digest: string(),
objectId: string(),
version: union([pipe(number(), integer()), string(), bigint()]),
});
const ObjectArg = safeEnum({
ImmOrOwned: ObjectRef,
Shared: object({
objectId: ObjectID,
initialSharedVersion: JsonU64,
mutable: boolean(),
}),
Receiving: ObjectRef,
});
export const NormalizedCallArg = safeEnum({
Object: ObjectArg,
Pure: array(pipe(number(), integer())),
});
const TransactionInput = union([
object({
kind: literal('Input'),
index: pipe(number(), integer()),
value: unknown(),
type: optional(literal('object')),
}),
object({
kind: literal('Input'),
index: pipe(number(), integer()),
value: unknown(),
type: literal('pure'),
}),
]);
const TransactionExpiration = union([
object({ Epoch: pipe(number(), integer()) }),
object({ None: nullable(literal(true)) }),
]);
const StringEncodedBigint = pipe(
union([number(), string(), bigint()]),
check((val) => {
if (!['string', 'number', 'bigint'].includes(typeof val)) return false;
try {
BigInt(val as string);
return true;
} catch {
return false;
}
}),
);
export const TypeTag: GenericSchema<TypeTagType> = union([
object({ bool: nullable(literal(true)) }),
object({ u8: nullable(literal(true)) }),
object({ u64: nullable(literal(true)) }),
object({ u128: nullable(literal(true)) }),
object({ address: nullable(literal(true)) }),
object({ signer: nullable(literal(true)) }),
object({ vector: lazy(() => TypeTag) }),
object({ struct: lazy(() => StructTag) }),
object({ u16: nullable(literal(true)) }),
object({ u32: nullable(literal(true)) }),
object({ u256: nullable(literal(true)) }),
]);
// https://github.com/MystenLabs/sui/blob/cea8742e810142a8145fd83c4c142d61e561004a/external-crates/move/crates/move-core-types/src/language_storage.rs#L140-L147
export const StructTag: GenericSchema<StructTagType> = object({
address: string(),
module: string(),
name: string(),
typeParams: array(TypeTag),
});
const GasConfig = object({
budget: optional(StringEncodedBigint),
price: optional(StringEncodedBigint),
payment: optional(array(ObjectRef)),
owner: optional(string()),
});
const TransactionArgumentTypes = [
TransactionInput,
object({ kind: literal('GasCoin') }),
object({ kind: literal('Result'), index: pipe(number(), integer()) }),
object({
kind: literal('NestedResult'),
index: pipe(number(), integer()),
resultIndex: pipe(number(), integer()),
}),
] as const;
// Generic transaction argument
export const TransactionArgument = union([...TransactionArgumentTypes]);
const MoveCallTransaction = object({
kind: literal('MoveCall'),
target: pipe(
string(),
check((target) => target.split('::').length === 3),
) as GenericSchema<`${string}::${string}::${string}`>,
typeArguments: array(string()),
arguments: array(TransactionArgument),
});
const TransferObjectsTransaction = object({
kind: literal('TransferObjects'),
objects: array(TransactionArgument),
address: TransactionArgument,
});
const SplitCoinsTransaction = object({
kind: literal('SplitCoins'),
coin: TransactionArgument,
amounts: array(TransactionArgument),
});
const MergeCoinsTransaction = object({
kind: literal('MergeCoins'),
destination: TransactionArgument,
sources: array(TransactionArgument),
});
const MakeMoveVecTransaction = object({
kind: literal('MakeMoveVec'),
type: union([object({ Some: TypeTag }), object({ None: nullable(literal(true)) })]),
objects: array(TransactionArgument),
});
const PublishTransaction = object({
kind: literal('Publish'),
modules: array(array(pipe(number(), integer()))),
dependencies: array(string()),
});
const UpgradeTransaction = object({
kind: literal('Upgrade'),
modules: array(array(pipe(number(), integer()))),
dependencies: array(string()),
packageId: string(),
ticket: TransactionArgument,
});
const TransactionTypes = [
MoveCallTransaction,
TransferObjectsTransaction,
SplitCoinsTransaction,
MergeCoinsTransaction,
PublishTransaction,
UpgradeTransaction,
MakeMoveVecTransaction,
] as const;
const TransactionType = union([...TransactionTypes]);
export const SerializedTransactionDataV1 = object({
version: literal(1),
sender: optional(string()),
expiration: nullish(TransactionExpiration),
gasConfig: GasConfig,
inputs: array(TransactionInput),
transactions: array(TransactionType),
});
export type SerializedTransactionDataV1 = InferOutput<typeof SerializedTransactionDataV1>;
export function serializeV1TransactionData(
transactionData: TransactionData,
): SerializedTransactionDataV1 {
const inputs: InferOutput<typeof TransactionInput>[] = transactionData.inputs.map(
(input, index) => {
if (input.Object) {
return {
kind: 'Input',
index,
value: {
Object: input.Object.ImmOrOwnedObject
? {
ImmOrOwned: input.Object.ImmOrOwnedObject,
}
: input.Object.Receiving
? {
Receiving: {
digest: input.Object.Receiving.digest,
version: input.Object.Receiving.version,
objectId: input.Object.Receiving.objectId,
},
}
: {
Shared: {
mutable: input.Object.SharedObject.mutable,
initialSharedVersion: input.Object.SharedObject.initialSharedVersion,
objectId: input.Object.SharedObject.objectId,
},
},
},
type: 'object',
};
}
if (input.Pure) {
return {
kind: 'Input',
index,
value: {
Pure: Array.from(fromBase64(input.Pure.bytes)),
},
type: 'pure',
};
}
if (input.UnresolvedPure) {
return {
kind: 'Input',
type: 'pure',
index,
value: input.UnresolvedPure.value,
};
}
if (input.UnresolvedObject) {
return {
kind: 'Input',
type: 'object',
index,
value: input.UnresolvedObject.objectId,
};
}
throw new Error('Invalid input');
},
);
return {
version: 1,
sender: transactionData.sender ?? undefined,
expiration:
transactionData.expiration?.$kind === 'Epoch'
? { Epoch: Number(transactionData.expiration.Epoch) }
: transactionData.expiration
? { None: true }
: null,
gasConfig: {
owner: transactionData.gasData.owner ?? undefined,
budget: transactionData.gasData.budget ?? undefined,
price: transactionData.gasData.price ?? undefined,
payment: transactionData.gasData.payment ?? undefined,
},
inputs,
transactions: transactionData.commands.map((command): InferOutput<typeof TransactionType> => {
if (command.MakeMoveVec) {
return {
kind: 'MakeMoveVec',
type:
command.MakeMoveVec.type === null
? { None: true }
: { Some: TypeTagSerializer.parseFromStr(command.MakeMoveVec.type) },
objects: command.MakeMoveVec.elements.map((arg) =>
convertTransactionArgument(arg, inputs),
),
};
}
if (command.MergeCoins) {
return {
kind: 'MergeCoins',
destination: convertTransactionArgument(command.MergeCoins.destination, inputs),
sources: command.MergeCoins.sources.map((arg) => convertTransactionArgument(arg, inputs)),
};
}
if (command.MoveCall) {
return {
kind: 'MoveCall',
target: `${command.MoveCall.package}::${command.MoveCall.module}::${command.MoveCall.function}`,
typeArguments: command.MoveCall.typeArguments,
arguments: command.MoveCall.arguments.map((arg) =>
convertTransactionArgument(arg, inputs),
),
};
}
if (command.Publish) {
return {
kind: 'Publish',
modules: command.Publish.modules.map((mod) => Array.from(fromBase64(mod))),
dependencies: command.Publish.dependencies,
};
}
if (command.SplitCoins) {
return {
kind: 'SplitCoins',
coin: convertTransactionArgument(command.SplitCoins.coin, inputs),
amounts: command.SplitCoins.amounts.map((arg) => convertTransactionArgument(arg, inputs)),
};
}
if (command.TransferObjects) {
return {
kind: 'TransferObjects',
objects: command.TransferObjects.objects.map((arg) =>
convertTransactionArgument(arg, inputs),
),
address: convertTransactionArgument(command.TransferObjects.address, inputs),
};
}
if (command.Upgrade) {
return {
kind: 'Upgrade',
modules: command.Upgrade.modules.map((mod) => Array.from(fromBase64(mod))),
dependencies: command.Upgrade.dependencies,
packageId: command.Upgrade.package,
ticket: convertTransactionArgument(command.Upgrade.ticket, inputs),
};
}
throw new Error(`Unknown transaction ${Object.keys(command)}`);
}),
};
}
function convertTransactionArgument(
arg: Argument,
inputs: InferOutput<typeof TransactionInput>[],
): InferOutput<typeof TransactionArgument> {
if (arg.$kind === 'GasCoin') {
return { kind: 'GasCoin' };
}
if (arg.$kind === 'Result') {
return { kind: 'Result', index: arg.Result };
}
if (arg.$kind === 'NestedResult') {
return { kind: 'NestedResult', index: arg.NestedResult[0], resultIndex: arg.NestedResult[1] };
}
if (arg.$kind === 'Input') {
return inputs[arg.Input];
}
throw new Error(`Invalid argument ${Object.keys(arg)}`);
}
export function transactionDataFromV1(data: SerializedTransactionDataV1): TransactionData {
return parse(TransactionData, {
version: 2,
sender: data.sender ?? null,
expiration: data.expiration
? 'Epoch' in data.expiration
? { Epoch: data.expiration.Epoch }
: { None: true }
: null,
gasData: {
owner: data.gasConfig.owner ?? null,
budget: data.gasConfig.budget?.toString() ?? null,
price: data.gasConfig.price?.toString() ?? null,
payment:
data.gasConfig.payment?.map((ref) => ({
digest: ref.digest,
objectId: ref.objectId,
version: ref.version.toString(),
})) ?? null,
},
inputs: data.inputs.map((input) => {
if (input.kind === 'Input') {
if (is(NormalizedCallArg, input.value)) {
const value = parse(NormalizedCallArg, input.value);
if (value.Object) {
if (value.Object.ImmOrOwned) {
return {
Object: {
ImmOrOwnedObject: {
objectId: value.Object.ImmOrOwned.objectId,
version: String(value.Object.ImmOrOwned.version),
digest: value.Object.ImmOrOwned.digest,
},
},
};
}
if (value.Object.Shared) {
return {
Object: {
SharedObject: {
mutable: value.Object.Shared.mutable ?? null,
initialSharedVersion: value.Object.Shared.initialSharedVersion,
objectId: value.Object.Shared.objectId,
},
},
};
}
if (value.Object.Receiving) {
return {
Object: {
Receiving: {
digest: value.Object.Receiving.digest,
version: String(value.Object.Receiving.version),
objectId: value.Object.Receiving.objectId,
},
},
};
}
throw new Error('Invalid object input');
}
return {
Pure: {
bytes: toBase64(new Uint8Array(value.Pure)),
},
};
}
if (input.type === 'object') {
return {
UnresolvedObject: {
objectId: input.value as string,
},
};
}
return {
UnresolvedPure: {
value: input.value,
},
};
}
throw new Error('Invalid input');
}),
commands: data.transactions.map((transaction) => {
switch (transaction.kind) {
case 'MakeMoveVec':
return {
MakeMoveVec: {
type:
'Some' in transaction.type
? TypeTagSerializer.tagToString(transaction.type.Some)
: null,
elements: transaction.objects.map((arg) => parseV1TransactionArgument(arg)),
},
};
case 'MergeCoins': {
return {
MergeCoins: {
destination: parseV1TransactionArgument(transaction.destination),
sources: transaction.sources.map((arg) => parseV1TransactionArgument(arg)),
},
};
}
case 'MoveCall': {
const [pkg, mod, fn] = transaction.target.split('::');
return {
MoveCall: {
package: pkg,
module: mod,
function: fn,
typeArguments: transaction.typeArguments,
arguments: transaction.arguments.map((arg) => parseV1TransactionArgument(arg)),
},
};
}
case 'Publish': {
return {
Publish: {
modules: transaction.modules.map((mod) => toBase64(Uint8Array.from(mod))),
dependencies: transaction.dependencies,
},
};
}
case 'SplitCoins': {
return {
SplitCoins: {
coin: parseV1TransactionArgument(transaction.coin),
amounts: transaction.amounts.map((arg) => parseV1TransactionArgument(arg)),
},
};
}
case 'TransferObjects': {
return {
TransferObjects: {
objects: transaction.objects.map((arg) => parseV1TransactionArgument(arg)),
address: parseV1TransactionArgument(transaction.address),
},
};
}
case 'Upgrade': {
return {
Upgrade: {
modules: transaction.modules.map((mod) => toBase64(Uint8Array.from(mod))),
dependencies: transaction.dependencies,
package: transaction.packageId,
ticket: parseV1TransactionArgument(transaction.ticket),
},
};
}
}
throw new Error(`Unknown transaction ${Object.keys(transaction)}`);
}),
} satisfies InferInput<typeof TransactionData>);
}
function parseV1TransactionArgument(
arg: InferOutput<typeof TransactionArgument>,
): InferInput<typeof Argument> {
switch (arg.kind) {
case 'GasCoin': {
return { GasCoin: true };
}
case 'Result':
return { Result: arg.index };
case 'NestedResult': {
return { NestedResult: [arg.index, arg.resultIndex] };
}
case 'Input': {
return { Input: arg.index };
}
}
}