PHP WebShell
Текущая директория: /usr/lib/node_modules/bitgo/node_modules/@iota/iota-sdk/src/transactions
Просмотр файла: Transaction.ts
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import type { SerializedBcs } from '@iota/bcs';
import { fromBase64, isSerializedBcs } from '@iota/bcs';
import type { InferInput } from 'valibot';
import { is, parse } from 'valibot';
import type { IotaClient } from '../client/index.js';
import type { SignatureWithBytes, Signer } from '../cryptography/index.js';
import { normalizeIotaAddress } from '../utils/iota-types.js';
import type { TransactionArgument } from './Commands.js';
import { Commands } from './Commands.js';
import type { CallArg, Command } from './data/internal.js';
import { Argument, NormalizedCallArg, ObjectRef, TransactionExpiration } from './data/internal.js';
import { serializeV1TransactionData } from './data/v1.js';
import { SerializedTransactionDataV2 } from './data/v2.js';
import { Inputs } from './Inputs.js';
import type {
BuildTransactionOptions,
SerializeTransactionOptions,
TransactionPlugin,
} from './json-rpc-resolver.js';
import { resolveTransactionData } from './json-rpc-resolver.js';
import { createObjectMethods } from './object.js';
import { createPure } from './pure.js';
import { TransactionDataBuilder } from './TransactionData.js';
import { getIdFromCallArg } from './utils.js';
export type TransactionObjectArgument =
| Exclude<InferInput<typeof Argument>, { Input: unknown; type?: 'pure' }>
| ((
tx: Transaction,
) => Exclude<InferInput<typeof Argument>, { Input: unknown; type?: 'pure' }>);
export type TransactionResult = Extract<Argument, { Result: unknown }> &
Extract<Argument, { NestedResult: unknown }>[];
function createTransactionResult(index: number) {
const baseResult = { $kind: 'Result' as const, Result: index };
const nestedResults: {
$kind: 'NestedResult';
NestedResult: [number, number];
}[] = [];
const nestedResultFor = (
resultIndex: number,
): {
$kind: 'NestedResult';
NestedResult: [number, number];
} =>
(nestedResults[resultIndex] ??= {
$kind: 'NestedResult' as const,
NestedResult: [index, resultIndex],
});
return new Proxy(baseResult, {
set() {
throw new Error(
'The transaction result is a proxy, and does not support setting properties directly',
);
},
// TODO: Instead of making this return a concrete argument, we should ideally
// make it reference-based (so that this gets resolved at build-time), which
// allows re-ordering transactions.
get(target, property) {
// This allows this transaction argument to be used in the singular form:
if (property in target) {
return Reflect.get(target, property);
}
// Support destructuring:
if (property === Symbol.iterator) {
return function* () {
let i = 0;
while (true) {
yield nestedResultFor(i);
i++;
}
};
}
if (typeof property === 'symbol') return;
const resultIndex = parseInt(property, 10);
if (Number.isNaN(resultIndex) || resultIndex < 0) return;
return nestedResultFor(resultIndex);
},
}) as TransactionResult;
}
const TRANSACTION_BRAND = Symbol.for('@iota/transaction') as never;
interface SignOptions extends BuildTransactionOptions {
signer: Signer;
}
export function isTransaction(obj: unknown): obj is Transaction {
return !!obj && typeof obj === 'object' && (obj as any)[TRANSACTION_BRAND] === true;
}
export type TransactionObjectInput = string | CallArg | TransactionObjectArgument;
interface TransactionPluginRegistry {
// eslint-disable-next-line @typescript-eslint/ban-types
buildPlugins: Map<string | Function, TransactionPlugin>;
// eslint-disable-next-line @typescript-eslint/ban-types
serializationPlugins: Map<string | Function, TransactionPlugin>;
}
const modulePluginRegistry: TransactionPluginRegistry = {
buildPlugins: new Map(),
serializationPlugins: new Map(),
};
const TRANSACTION_REGISTRY_KEY = Symbol.for('@iota/transaction/registry');
function getGlobalPluginRegistry() {
try {
const target = globalThis as {
[TRANSACTION_REGISTRY_KEY]?: TransactionPluginRegistry;
};
if (!target[TRANSACTION_REGISTRY_KEY]) {
target[TRANSACTION_REGISTRY_KEY] = modulePluginRegistry;
}
return target[TRANSACTION_REGISTRY_KEY];
} catch (e) {
return modulePluginRegistry;
}
}
/**
* Transaction Builder
*/
export class Transaction {
#serializationPlugins: TransactionPlugin[];
#buildPlugins: TransactionPlugin[];
#intentResolvers = new Map<string, TransactionPlugin>();
/**
* Converts from a serialize transaction kind (built with `build({ onlyTransactionKind: true })`) to a `Transaction` class.
* Supports either a byte array, or base64-encoded bytes.
*/
static fromKind(serialized: string | Uint8Array) {
const tx = new Transaction();
tx.#data = TransactionDataBuilder.fromKindBytes(
typeof serialized === 'string' ? fromBase64(serialized) : serialized,
);
return tx;
}
/**
* Converts from a serialized transaction format to a `Transaction` class.
* There are two supported serialized formats:
* - A string returned from `Transaction#serialize`. The serialized format must be compatible, or it will throw an error.
* - A byte array (or base64-encoded bytes) containing BCS transaction data.
*/
static from(transaction: string | Uint8Array | Transaction) {
const newTransaction = new Transaction();
if (isTransaction(transaction)) {
newTransaction.#data = new TransactionDataBuilder(transaction.getData());
} else if (typeof transaction !== 'string' || !transaction.startsWith('{')) {
newTransaction.#data = TransactionDataBuilder.fromBytes(
typeof transaction === 'string' ? fromBase64(transaction) : transaction,
);
} else {
newTransaction.#data = TransactionDataBuilder.restore(JSON.parse(transaction));
}
return newTransaction;
}
/** @deprecated global plugins should be registered with a name */
static registerGlobalSerializationPlugin(step: TransactionPlugin): void;
static registerGlobalSerializationPlugin(name: string, step: TransactionPlugin): void;
static registerGlobalSerializationPlugin(
stepOrStep: TransactionPlugin | string,
step?: TransactionPlugin,
) {
getGlobalPluginRegistry().serializationPlugins.set(
stepOrStep,
step ?? (stepOrStep as TransactionPlugin),
);
}
static unregisterGlobalSerializationPlugin(name: string) {
getGlobalPluginRegistry().serializationPlugins.delete(name);
}
/** @deprecated global plugins should be registered with a name */
static registerGlobalBuildPlugin(step: TransactionPlugin): void;
static registerGlobalBuildPlugin(name: string, step: TransactionPlugin): void;
static registerGlobalBuildPlugin(
stepOrStep: TransactionPlugin | string,
step?: TransactionPlugin,
) {
getGlobalPluginRegistry().buildPlugins.set(
stepOrStep,
step ?? (stepOrStep as TransactionPlugin),
);
}
static unregisterGlobalBuildPlugin(name: string) {
getGlobalPluginRegistry().buildPlugins.delete(name);
}
addSerializationPlugin(step: TransactionPlugin) {
this.#serializationPlugins.push(step);
}
addBuildPlugin(step: TransactionPlugin) {
this.#buildPlugins.push(step);
}
addIntentResolver(intent: string, resolver: TransactionPlugin) {
if (this.#intentResolvers.has(intent) && this.#intentResolvers.get(intent) !== resolver) {
throw new Error(`Intent resolver for ${intent} already exists`);
}
this.#intentResolvers.set(intent, resolver);
}
setSender(sender: string) {
this.#data.sender = sender;
}
/**
* Sets the sender only if it has not already been set.
* This is useful for sponsored transaction flows where the sender may not be the same as the signer address.
*/
setSenderIfNotSet(sender: string) {
if (!this.#data.sender) {
this.#data.sender = sender;
}
}
setExpiration(expiration?: InferInput<typeof TransactionExpiration> | null) {
this.#data.expiration = expiration ? parse(TransactionExpiration, expiration) : null;
}
setGasPrice(price: number | bigint) {
this.#data.gasConfig.price = String(price);
}
setGasBudget(budget: number | bigint) {
this.#data.gasConfig.budget = String(budget);
}
setGasBudgetIfNotSet(budget: number | bigint) {
if (this.#data.gasData.budget == null) {
this.#data.gasConfig.budget = String(budget);
}
}
setGasOwner(owner: string) {
this.#data.gasConfig.owner = owner;
}
setGasPayment(payments: ObjectRef[]) {
this.#data.gasConfig.payment = payments.map((payment) => parse(ObjectRef, payment));
}
#data: TransactionDataBuilder;
/** @deprecated Use `getData()` instead. */
get blockData() {
return serializeV1TransactionData(this.#data.snapshot());
}
/** Get a snapshot of the transaction data, in JSON form: */
getData() {
return this.#data.snapshot();
}
// Used to brand transaction classes so that they can be identified, even between multiple copies
// of the builder.
get [TRANSACTION_BRAND]() {
return true;
}
// Temporary workaround for the wallet interface accidentally serializing transactions via postMessage
get pure(): ReturnType<typeof createPure<Argument>> {
Object.defineProperty(this, 'pure', {
enumerable: false,
value: createPure<Argument>((value): Argument => {
if (isSerializedBcs(value)) {
return this.#data.addInput('pure', {
$kind: 'Pure',
Pure: {
bytes: value.toBase64(),
},
});
}
// TODO: we can also do some deduplication here
return this.#data.addInput(
'pure',
is(NormalizedCallArg, value)
? parse(NormalizedCallArg, value)
: value instanceof Uint8Array
? Inputs.Pure(value)
: { $kind: 'UnresolvedPure', UnresolvedPure: { value } },
);
}),
});
return this.pure;
}
constructor() {
const globalPlugins = getGlobalPluginRegistry();
this.#data = new TransactionDataBuilder();
this.#buildPlugins = [...globalPlugins.buildPlugins.values()];
this.#serializationPlugins = [...globalPlugins.serializationPlugins.values()];
}
/** Returns an argument for the gas coin, to be used in a transaction. */
get gas() {
return { $kind: 'GasCoin' as const, GasCoin: true as const };
}
/**
* Add a new object input to the transaction.
*/
object = createObjectMethods(
(value: TransactionObjectInput): { $kind: 'Input'; Input: number; type?: 'object' } => {
if (typeof value === 'function') {
return this.object(value(this));
}
if (typeof value === 'object' && is(Argument, value)) {
return value as { $kind: 'Input'; Input: number; type?: 'object' };
}
const id = getIdFromCallArg(value);
const inserted = this.#data.inputs.find((i) => id === getIdFromCallArg(i));
// Upgrade shared object inputs to mutable if needed:
if (
inserted?.Object?.SharedObject &&
typeof value === 'object' &&
value.Object?.SharedObject
) {
inserted.Object.SharedObject.mutable =
inserted.Object.SharedObject.mutable || value.Object.SharedObject.mutable;
}
return inserted
? { $kind: 'Input', Input: this.#data.inputs.indexOf(inserted), type: 'object' }
: this.#data.addInput(
'object',
typeof value === 'string'
? {
$kind: 'UnresolvedObject',
UnresolvedObject: { objectId: normalizeIotaAddress(value) },
}
: value,
);
},
);
/**
* Add a new object input to the transaction using the fully-resolved object reference.
* If you only have an object ID, use `builder.object(id)` instead.
*/
objectRef(...args: Parameters<(typeof Inputs)['ObjectRef']>) {
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.
*/
sharedObjectRef(...args: Parameters<(typeof Inputs)['SharedObjectRef']>) {
return this.object(Inputs.SharedObjectRef(...args));
}
/** Add a transaction to the transaction */
add<T = TransactionResult>(command: Command | ((tx: Transaction) => T)): T {
if (typeof command === 'function') {
return command(this);
}
const index = this.#data.commands.push(command);
return createTransactionResult(index - 1) as T;
}
#normalizeTransactionArgument(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arg: TransactionArgument | SerializedBcs<any>,
) {
if (isSerializedBcs(arg)) {
return this.pure(arg);
}
return this.#resolveArgument(arg as TransactionArgument);
}
#resolveArgument(arg: TransactionArgument): Argument {
if (typeof arg === 'function') {
return parse(Argument, arg(this));
}
return parse(Argument, arg);
}
// Method shorthands:
splitCoins(
coin: TransactionObjectArgument | string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
amounts: (TransactionArgument | SerializedBcs<any> | number | string | bigint)[],
) {
return this.add(
Commands.SplitCoins(
typeof coin === 'string' ? this.object(coin) : this.#resolveArgument(coin),
amounts.map((amount) =>
typeof amount === 'number' ||
typeof amount === 'bigint' ||
typeof amount === 'string'
? this.pure.u64(amount)
: this.#normalizeTransactionArgument(amount),
),
),
);
}
mergeCoins(
destination: TransactionObjectArgument | string,
sources: (TransactionObjectArgument | string)[],
) {
return this.add(
Commands.MergeCoins(
this.object(destination),
sources.map((src) => this.object(src)),
),
);
}
publish({ modules, dependencies }: { modules: number[][] | string[]; dependencies: string[] }) {
return this.add(
Commands.Publish({
modules,
dependencies,
}),
);
}
upgrade({
modules,
dependencies,
package: packageId,
ticket,
}: {
modules: number[][] | string[];
dependencies: string[];
package: string;
ticket: TransactionObjectArgument | string;
}) {
return this.add(
Commands.Upgrade({
modules,
dependencies,
package: packageId,
ticket: this.object(ticket),
}),
);
}
moveCall({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arguments: args,
...input
}:
| {
package: string;
module: string;
function: string;
arguments?: (TransactionArgument | SerializedBcs<any>)[];
typeArguments?: string[];
}
| {
target: string;
arguments?: (TransactionArgument | SerializedBcs<any>)[];
typeArguments?: string[];
}) {
return this.add(
Commands.MoveCall({
...input,
arguments: args?.map((arg) => this.#normalizeTransactionArgument(arg)),
} as Parameters<typeof Commands.MoveCall>[0]),
);
}
transferObjects(
objects: (TransactionObjectArgument | string)[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
address: TransactionArgument | SerializedBcs<any> | string,
) {
return this.add(
Commands.TransferObjects(
objects.map((obj) => this.object(obj)),
typeof address === 'string'
? this.pure.address(address)
: this.#normalizeTransactionArgument(address),
),
);
}
makeMoveVec({
type,
elements,
}: {
elements: (TransactionObjectArgument | string)[];
type?: string;
}) {
return this.add(
Commands.MakeMoveVec({
type,
elements: elements.map((obj) => this.object(obj)),
}),
);
}
/**
* @deprecated Use toJSON instead.
* For synchronous serialization, you can use `getData()`
* */
serialize() {
return JSON.stringify(serializeV1TransactionData(this.#data.snapshot()));
}
async toJSON(options: SerializeTransactionOptions = {}): Promise<string> {
await this.prepareForSerialization(options);
return JSON.stringify(
parse(SerializedTransactionDataV2, this.#data.snapshot()),
(_key, value) => (typeof value === 'bigint' ? value.toString() : value),
2,
);
}
/** Build the transaction to BCS bytes, and sign it with the provided keypair. */
async sign(options: SignOptions): Promise<SignatureWithBytes> {
const { signer, ...buildOptions } = options;
const bytes = await this.build(buildOptions);
return signer.signTransaction(bytes);
}
/** Build the transaction to BCS bytes. */
async build(options: BuildTransactionOptions = {}): Promise<Uint8Array> {
await this.prepareForSerialization(options);
await this.#prepareBuild(options);
return this.#data.build({
maxSizeBytes: options.maxSizeBytes,
onlyTransactionKind: options.onlyTransactionKind,
});
}
/** Derive transaction digest */
async getDigest(
options: {
client?: IotaClient;
} = {},
): Promise<string> {
await this.#prepareBuild(options);
return this.#data.getDigest();
}
/**
* Prepare the transaction by validating the transaction data and resolving all inputs
* so that it can be built into bytes.
*/
async #prepareBuild(options: BuildTransactionOptions) {
if (!options.onlyTransactionKind && !this.#data.sender) {
throw new Error('Missing transaction sender');
}
await this.#runPlugins([...this.#buildPlugins, resolveTransactionData], options);
}
async #runPlugins(plugins: TransactionPlugin[], options: SerializeTransactionOptions) {
const createNext = (i: number) => {
if (i >= plugins.length) {
return () => {};
}
const plugin = plugins[i];
return async () => {
const next = createNext(i + 1);
let calledNext = false;
let nextResolved = false;
await plugin(this.#data, options, async () => {
if (calledNext) {
throw new Error(`next() was call multiple times in TransactionPlugin ${i}`);
}
calledNext = true;
await next();
nextResolved = true;
});
if (!calledNext) {
throw new Error(`next() was not called in TransactionPlugin ${i}`);
}
if (!nextResolved) {
throw new Error(`next() was not awaited in TransactionPlugin ${i}`);
}
};
};
await createNext(0)();
}
async prepareForSerialization(options: SerializeTransactionOptions) {
const intents = new Set<string>();
for (const command of this.#data.commands) {
if (command.$Intent) {
intents.add(command.$Intent.name);
}
}
const steps = [...this.#serializationPlugins];
for (const intent of intents) {
if (options.supportedIntents?.includes(intent)) {
continue;
}
if (!this.#intentResolvers.has(intent)) {
throw new Error(`Missing intent resolver for ${intent}`);
}
steps.push(this.#intentResolvers.get(intent)!);
}
await this.#runPlugins(steps, options);
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!