stellar-plus
Version:
beta version of stellar-plus, an all-in-one sdk for the Stellar blockchain
457 lines (455 loc) • 22.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContractEngine = void 0;
const tslib_1 = require("tslib");
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const buffer_1 = require("buffer");
const stellar_sdk_1 = require("@stellar/stellar-sdk");
const contract_1 = require("@stellar/stellar-sdk/contract");
const errors_1 = require("../../../stellar-plus/core/contract-engine/errors");
const types_1 = require("../../../stellar-plus/core/contract-engine/types");
const types_2 = require("../../../stellar-plus/core/pipelines/build-transaction/types");
const soroban_transaction_1 = require("../../../stellar-plus/core/pipelines/soroban-transaction");
const rpc_1 = require("../../../stellar-plus/rpc");
const functions_1 = require("../../../stellar-plus/utils/functions");
const inject_preprocess_parameter_1 = require("../../../stellar-plus/utils/pipeline/plugins/generic/inject-preprocess-parameter");
const extract_invocation_output_1 = require("../../../stellar-plus/utils/pipeline/plugins/simulate-transaction/extract-invocation-output");
const extract_contract_id_1 = require("../../../stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-contract-id");
const extract_invocation_output_2 = require("../../../stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-invocation-output");
const extract_wasm_hash_1 = require("../../../stellar-plus/utils/pipeline/plugins/soroban-get-transaction/extract-wasm-hash");
const process_spec_entry_stream_1 = require("../../../stellar-plus/utils/wasm/process-spec-entry-stream");
class ContractEngine {
/**
*
* @param {NetworkConfig} networkConfig - The network to use.
* @param contractParameters - The contract parameters.
* @param {Spec} contractParameters.spec - The contract specification object.
* @param {string=} contractParameters.contractId - The contract id.
* @param {Buffer=} contractParameters.wasm - The contract wasm file as a buffer.
* @param {string=} contractParameters.wasmHash - The contract wasm hash id.
* @param {Options=} options - A set of custom options to modify the behavior of the contract engine.
* @param {SorobanTransactionPipelineOptions=} options.sorobanTransactionPipeline - The Soroban transaction pipeline.
* @description - The contract engine is used for interacting with contracts on the network. This class can be extended to create a contract client, abstracting away the Soroban integration.
*
* @example - The following example shows how to invoke a contract method that alters the state of the contract.
* ```typescript
* const contract = new ContractEngine(network, spec, contractId)
*
* const output = await contract.invoke({
* method: 'add',
* args: {
* a: 1,
* b: 2,
* },
* signers: [accountHandler],
* })
*
* console.log(output) // 3
* ```
*
* @example - The following example shows how to invoke a contract method that does not alter the state of the contract.
* ```typescript
* const contract = new ContractEngine(network, spec, contractId)
* const output = await contract.read({
* method: 'get',
* args: {
* key: 'myKey',
* },
* })
* console.log(output) // 'myValue'
* ```
*/
constructor(args) {
var _a, _b;
const { networkConfig, contractParameters, options } = args;
this.networkConfig = networkConfig;
this.rpcHandler = ((_a = options === null || options === void 0 ? void 0 : options.sorobanTransactionPipeline) === null || _a === void 0 ? void 0 : _a.customRpcHandler)
? options.sorobanTransactionPipeline.customRpcHandler
: new rpc_1.DefaultRpcHandler(this.networkConfig);
this.spec = contractParameters.spec;
this.contractId = contractParameters.contractId;
this.wasm = contractParameters.wasm;
this.wasmHash = contractParameters.wasmHash;
this.options = Object.assign({}, options);
this.sorobanTransactionPipeline = new soroban_transaction_1.SorobanTransactionPipeline(networkConfig, Object.assign({ customRpcHandler: (_b = options === null || options === void 0 ? void 0 : options.sorobanTransactionPipeline) === null || _b === void 0 ? void 0 : _b.customRpcHandler }, this.options.sorobanTransactionPipeline));
}
getContractId() {
this.requireContractId();
return this.contractId;
}
getWasm() {
this.requireWasm();
return this.wasm;
}
getWasmHash() {
this.requireWasmHash();
return this.wasmHash;
}
getContractFootprint() {
this.requireContractId();
return new stellar_sdk_1.Contract(this.contractId).getFootprint();
}
getRpcHandler() {
return this.rpcHandler;
}
/**
*
* @param {void} args - No arguments.
*
* @returns {Promise<number>} The 'liveUntilLedgerSeq' value representing the ledger sequence number until which the contract instance is live.
*
* @description - Returns the ledger sequence number until which the contract instance is live. When the contract instance is live, it can be invoked. When the liveUntilLedgerSeq is reached, the contract instance is archived and can no longer be invoked until a restore is performed.
*/
getContractInstanceLiveUntilLedgerSeq() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const contractInstance = yield this.getContractInstanceLedgerEntry();
if (!contractInstance.liveUntilLedgerSeq) {
throw errors_1.CEError.contractInstanceMissingLiveUntilLedgerSeq();
}
return contractInstance.liveUntilLedgerSeq;
});
}
/**
*
* @param {void} args - No arguments.
*
* @returns {Promise<number>} The 'liveUntilLedgerSeq' value representing the ledger sequence number until which the contract code is live.
*
* @description - Returns the ledger sequence number until which the contract code is live. When the contract code is live, it can be deployed into new instances, generating a new unique contract id for each. When the liveUntilLedgerSeq is reached, the contract code is archived and can no longer be deployed until a restore is performed.
*
* */
getContractCodeLiveUntilLedgerSeq() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const contractCode = yield this.getContractCodeLedgerEntry();
if (!contractCode.liveUntilLedgerSeq) {
throw errors_1.CEError.contractCodeMissingLiveUntilLedgerSeq();
}
return contractCode.liveUntilLedgerSeq;
});
}
/**
*
* @args {SorobanSimulateArgs<object>} args - The arguments for the invocation.
* @param {string} args.method - The method to invoke as it is identified in the contract.
* @param {object} args.methodArgs - The arguments for the method invocation.
* @param {EnvelopeHeader} args.header - The header for the invocation.
*
* @returns {Promise<unknown>} The output of the invocation.
*
* @description - Simulate an invocation of a contract method that does not alter the state of the contract.
* This function does not require any signers. It builds a transaction, simulates it, and extracts the output of the invocation from the simulation.
*
* @example - The following example shows how to simulate a contract method invocation.
* ```typescript
* const contract = new ContractEngine(network, spec, contractId)
* const output = await contract.read({
* method: 'get',
* args: {
* key: 'myKey',
* },
* })
* console.log(output) // 'myValue'
* ```
*/
readFromContract(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const options = Object.assign(Object.assign({}, args.options), { simulateOnly: true });
return yield this.runTransactionPipeline(Object.assign(Object.assign({}, args), { options }));
});
}
/**
*
* @args {SorobanInvokeArgs<object>} args - The arguments for the invocation.
* @param {string} args.method - The method to invoke as it is identified in the contract.
* @param {object} args.methodArgs - The arguments for the method invocation.
* @param {EnvelopeHeader} args.header - The header for the invocation.
* @param {AccountHandler[]} args.signers - The signers for the invocation.
* @param {FeeBumpHeader=} args.feeBump - The fee bump header for the invocation.
*
* @returns {Promise<unknown>} The output of the invocation.
*
* @description - Invokes a contract method that alters the state of the contract.
* This function requires signers. It builds a transaction, simulates it, signs it, submits it to the network, and extracts the output of the invocation from the processed transaction.
*
* @example - The following example shows how to invoke a contract method that alters the state of the contract.
* ```typescript
* const contract = new ContractEngine(network, spec, contractId)
*
* const output = await contract.invoke({
* method: 'add',
* args: {
* a: 1,
* b: 2,
* },
* signers: [accountHandler],
* })
*
* console.log(output) // 3
* ```
*/
invokeContract(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const options = Object.assign(Object.assign({}, args.options), { simulateOnly: false });
return yield this.runTransactionPipeline(Object.assign(Object.assign({}, args), { options }));
});
}
runTransactionPipeline(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.requireContractId();
this.requireSpec();
const { method, methodArgs, options } = args;
const txInvocation = Object.assign({}, args);
const encodedArgs = this.spec.funcArgsToScVals(method, methodArgs); // Spec verified in requireSpec
const contract = new stellar_sdk_1.Contract(this.contractId); // Contract Id verified in requireContractId
const contractCallOperation = contract.call(method, ...encodedArgs);
const executionPlugins = [
...((options === null || options === void 0 ? void 0 : options.simulateOnly)
? [new extract_invocation_output_1.ExtractInvocationOutputFromSimulationPlugin(this.spec, method)]
: [new extract_invocation_output_2.ExtractInvocationOutputPlugin(this.spec, method)]),
...((options === null || options === void 0 ? void 0 : options.executionPlugins) || []),
];
const result = yield this.sorobanTransactionPipeline.execute({
txInvocation,
operations: [contractCallOperation],
options: Object.assign(Object.assign({}, options), { executionPlugins }),
});
return result;
});
}
//==========================================
// Meta Management Methods
//==========================================
//
//
/**
* @param {TransactionInvocation} txInvocation - The transaction invocation object to use in this transaction.
*
* @description - Uploads the contract wasm to the network and stores the wasm hash in the contract engine.
*
* @requires - The wasm file buffer to be set in the contract engine.
*
* */
uploadWasm(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a;
this.requireWasm();
try {
const uploadOperation = stellar_sdk_1.Operation.uploadContractWasm({ wasm: this.wasm }); // Wasm verified in requireWasm
const result = yield this.sorobanTransactionPipeline.execute({
txInvocation: args,
operations: [uploadOperation],
options: Object.assign(Object.assign({}, args.options), { executionPlugins: [new extract_wasm_hash_1.ExtractWasmHashPlugin()], verboseOutput: true }),
});
this.wasmHash = (_a = result.sorobanGetTransactionPipelineOutput.output) === null || _a === void 0 ? void 0 : _a.wasmHash;
return result;
}
catch (error) {
throw errors_1.CEError.failedToUploadWasm(error);
}
});
}
/**
* @param {TransactionInvocation} txInvocation - The transaction invocation object to use in this transaction.
*
* @description - Deploys a new instance of the contract to the network and stores the contract id in the contract engine.
*
* @requires - The wasm hash to be set in the contract engine.
*
* */
deploy(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a;
this.requireWasmHash();
try {
if (args.contractArgs)
this.requireSpec();
const encodedArgs = args.contractArgs
? this.spec.funcArgsToScVals('__constructor', args.contractArgs)
: undefined;
const deployOperation = stellar_sdk_1.Operation.createCustomContract({
address: new stellar_sdk_1.Address(args.header.source),
wasmHash: buffer_1.Buffer.from(this.wasmHash, 'hex'), // Wasm hash verified in requireWasmHash
salt: (0, functions_1.generateRandomSalt)(),
constructorArgs: encodedArgs,
});
const result = yield this.sorobanTransactionPipeline.execute({
txInvocation: args,
operations: [deployOperation],
options: Object.assign(Object.assign({}, args.options), { executionPlugins: [new extract_contract_id_1.ExtractContractIdPlugin()], verboseOutput: true }),
});
this.contractId = (_a = result.sorobanGetTransactionPipelineOutput.output) === null || _a === void 0 ? void 0 : _a.contractId;
return result;
}
catch (error) {
throw errors_1.CEError.failedToDeployContract(error);
}
});
}
wrapAndDeployClassicAsset(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
this.requireNoContractId();
try {
const txInvocation = args;
const wrapOperation = stellar_sdk_1.Operation.createStellarAssetContract({
asset: args.asset,
});
const result = yield this.sorobanTransactionPipeline.execute({
txInvocation,
operations: [wrapOperation],
options: Object.assign(Object.assign({}, args.options), { executionPlugins: [new extract_contract_id_1.ExtractContractIdPlugin()], verboseOutput: true }),
});
this.contractId = (_a = result.sorobanGetTransactionPipelineOutput.output) === null || _a === void 0 ? void 0 : _a.contractId;
return result;
}
catch (error) {
let isAssetAlreadyWrapped = false;
try {
const events = (_c = (_b = error.meta) === null || _b === void 0 ? void 0 : _b.sorobanSimulationData) === null || _c === void 0 ? void 0 : _c.events;
const dataVec = events ? events[0].event().body().v0().data().vec() : [];
if (dataVec && ((_d = dataVec[0].value()) === null || _d === void 0 ? void 0 : _d.toString()) === 'contract already exists') {
const contractId = stellar_sdk_1.Address.contract(dataVec[1].bytes()).toString();
this.contractId = contractId;
isAssetAlreadyWrapped = true;
}
}
finally {
if (!isAssetAlreadyWrapped) {
// eslint-disable-next-line no-unsafe-finally
throw errors_1.CEError.failedToWrapAsset(error);
}
}
}
});
}
restoreContractInstance(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return yield this.restore(Object.assign({ keys: [(yield this.getContractInstanceLedgerEntry()).key] }, args));
});
}
/**
*
* @param {TransactionInvocation} txInvocation - The transaction invocation object to use in this transaction.
*
* @returns {Promise<void>} - The output of the invocation.
*
* @description - Restores the contract code.
*/
restoreContractCode(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
return yield this.restore(Object.assign({ keys: [(yield this.getContractCodeLedgerEntry()).key] }, args));
});
}
/**
*
* @param {void} args - No arguments.
*
* @returns {Promise<void>} - The output of the invocation.
*
* @description - Loads the contract specification from the wasm file and stores it in the contract engine.
*/
loadSpecFromWasm() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.requireWasm();
const wasmModule = yield WebAssembly.compile(this.wasm);
const xdrSections = WebAssembly.Module.customSections(wasmModule, 'contractspecv0');
if (xdrSections.length === 0) {
throw errors_1.CEError.missingSpecInWasm();
}
const bufferSection = buffer_1.Buffer.from(xdrSections[0]);
const specEntryArray = (0, process_spec_entry_stream_1.processSpecEntryStream)(bufferSection);
const spec = new contract_1.Spec(specEntryArray);
this.spec = spec;
});
}
//==========================================
// Internal Methods
//==========================================
//
//
requireContractId() {
if (!this.contractId) {
throw errors_1.CEError.missingContractId();
}
}
requireNoContractId() {
if (this.contractId) {
throw errors_1.CEError.contractIdAlreadySet();
}
}
requireWasm() {
if (!this.wasm) {
throw errors_1.CEError.missingWasm();
}
}
requireWasmHash() {
if (!this.wasmHash) {
throw errors_1.CEError.missingWasmHash();
}
}
requireSpec() {
if (!this.spec) {
throw errors_1.CEError.missingSpec();
}
}
getContractCodeLedgerEntry() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.requireWasmHash();
const ledgerEntries = (yield this.getRpcHandler().getLedgerEntries(stellar_sdk_1.xdr.LedgerKey.contractCode(new stellar_sdk_1.xdr.LedgerKeyContractCode({ hash: buffer_1.Buffer.from(this.getWasmHash(), 'hex') }))));
const contractCode = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractCode');
if (!contractCode) {
throw errors_1.CEError.contractCodeNotFound(ledgerEntries);
}
return contractCode;
});
}
getContractInstanceLedgerEntry() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this.requireWasmHash();
const footprint = this.getContractFootprint();
const ledgerEntries = (yield this.getRpcHandler().getLedgerEntries(footprint));
const contractInstance = ledgerEntries.entries.find((entry) => entry.key.switch().name === 'contractData');
if (!contractInstance) {
throw errors_1.CEError.contractInstanceNotFound(ledgerEntries);
}
return contractInstance;
});
}
/**
* @args {RestoreFootprintArgs} args - The arguments for the invocation.
* @param {EnvelopeHeader} args.header - The header for the transaction.
* @param {AccountHandler[]} args.signers - The signers for the transaction.
* @param {FeeBumpHeader=} args.feeBump - The fee bump header for the transaction. This is optional.
*
* Option 1: Provide the keys directly.
* @param {xdr.LedgerKey[]} args.keys - The keys to restore.
* Option 2: Provide the restore preamble.
* @param { RestoreFootprintWithRestorePreamble} args.restorePreamble - The restore preamble.
* @param {string} args.restorePreamble.minResourceFee - The minimum resource fee.
* @param {SorobanDataBuilder} args.restorePreamble.transactionData - The transaction data.
*
* @returns {Promise<void>}
*
* @description - Execute a transaction to restore a given footprint.
*/
restore(args) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const txInvocation = args;
const sorobanData = (0, types_1.isRestoreFootprintWithLedgerKeys)(args)
? new stellar_sdk_1.SorobanDataBuilder().setReadWrite(args.keys).build()
: args.restorePreamble.transactionData.build();
const options = {};
const injectionParameter = { sorobanData: sorobanData };
const restoreFootprintOperation = stellar_sdk_1.Operation.restoreFootprint(options);
yield this.sorobanTransactionPipeline.execute({
txInvocation,
operations: [restoreFootprintOperation],
options: Object.assign(Object.assign({}, args.options), { executionPlugins: [
new inject_preprocess_parameter_1.InjectPreprocessParameterPlugin(injectionParameter, types_2.BuildTransactionPipelineType.id, 'preProcess'),
] }),
});
return;
});
}
}
exports.ContractEngine = ContractEngine;