Skip to content

Commit

Permalink
add getContractWasmByHash and analogous method in client
Browse files Browse the repository at this point in the history
  • Loading branch information
BlaineHeffron committed May 9, 2024
1 parent 8866018 commit 752f177
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 14 deletions.
18 changes: 16 additions & 2 deletions src/contract_client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,24 @@ export class ContractClient {
});
}

/**
* Generate a ContractClient instance from the ContractClientOptions and the wasm hash
*/
static async fromWasmHash(wasmHash: Buffer, options: ContractClientOptions): Promise<ContractClient> {
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);
return ContractClient.fromWasm(wasm, options);
}

/**
* Generate a ContractClient instance from the ContractClientOptions and the wasm binary
*/
static async fromWasm(wasm: BufferSource, options: ContractClientOptions): Promise<ContractClient> {
static async fromWasm(wasm: Buffer, options: ContractClientOptions): Promise<ContractClient> {
const wasmModule = await WebAssembly.compile(wasm);
const xdrSections = WebAssembly.Module.customSections(wasmModule, "contractspecv0");
if (xdrSections.length === 0) {
Expand All @@ -72,7 +86,7 @@ export class ContractClient {
const { rpcUrl, contractId, allowHttp } = options;
const serverOpts: Server.Options = { allowHttp };
const server = new Server(rpcUrl, serverOpts);
const wasm = await server.getContractWasm(contractId);
const wasm = await server.getContractWasmByContractId(contractId);
return ContractClient.fromWasm(wasm, options);
}

Expand Down
35 changes: 33 additions & 2 deletions src/soroban/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,18 +248,19 @@ export class Server {
* @returns {Promise<Buffer>} a Buffer containing the WASM bytecode
*
* @throws {Error} If the contract or its associated WASM bytecode cannot be
* :
* found on the network.
*
* @example
* const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5";
* server.getContractWasm(contractId).then(wasmBuffer => {
* server.getContractWasmByContractId(contractId).then(wasmBuffer => {
* console.log("WASM bytecode length:", wasmBuffer.length);
* // ... do something with the WASM bytecode ...
* }).catch(err => {
* console.error("Error fetching WASM bytecode:", err);
* });
*/
public async getContractWasm(
public async getContractWasmByContractId(
contractId: string
): Promise<Buffer> {
const contractLedgerKey = new Contract(contractId).getFootprint();
Expand All @@ -275,6 +276,35 @@ export class Server {
.executable()
.wasmHash();

return this.getContractWasmByHash(wasmHash);
}

/**
* Retrieves the WASM bytecode for a given contract hash.
*
* This method allows you to fetch the WASM bytecode associated with a contract
* deployed on the Soroban network using the contract's WASM hash. The WASM bytecode
* represents the executable code of the contract.
*
* @param {Buffer} wasmHash the WASM hash of the contract
*
* @returns {Promise<Buffer>} a Buffer containing the WASM bytecode
*
* @throws {Error} If the contract or its associated WASM bytecode cannot be
* found on the network.
*
* @example
* const wasmHash = Buffer.from("...");
* server.getContractWasmByHash(wasmHash).then(wasmBuffer => {
* console.log("WASM bytecode length:", wasmBuffer.length);
* // ... do something with the WASM bytecode ...
* }).catch(err => {
* console.error("Error fetching WASM bytecode:", err);
* });
*/
public async getContractWasmByHash(
wasmHash: Buffer
): Promise<Buffer> {
const ledgerKeyWasmHash = xdr.LedgerKey.contractCode(
new xdr.LedgerKeyContractCode({
hash: wasmHash
Expand All @@ -291,6 +321,7 @@ export class Server {
}



/**
* Reads the current value of arbitrary ledger entries directly.
*
Expand Down
8 changes: 7 additions & 1 deletion test/e2e/src/test-custom-types.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const test = require('ava')
const { Address } = require('../../..')
const { Ok, Err } = require('../../../lib/rust_types')
const { clientFor, clientForFromTest } = require('./util')
const { clientFor, clientForFromTest, clientFromConstructor } = require('./util')

test.before(async t => {
const { client, keypair, contractId } = await clientFor('customTypes')
Expand All @@ -15,6 +15,12 @@ test('hello', async t => {
t.is(result, 'tests')
})

test('hello from constructor', async t => {
const { client } = await clientFromConstructor('customTypes')
const { result } = await client.hello({ hello: 'tests' })
t.is(result, 'tests')
})

test("view method with empty keypair", async (t) => {
const { client: client2 } = await clientFor('customTypes', {
keypair: undefined,
Expand Down
55 changes: 49 additions & 6 deletions test/e2e/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ const {
const contracts = {
customTypes: {
hash: spawnSync("./target/bin/soroban", ["contract", "install", "--wasm", `${__dirname}/../wasms/test_custom_types.wasm`], { shell: true, encoding: "utf8" }).stdout.trim(),
xdr: JSON.parse(spawnSync("./target/bin/soroban", ["contract", "inspect", "--wasm", `${__dirname}/../wasms/test_custom_types.wasm`, "--output", "xdr-base64-array"], { shell: true, encoding: "utf8" }).stdout.trim()),
path: `${__dirname}/../wasms/test_custom_types.wasm`,
},
helloWorld: {
hash: spawnSync("./target/bin/soroban", ["contract", "install", "--wasm", `${__dirname}/../wasms/test_hello_world.wasm`], { shell: true, encoding: "utf8" }).stdout.trim(),
xdr: JSON.parse(spawnSync("./target/bin/soroban", ["contract", "inspect", "--wasm", `${__dirname}/../wasms/test_hello_world.wasm`, "--output", "xdr-base64-array"], { shell: true, encoding: "utf8" }).stdout.trim()),
path: `${__dirname}/../wasms/test_hello_world.wasm`
},
swap: {
hash: spawnSync("./target/bin/soroban", ["contract", "install", "--wasm", `${__dirname}/../wasms/test_swap.wasm`], { shell: true, encoding: "utf8" }).stdout.trim(),
xdr: JSON.parse(spawnSync("./target/bin/soroban", ["contract", "inspect", "--wasm", `${__dirname}/../wasms/test_swap.wasm`, "--output", "xdr-base64-array"], { shell: true, encoding: "utf8" }).stdout.trim()),
path: `${__dirname}/../wasms/test_swap.wasm`
},
token: {
hash: spawnSync("./target/bin/soroban", ["contract", "install", "--wasm", `${__dirname}/../wasms/test_token.wasm`], { shell: true, encoding: "utf8" }).stdout.trim(),
xdr: JSON.parse(spawnSync("./target/bin/soroban", ["contract", "inspect", "--wasm", `${__dirname}/../wasms/test_token.wasm`, "--output", "xdr-base64-array"], { shell: true, encoding: "utf8" }).stdout.trim()),
path: `${__dirname}/../wasms/test_token.wasm`
},
};
module.exports.contracts = contracts
Expand Down Expand Up @@ -59,8 +59,51 @@ async function clientFor(contract, { keypair = generateFundedKeypair(), contract
keypair = await keypair // eslint-disable-line no-param-reassign
const wallet = basicNodeSigner(keypair, networkPassphrase)

const spec = new ContractSpec(contracts[contract].xdr)
const wasmHash = contracts[contract].hash;
if (!wasmHash) {
throw new Error(`No wasm hash found for \`contracts[${contract}]\`! ${JSON.stringify(contracts[contract], null, 2)}`)
}

// TODO: do this with js-stellar-sdk, instead of shelling out to the CLI
contractId = contractId ?? spawnSync("./target/bin/soroban", [ // eslint-disable-line no-param-reassign
"contract",
"deploy",
"--source",
keypair.secret(),
"--wasm-hash",
wasmHash,
], { shell: true, encoding: "utf8" }).stdout.trim();

const client = await ContractClient.fromWasmHash(Buffer.from(wasmHash, "hex"), {
networkPassphrase,
contractId,
rpcUrl,
allowHttp: true,
publicKey: keypair.publicKey(),
...wallet,
});
return {
keypair,
client,
contractId,
}
}
module.exports.clientFor = clientFor

async function clientFromConstructor(contract, { keypair = generateFundedKeypair(), contractId } = {}) {
if (!contracts[contract]) {
throw new Error(
`Contract ${contract} not found. ` +
`Pick one of: ${Object.keys(contracts).join(", ")}`
)
}
keypair = await keypair // eslint-disable-line no-param-reassign
const wallet = basicNodeSigner(keypair, networkPassphrase)

const {path} = contracts[contract];
const xdr = JSON.parse(spawnSync("./target/bin/soroban", ["contract", "inspect", "--wasm", path, "--output", "xdr-base64-array"], { shell: true, encoding: "utf8" }).stdout.trim())

const spec = new ContractSpec(xdr);
const wasmHash = contracts[contract].hash;
if (!wasmHash) {
throw new Error(`No wasm hash found for \`contracts[${contract}]\`! ${JSON.stringify(contracts[contract], null, 2)}`)
Expand Down Expand Up @@ -90,7 +133,7 @@ async function clientFor(contract, { keypair = generateFundedKeypair(), contract
contractId,
}
}
module.exports.clientFor = clientFor
module.exports.clientFromConstructor = clientFromConstructor

/**
* Generates a ContractClient given the contractId using the from method.
Expand Down
6 changes: 3 additions & 3 deletions test/unit/server/soroban/get_contract_wasm_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describe("Server#getContractWasm", () => {
);

this.server
.getContractWasm(contractId)
.getContractWasmByContractId(contractId)
.then((wasmData) => {
expect(wasmData).to.eql(wasmBuffer);
done();
Expand All @@ -157,7 +157,7 @@ describe("Server#getContractWasm", () => {
.returns(Promise.resolve({ data: { result: { entries: [] } } }));

this.server
.getContractWasm(contractId)
.getContractWasmByContractId(contractId)
.then(function (_response) {
done(new Error("Expected error"));
})
Expand Down Expand Up @@ -208,7 +208,7 @@ describe("Server#getContractWasm", () => {
.returns(Promise.resolve({ data: { result: { entries: [] } } }));

this.server
.getContractWasm(contractId)
.getContractWasmByContractId(contractId)
.then(function (_response) {
done(new Error("Expected error"));
})
Expand Down

0 comments on commit 752f177

Please sign in to comment.