Skip to content

Commit

Permalink
feat: add new static method to deploy and construct contracts
Browse files Browse the repository at this point in the history
This will allow using a wasm hash and constructor args to create an assembled transaction that will deploy and initialize a contract. Generated types in the CLI are to follow.
  • Loading branch information
willemneal authored and chadoh committed Nov 8, 2024
1 parent 537a187 commit 6c7d3d3
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 20 deletions.
25 changes: 17 additions & 8 deletions src/contract/assembled_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,22 +442,31 @@ export class AssembledTransaction<T> {
* })
*/
static async build<T>(
options: AssembledTransactionOptions<T>,
options: AssembledTransactionOptions<T>
): Promise<AssembledTransaction<T>> {
const tx = new AssembledTransaction(options);
const contract = new Contract(options.contractId);

const account = await getAccount(
options,
tx.server
return AssembledTransaction.buildWithOp(
contract.call(options.method, ...(options.args ?? [])),
options
);
}

//TODO: Docs
static async buildWithOp<T>(
// TODO: Docs either CreateContractArgsV2
// or CreateContractArgs
// or InvokeContractArgs
operation: xdr.Operation,
options: AssembledTransactionOptions<T>
): Promise<AssembledTransaction<T>> {
const tx = new AssembledTransaction(options);
const account = await getAccount(options, tx.server);
tx.raw = new TransactionBuilder(account, {
fee: options.fee ?? BASE_FEE,
networkPassphrase: options.networkPassphrase,
})
.addOperation(contract.call(options.method, ...(options.args ?? [])))
.setTimeout(options.timeoutInSeconds ?? DEFAULT_TIMEOUT);
.setTimeout(options.timeoutInSeconds ?? DEFAULT_TIMEOUT)
.addOperation(operation);

if (options.simulate) await tx.simulate();

Expand Down
113 changes: 102 additions & 11 deletions src/contract/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { xdr } from "@stellar/stellar-base";
import {
Operation,
Transaction,
TransactionBuilder,
xdr,
Account,
hash,
Address,
} from "@stellar/stellar-base";
import { Spec } from "./spec";
import { Server } from '../rpc';
import { AssembledTransaction } from "./assembled_transaction";
import type { ClientOptions, MethodOptions } from "./types";
import { processSpecEntryStream } from './utils';
import { DEFAULT_TIMEOUT } from "./types";

const CONSTRUCTOR_FUNC = "__constructor";


/**
* Generate a class from the contract spec that where each contract method
Expand All @@ -20,15 +32,73 @@ import { processSpecEntryStream } from './utils';
* @param {ClientOptions} options see {@link ClientOptions}
*/
export class Client {
static async deploy<T = Client>(
args: Record<string, any> | null,
options: MethodOptions &
Omit<ClientOptions, "contractId"> & {
wasmHash: Buffer | string;
salt: Buffer;
format?: "hex" | "base64";
}
): Promise<AssembledTransaction<T>> {
let spec = await specFromWasmHash(
options.wasmHash,
options,
options.format
);
const wasmHashBuffer =
typeof options.wasmHash === "string"
? Buffer.from(options.wasmHash, options.format)
: (options.wasmHash as Buffer);
let constructorArgs: xdr.ScVal[] = args
? spec.funcArgsToScVals(CONSTRUCTOR_FUNC, args)
: [];
let address = new Address(options.publicKey!);
let contractIdPreimage =
xdr.ContractIdPreimage.contractIdPreimageFromAddress(
new xdr.ContractIdPreimageFromAddress({
salt: options.salt,
address: address.toScAddress(),
})
);
let contractId = Address.contract(
hash(contractIdPreimage.toXDR())
).toString();
console.log("contractId", contractId);
let func = xdr.HostFunction.hostFunctionTypeCreateContractV2(
new xdr.CreateContractArgsV2({
constructorArgs,
contractIdPreimage,
executable:
xdr.ContractExecutable.contractExecutableWasm(wasmHashBuffer),
})
);
let operation = Operation.invokeHostFunction({
func,
source: address.toString(),
});

return AssembledTransaction.buildWithOp(operation, {
...options,
contractId,
method: CONSTRUCTOR_FUNC,
parseResultXdr: (result: xdr.ScVal) =>
new Client(spec, { ...options, contractId }),
}) as unknown as AssembledTransaction<T>;
}

constructor(
public readonly spec: Spec,
public readonly options: ClientOptions,
public readonly options: ClientOptions
) {
this.spec.funcs().forEach((xdrFn) => {
const method = xdrFn.name().toString();
if (method === CONSTRUCTOR_FUNC) {
return;
}
const assembleTransaction = (
args?: Record<string, any>,
methodOptions?: MethodOptions,
methodOptions?: MethodOptions
) =>
AssembledTransaction.build({
method,
Expand Down Expand Up @@ -87,14 +157,7 @@ export class Client {
* @throws {Error} If the contract spec cannot be obtained from the provided wasm binary.
*/
static async fromWasm(wasm: Buffer, options: ClientOptions): Promise<Client> {
const wasmModule = await WebAssembly.compile(wasm);
const xdrSections = WebAssembly.Module.customSections(wasmModule, "contractspecv0");
if (xdrSections.length === 0) {
throw new Error('Could not obtain contract spec from wasm');
}
const bufferSection = Buffer.from(xdrSections[0]);
const specEntryArray = processSpecEntryStream(bufferSection);
const spec = new Spec(specEntryArray);
const spec = await specFromWasm(wasm);
return new Client(spec, options);
}

Expand Down Expand Up @@ -132,4 +195,32 @@ export class Client {
txFromXDR = <T>(xdrBase64: string): AssembledTransaction<T> => AssembledTransaction.fromXDR(this.options, xdrBase64, this.spec);

}
async function specFromWasm(wasm: Buffer) {
const wasmModule = await WebAssembly.compile(wasm);
const xdrSections = WebAssembly.Module.customSections(
wasmModule,
"contractspecv0"
);
if (xdrSections.length === 0) {
throw new Error("Could not obtain contract spec from wasm");
}
const bufferSection = Buffer.from(xdrSections[0]);
const specEntryArray = processSpecEntryStream(bufferSection);
const spec = new Spec(specEntryArray);
return spec;
}

async function specFromWasmHash(
wasmHash: Buffer | string,
options: Server.Options & { rpcUrl: string },
format: "hex" | "base64" = "hex"
): Promise<Spec> {
if (!options || !options.rpcUrl) {
throw new TypeError("options must contain rpcUrl");
}
const { rpcUrl, allowHttp } = options;
const serverOpts: Server.Options = { allowHttp };
const server = new Server(rpcUrl, serverOpts);
const wasm = await server.getContractWasmByHash(wasmHash, format);
return specFromWasm(wasm);
}
26 changes: 25 additions & 1 deletion test/unit/spec/contract_spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { xdr, Address, contract, Keypair } from "../../../lib";
import { xdr, Address, contract, Keypair, hash } from "../../../lib";
import { JSONSchemaFaker } from "json-schema-faker";

import spec from "../spec.json";
import spec_contstructor from "../spec_constructor.json";
import { expect } from "chai";

const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW";
const addr = Address.fromString(publicKey);
let SPEC: contract.Spec;
let SPEC_CONSTRUCTOR: contract.Spec;

JSONSchemaFaker.format("address", () => {
let keypair = Keypair.random();
Expand All @@ -15,6 +17,7 @@ JSONSchemaFaker.format("address", () => {

before(() => {
SPEC = new contract.Spec(spec);
SPEC_CONSTRUCTOR = new contract.Spec(spec_contstructor);
});

it("throws if no entries", () => {
Expand Down Expand Up @@ -267,6 +270,27 @@ describe("parsing and building ScVals", function () {
});
});

describe("Constructor", function () {
it("Can round trip constructor", async function () {
let names = SPEC_CONSTRUCTOR.funcs().map((f) => f.name().toString());
console.log(names);
let keypair = Keypair.random();
const networkPassphrase = "Standalone Network ; February 2017";
const rpcUrl = process.env.SOROBAN_RPC_URL ?? "http://localhost:8000/soroban/rpc";
let deployAndConstructTx = await contract.Client.deploy(
{ counter: 42 },
{
networkPassphrase,
rpcUrl,
wasmHash: "",
salt: hash(Buffer.from("salt")),
publicKey: keypair.publicKey(),
},
);
console.log(deployAndConstructTx.toXDR());
});
});

export const GIGA_MAP = xdr.ScSpecEntry.scSpecEntryUdtStructV0(
new xdr.ScSpecUdtStructV0({
doc: "This is a kitchen sink of all the types",
Expand Down
4 changes: 4 additions & 0 deletions test/unit/spec_constructor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
"AAAAAAAAABNFeGFtcGxlIGNvbnN0cnVjdG9yAAAAAA1fX2NvbnN0cnVjdG9yAAAAAAAAAQAAAAAAAAAHY291bnRlcgAAAAAEAAAAAA==",
"AAAAAAAAAA1Db3VudGVyIHZhbHVlAAAAAAAAB2NvdW50ZXIAAAAAAAAAAAEAAAAE"
]

0 comments on commit 6c7d3d3

Please sign in to comment.