From 2bf7a4011737f37748b3c8d5a58837823e870e2a Mon Sep 17 00:00:00 2001 From: Adam Avramov Date: Tue, 24 Oct 2023 13:36:48 +0300 Subject: [PATCH] BREAKING: refactor(agent,cw,scrt): saner upload method - common upload, abstract chain-specific doUpload - type gymnastics on Batch - can now pass string (file path), URL (file: or other), Uint8Array, or Uploadable to agent.upload --- agent/agent-chain.ts | 60 ++++++++++++++++++++++++++++++------ agent/agent-services.ts | 3 +- connect/cw/cw-base.ts | 2 +- connect/scrt/scrt-chain.ts | 13 ++++---- connect/scrt/scrt-mocknet.ts | 4 +-- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/agent/agent-chain.ts b/agent/agent-chain.ts index d3838f123e9..4a4632c1772 100644 --- a/agent/agent-chain.ts +++ b/agent/agent-chain.ts @@ -486,8 +486,44 @@ export abstract class Agent { /** Send native tokens to multiple recipients. */ abstract sendMany (outputs: [Address, ICoin[]][], opts?: ExecOpts): Promise - /** Upload code, generating a new code id/hash pair. */ - abstract upload (data: Uint8Array, meta?: Partial): Promise + /** Upload a contract's code, generating a new code id/hash pair. */ + async upload (uploadable: string|URL|Uint8Array|Partial): Promise { + const fromPath = async (path: string) => { + const { readFile } = await import('node:fs/promises') + return await readFile(path) + } + const fromURL = async (url: URL) => { + if (url.protocol === 'file:') { + const { fileURLToPath } = await import('node:url') + return await fromPath(fileURLToPath(url)) + } else { + return new Uint8Array(await (await fetch(url)).arrayBuffer()) + } + } + let data: Uint8Array + const t0 = + new Date() + if (typeof uploadable === 'string') { + data = await fromPath(uploadable) + } else if (uploadable instanceof URL) { + data = await fromURL(uploadable) + } else if (uploadable instanceof Uint8Array) { + data = uploadable + } else if (uploadable.artifact) { + uploadable = uploadable.artifact + if (typeof uploadable === 'string') { + data = await fromPath(uploadable) + } else if (uploadable instanceof URL) { + data = await fromURL(uploadable) + } + } else { + throw new Error('Invalid argument passed to Agent#upload') + } + const result = this.doUpload(data!) + this.log.debug(`Uploaded in ${t0}msec:`, result) + return result + } + + protected abstract doUpload (data: Uint8Array): Promise /** Get an uploader instance which performs code uploads and optionally caches them. */ getUploader ($U: UploaderClass, options?: Partial): U { @@ -578,7 +614,7 @@ export class StubAgent extends Agent { } /** Stub implementation of code upload. */ - upload (data: Uint8Array, meta?: Partial): Promise { + protected doUpload (data: Uint8Array): Promise { this.log.warn('Agent#upload: this function is stub; use a subclass of Agent') return Promise.resolve({ chainId: this.chain!.id, @@ -618,12 +654,13 @@ export function assertAgent (thing: { agent?: A|null } = {}): /** A constructor for a Batch subclass. */ export interface BatchClass extends Class>{} -/** Batch is an alternate executor that collects collects messages to broadcast - * as a single transaction in order to execute them simultaneously. For that, it - * uses the API of its parent Agent. You can use it in scripts with: - * await agent.batch().wrap(async batch=>{ client.as(batch).exec(...) }) - * */ -export abstract class Batch implements Agent { +type BatchAgent = Omit & { ready: Promise } + +/** Batch is an alternate executor that collects messages to broadcast + * as a single transaction in order to execute them simultaneously. + * For that, it uses the API of its parent Agent. You can use it in scripts with: + * await agent.batch().wrap(async batch=>{ client.as(batch).exec(...) }) */ +export abstract class Batch implements BatchAgent { /** Messages in this batch, unencrypted. */ msgs: any[] = [] /** Next message id. */ @@ -777,7 +814,10 @@ export abstract class Batch implements Agent { /** Uploads are disallowed in the middle of a batch because * it's easy to go over the max request size, and * difficult to know what that is in advance. */ - async upload (data: Uint8Array, meta?: Partial): Promise { + async upload (data: Uint8Array): Promise { + throw new Error.Invalid.Batching("upload") + } + async doUpload (data: Uint8Array): Promise { throw new Error.Invalid.Batching("upload") } /** Uploads are disallowed in the middle of a batch because diff --git a/agent/agent-services.ts b/agent/agent-services.ts index 6bd1b333e6f..743375af301 100644 --- a/agent/agent-services.ts +++ b/agent/agent-services.ts @@ -108,7 +108,6 @@ export abstract class Builder { * Builder implementations override this, though. */ abstract buildMany (sources: (string|Buildable)[], ...args: unknown[]): Promise } - /** Uploader: uploads a `Template`'s `artifact` to a specific `Chain`, * binding the `Template` to a particular `chainId` and `codeId`. */ export class Uploader { @@ -180,7 +179,7 @@ export class Uploader { const log = new Console(`${contract.codeHash} -> ${this.agent.chain?.id??'(unknown chain id)'}`) log(`from ${bold(contract.artifact)}`) log(`${bold(String(data.length))} bytes (uncompressed)`) - const result = await this.agent.upload(data, contract) + const result = await this.agent.upload(contract) this.checkCodeHash(contract, result) const { codeId, codeHash, uploadTx } = result log(`done, code id`, codeId) diff --git a/connect/cw/cw-base.ts b/connect/cw/cw-base.ts index 33a98941eb9..034767f60ce 100644 --- a/connect/cw/cw-base.ts +++ b/connect/cw/cw-base.ts @@ -199,7 +199,7 @@ class CWAgent extends Agent { throw new Error('not implemented') } - async upload (data: Uint8Array, meta?: Partial): Promise { + protected async doUpload (data: Uint8Array): Promise { const { api } = await this.ready if (!this.address) throw new Error.Missing.Address() const result = await api.upload( diff --git a/connect/scrt/scrt-chain.ts b/connect/scrt/scrt-chain.ts index 49ca3c94806..571d72e2ce7 100644 --- a/connect/scrt/scrt-chain.ts +++ b/connect/scrt/scrt-chain.ts @@ -19,7 +19,7 @@ import { import type { AgentClass, Built, Uploaded, AgentFees, ChainClass, Uint128, BatchClass, Client, ExecOpts, ICoin, Message, Name, AnyContract, Address, TxHash, ChainId, CodeId, CodeHash, Label, - Instantiated + Instantiated, Uploadable } from '@fadroma/agent' /** Represents a Secret Network API endpoint. */ @@ -141,10 +141,10 @@ class ScrtChain extends Chain { ...options||{}, }) as ScrtChain - /** Connect to a Secret Network devnet. */ - static devnet = (options: Partial = {}): ScrtChain => super.devnet({ - ...options||{}, - }) as ScrtChain + /** Connect to Secret Network in testnet mode. */ + static devnet = (options: Partial = {}): ScrtChain => { + throw new Error('Devnet not installed. Import @hackbg/fadroma') + } /** Connect to a Secret Network mocknet. */ static mocknet = (options: Partial = {}): Mocknet.Chain => new Mocknet.Chain({ @@ -357,7 +357,7 @@ class ScrtAgent extends Agent { } /** Upload a WASM binary. */ - async upload (data: Uint8Array): Promise { + protected async doUpload (data: Uint8Array): Promise { const { api } = await this.ready type Log = { type: string, key: string } if (!this.address) throw new Error.Missing.Address() @@ -385,7 +385,6 @@ class ScrtAgent extends Agent { this.log.error(`Code id not found in result.`) throw new Error.Failed.Upload({ ...result, noCodeId: true }) } - this.log.debug(`gas used for upload of ${data.length} bytes:`, result.gasUsed) return { chainId: assertChain(this).id, codeId, diff --git a/connect/scrt/scrt-mocknet.ts b/connect/scrt/scrt-mocknet.ts index c4cfcfabc77..6248d167304 100644 --- a/connect/scrt/scrt-mocknet.ts +++ b/connect/scrt/scrt-mocknet.ts @@ -260,8 +260,8 @@ class MocknetAgent extends Agent { } /** Upload a binary to the mocknet. */ - async upload (wasm: Uint8Array, meta?: Partial): Promise { - return new Contract(await this.chain.upload(wasm, meta)) as unknown as Uploaded + protected async doUpload (wasm: Uint8Array): Promise { + return new Contract(await this.chain.upload(wasm)) as unknown as Uploaded } /** Instantiate a contract on the mocknet. */ async instantiate (instance: Contract): Promise {