diff --git a/agent/agent-base.ts b/agent/agent-base.ts index be9b61784be..fe20afcc06e 100644 --- a/agent/agent-base.ts +++ b/agent/agent-base.ts @@ -1,23 +1,6 @@ -/** - - Fadroma: Base Console and Error Types - Copyright (C) 2023 Hack.bg - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -**/ - +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Error as BaseError } from '@hackbg/oops' import { Console, bold, colors } from '@hackbg/logs' import type { Chain } from './agent-chain' @@ -232,11 +215,11 @@ class AgentConsole extends Console { codeHashMismatch (address: string, expected: string|undefined, fetched: string) { return this.warn(`code hash mismatch for ${address}: expected ${expected}, fetched ${fetched}`) } - waitingForBlock (height: number, elapsed?: number) { - return this.log(`waiting for block > ${height}...`, elapsed ? `${elapsed}ms elapsed` : '') - } confirmCodeHash (address: string, codeHash: string) { - return this.info(`confirmed code hash of ${address}: ${codeHash}`) + return this.info(`Confirmed code hash of ${address}: ${codeHash}`) + } + waitingForBlock (height: number, elapsed?: number) { + return this.log(`Waiting for block > ${bold(String(height))}...`, elapsed ? `${elapsed}ms elapsed` : '') } batchMessages (msgs: any, N: number) { this.info(`Messages in batch`, `#${N}:`) diff --git a/agent/agent-batch.ts b/agent/agent-batch.ts index 8923f9cf332..442c2289367 100644 --- a/agent/agent-batch.ts +++ b/agent/agent-batch.ts @@ -1,3 +1,6 @@ +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Error, Console, into } from './agent-base' import type { Class, Message, CodeId, Address, Name, Into, ICoin, Many, CodeHash @@ -62,14 +65,15 @@ export abstract class Batch implements BatchAgent { memo: string, save: boolean }> = {}): Promise { - this.log(options.save ? 'Saving' : 'Submitting') if (this.depth > 0) { this.log.warn('Unnesting batch. Depth:', --this.depth) this.depth-- return null as any // result ignored } else if (options.save) { + this.log('Saving batch') return this.save(options.memo) } else { + this.log('Submitting batch') return this.submit(options.memo) } } @@ -111,11 +115,11 @@ export abstract class Batch implements BatchAgent { async instantiate ( contract: CodeId|Partial, options: { - label: Name, - initMsg: Into, - initFee?: unknown, - initFunds?: ICoin[], - initMemo?: string, + label: Name, + initMsg: Into, + initFee?: unknown, + initSend?: ICoin[], + initMemo?: string, } ): Promise { @@ -171,11 +175,11 @@ export abstract class Batch implements BatchAgent { outputs[key] = contract.address } else { outputs[key] = await this.instantiate(contract, { - label: contract.label!, - initMsg: contract.initMsg!, - initFee: contract.initFee || options.initFee, - initFunds: contract.initFunds || options.initFunds, - initMemo: contract.initMemo || options.initMemo + label: contract.label!, + initMsg: contract.initMsg!, + initFee: contract.initFee || options.initFee, + initSend: contract.initSend || options.initSend, + initMemo: contract.initMemo || options.initMemo }) } })) diff --git a/agent/agent-chain.ts b/agent/agent-chain.ts index 9172a24c553..519b359e93c 100644 --- a/agent/agent-chain.ts +++ b/agent/agent-chain.ts @@ -1,23 +1,6 @@ -/** - - Fadroma: Base Agent/Chain API - Copyright (C) 2023 Hack.bg - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -**/ - +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import type { Address, Message, ICoin, IFee, CodeHash, Class, Name, Many, CodeId, @@ -228,7 +211,7 @@ export abstract class Chain { return this.height.then(async startingHeight=>{ startingHeight = Number(startingHeight) if (isNaN(startingHeight)) { - this.log.warn('current block height undetermined. not waiting for next block') + this.log.warn('Current block height undetermined. not waiting for next block') return Promise.resolve(NaN) } this.log.waitingForBlock(startingHeight) @@ -240,7 +223,7 @@ export abstract class Chain { this.log.waitingForBlock(startingHeight, + new Date() - t) const height = await this.height if (height > startingHeight) { - this.log.info(`block height incremented to ${height}, continuing`) + this.log.info(`Block height incremented to ${bold(String(height))}, continuing`) return resolve(height) } } @@ -369,8 +352,8 @@ export abstract class Agent { } /** The wallet's mnemonic (write-only). */ - get mnemonic (): Promise { - throw new Error('mnemonic is write-only') + get mnemonic (): boolean { + return false } set mnemonic (mnemonic: string) { @@ -581,9 +564,9 @@ export abstract class Agent { async instantiateMany > ( contracts: M, options: { - initFee?: ICoin[]|'auto', - initFunds?: ICoin[], - initMemo?: string, + initFee?: ICoin[]|'auto', + initSend?: ICoin[], + initMemo?: string, } = {} ): Promise { // Returns an array of TX results. diff --git a/agent/agent-contract.ts b/agent/agent-contract.ts index c5a7a51da22..f34d9f6a53f 100644 --- a/agent/agent-contract.ts +++ b/agent/agent-contract.ts @@ -1,21 +1,6 @@ -/** - Fadroma: Contract Deployment API - Copyright (C) 2023 Hack.bg - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -**/ - +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Console, Error, HEAD } from './agent-base' @@ -56,15 +41,16 @@ assign.allowed = { 'repository', 'revision', 'dirty', 'workspace', 'crate', 'features', ] as Array, CompiledCode: [ - 'buildInfo', 'codeHash', 'codePath', 'codeData' + 'codeHash', + 'buildInfo', 'codePath', 'codeData' ] as Array, ContractUpload: [ - 'deployment', 'chainId', - 'codeId', 'uploadTx', 'uploadBy', 'uploadGas', 'uploadInfo', + 'codeHash', 'chainId', 'codeId', + 'uploadTx', 'uploadBy', 'uploadGas', 'uploadInfo', ] as Array, ContractInstance: [ - 'codeId', 'codeHash', 'label', 'address', 'initMsg', - 'initBy', 'initFunds', 'initFee', 'initMemo', 'initTx', 'initGas' + 'codeHash', 'chainId', 'codeId', 'label', 'address', 'initMsg', + 'initBy', 'initSend', 'initFee', 'initMemo', 'initTx', 'initGas' ] as Array, Deployment: [ 'name' @@ -149,9 +135,9 @@ export class CompiledCode { toReceipt () { return { - buildInfo: this.buildInfo, + codeHash: this.codeHash, codePath: this.codePath, - codeHash: this.codeHash + buildInfo: this.buildInfo, } } @@ -188,6 +174,8 @@ export class CompiledCode { } export class ContractUpload { + /** Code hash uniquely identifying the compiled code. */ + codeHash?: CodeHash /** ID of chain on which this contract is uploaded. */ chainId?: ChainId /** Code ID representing the identity of the contract's code on a specific chain. */ @@ -200,8 +188,6 @@ export class ContractUpload { uploadGas?: string|number /** extra info */ uploadInfo?: string - /** Code hash uniquely identifying the compiled code. */ - codeHash?: CodeHash constructor (properties: Partial = {}) { assign(this, properties, assign.allowed['ContractUpload']) @@ -209,10 +195,11 @@ export class ContractUpload { toReceipt () { return { + codeHash: this.codeHash, chainId: this.chainId, + codeId: this.codeId, uploadBy: this.uploadBy, uploadTx: this.uploadTx, - codeId: this.codeId } } @@ -247,10 +234,12 @@ export class ContractUpload { } export class ContractInstance { - /** Code ID representing the identity of the contract's code on a specific chain. */ - codeId?: CodeId /** Code hash uniquely identifying the compiled code. */ codeHash?: CodeHash + /** Code ID representing the identity of the contract's code on a specific chain. */ + chainId?: ChainId + /** Code ID representing the identity of the contract's code on a specific chain. */ + codeId?: CodeId /** Full label of the instance. Unique for a given Chain. */ label?: Label /** Address of this contract instance. Unique per chain. */ @@ -260,7 +249,7 @@ export class ContractInstance { /** Address of agent that performed the init tx. */ initBy?: Address|Agent /** Native tokens to send to the new contract. */ - initFunds?: ICoin[] + initSend?: ICoin[] /** Fee to use for init. */ initFee?: unknown /** Instantiation memo. */ @@ -277,12 +266,15 @@ export class ContractInstance { /** @returns the data for a deploy receipt */ toReceipt () { return { - initMsg: this.initMsg, - initBy: this.initBy, - initTx: this.initTx, - initGas: this.initGas, - address: this.address, - label: this.label, + codeHash: this.codeHash, + chainId: this.chainId, + codeId: this.codeId, + label: this.label, + address: this.address, + initMsg: this.initMsg, + initBy: this.initBy, + initTx: this.initTx, + initGas: this.initGas, } } @@ -309,7 +301,7 @@ export class Contract { if (properties?.builder) this.builder = properties?.builder if (properties?.binary) this.binary = new CompiledCode(properties.binary) if (properties?.uploader) this.uploader = properties?.uploader - if (properties?.template) this.uploaded = new ContractUpload(properties.template) + if (properties?.uploaded) this.uploaded = new ContractUpload(properties.uploaded) if (properties?.deployer) this.deployer = properties?.deployer if (properties?.instance) this.instance = new ContractInstance(properties.instance) } @@ -397,7 +389,7 @@ export type PartialContract = { builder?: Builder, binary?: Partial, uploader?: Agent|Address, - template?: Partial, + uploaded?: Partial, deployer?: Agent|Address, instance?: Partial } @@ -415,7 +407,7 @@ export class DeploymentUnit extends Contract { } } -export type DeploymentState = Partial> +export type DeploymentState = Partial> /** A constructor for a Deployment subclass. */ export interface DeploymentClass extends Class< @@ -449,17 +441,48 @@ export class Deployment extends Map { set (name: string, unit: Partial): this { if (!(unit instanceof DeploymentUnit)) unit = new DeploymentUnit(unit) - return super.set(name, unit) - } - - template (name: string, properties?: PartialContract): Contract { - this.set(name, { ...properties, name, isTemplate: true }) - return this.get(name)! - } - - contract (name: string, properties?: PartialContract): Contract { - this.set(name, { ...properties, name, isTemplate: false }) - return this.get(name)! + return super.set(name, unit as DeploymentUnit) + } + + /** Define a template, representing code that can be compiled + * and uploaded, but will not be automatically instantiated. + * This can then be used to define multiple instances of + * the same code. */ + template (name: string, properties?: + Partial & + Partial & + Partial + ): DeploymentUnit { + const template = new DeploymentUnit({ + name, + deployment: this, + isTemplate: true, + source: new SourceCode(properties), + binary: new CompiledCode(properties), + uploaded: new ContractUpload(properties) + }) + this.set(name, template) + return template + } + + /** Define a contract that will be automatically compiled, uploaded, + * and instantiated as part of this deployment. */ + contract (name: string, properties?: + Partial & + Partial & + Partial & + Partial + ): DeploymentUnit { + const contract = new DeploymentUnit({ + name, + deployment: this, + isTemplate: true, + source: new SourceCode(properties), + binary: new CompiledCode(properties), + uploaded: new ContractUpload(properties) + }) + this.set(name, contract) + return contract } async build ( @@ -495,12 +518,12 @@ export class Deployment extends Map { ): Promise> { const deploying: Array> = [] for (const [name, contract] of this.entries()) { - console.log({name, contract}) - if (contract.isTemplate) continue - deploying.push(contract.deploy({ - ...contract.instance, - ...options, - })) + if (!contract.isTemplate) { + deploying.push(contract.deploy({ + ...contract.instance, + ...options, + })) + } } const deployed: Record = {} for (const output of await Promise.all(deploying)) { diff --git a/agent/agent-store.ts b/agent/agent-store.ts index 696ae7c9b43..b5617831425 100644 --- a/agent/agent-store.ts +++ b/agent/agent-store.ts @@ -1,3 +1,6 @@ +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Console, Error } from './agent-base' import type { Class, CodeHash, Name } from './agent-base' import type { ChainId } from './agent-chain' @@ -5,7 +8,6 @@ import { Deployment, ContractUpload, SourceCode, CompiledCode } from './agent-co import type { DeploymentClass, DeploymentState } from './agent-contract' export abstract class Builder { - static variants: Record> = {} log = new Console(this.constructor.name) diff --git a/agent/agent-stub.ts b/agent/agent-stub.ts index 9f07599376d..09f272d7368 100644 --- a/agent/agent-stub.ts +++ b/agent/agent-stub.ts @@ -1,3 +1,6 @@ +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import type { Address, CodeHash, Message, CodeId, ICoin, Label } from './agent-base' import { Chain, Agent } from './agent-chain' import { Batch } from './agent-batch' @@ -76,13 +79,7 @@ export class StubAgent extends Agent { /** Stub implementation of contract init */ protected doInstantiate ( codeId: CodeId, - options: { - label: Label, - initMsg: Message, - initFee?: ICoin[]|'auto', - initFunds?: ICoin[], - initMemo?: string, - } + options: Parameters[1] ): Promise { @@ -96,7 +93,7 @@ export class StubAgent extends Agent { protected doExecute ( contract: { address: Address, codeHash: CodeHash }, message: Message, - options?: never + options?: Parameters[2] ): Promise { return Promise.resolve({}) } diff --git a/agent/agent-token.ts b/agent/agent-token.ts index 098c03aea76..c27801a5554 100644 --- a/agent/agent-token.ts +++ b/agent/agent-token.ts @@ -1,23 +1,6 @@ -/** - - Fadroma: Base Token Support - Copyright (C) 2023 Hack.bg - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -**/ - +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Coin } from './agent-base' import type { Agent, Address, ContractClientClass, Uint128, ICoin } from './agent' import { ContractClient } from './agent-contract' diff --git a/agent/agent.test.ts b/agent/agent.test.ts index 5fba1f8453f..212b9958e64 100644 --- a/agent/agent.test.ts +++ b/agent/agent.test.ts @@ -1,10 +1,15 @@ +/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . **/ import { Error, Console, Chain, StubChain, Agent, StubAgent, Batch, StubBatch, ContractClient, - DeployStore, Deployment, ContractUpload, ContractInstance, DeploymentContractLabel, + UploadStore, DeployStore, + Contract, SourceCode, CompiledCode, ContractUpload, ContractInstance, + Deployment, DeploymentContractLabel, assertChain, Builder, StubBuilder, Token, TokenFungible, TokenNonFungible, Swap, @@ -13,43 +18,17 @@ import { } from './agent' import assert from 'node:assert' import { fixture } from '../fixtures/fixtures' - import { Suite } from '@hackbg/ensuite' -export default new Suite([ - ['errors', testAgentErrors], - ['console', testAgentConsole], - ['chain', testChain], - ['devnet', testChainDevnet], - ['agent', testAgent], - ['batch', testBatch], - ['client', testClient], - ['labels', testLabels], - ['deployment', testDeployment], - ['decimals', testDecimals], - ['token', testToken], - ['collections', testCollections] -]) export async function testChain () { let chain = new StubChain() assert.throws(()=>chain.id) assert.throws(()=>chain.id='foo') assert.throws(()=>chain.mode) - - assert.throws(()=>new StubChain({ - mode: StubChain.Mode.Devnet, - id: 'stub', - url: 'stub' - }).ready) - + assert.throws(()=>new StubChain({ mode: StubChain.Mode.Devnet, id: 'stub', url: 'stub' }).ready) assert.equal(chain.chain, chain) assert.equal(assertChain({ chain }), chain) - - chain = new StubChain({ - mode: StubChain.Mode.Testnet, - id: 'stub', - url: 'stub' - }) + chain = new StubChain({ mode: StubChain.Mode.Testnet, id: 'stub', url: 'stub' }) assert.ok((await chain.ready).api) await chain.height await chain.nextBlock @@ -59,28 +38,21 @@ export async function testChain () { await chain.getHash('') await chain.getHash(0) await chain.getLabel('') - Object.defineProperty(chain, 'height', { get () { return Promise.resolve('NaN') } }) assert.equal(await chain.nextBlock, NaN) - assert.ok(StubChain.mainnet().isMainnet) assert.ok(!(StubChain.mainnet().devMode)) - assert.ok(StubChain.testnet().isTestnet) assert.ok(!(StubChain.testnet().devMode)) - assert.ok(StubChain.devnet().isDevnet) assert.ok(StubChain.devnet().devMode) - assert.ok(new StubChain({ mode: Chain.Mode.Mocknet }).isMocknet) assert.ok(new StubChain({ mode: Chain.Mode.Mocknet }).devMode) - } -export async function testChainDevnet () { - +export async function testDevnet () { const devnet = { accounts: [], chainId: 'foo', @@ -92,28 +64,23 @@ export async function testChainDevnet () { async getAccount () { return {} }, async assertPresence () {} } - const chain = new StubChain({ mode: Chain.Mode.Mainnet, devnet, id: 'bar', url: 'http://asdf.com', }) - const ready = chain.ready assert.ok(await ready) assert.ok(chain.ready === ready) - // Properties from Devnet are passed onto Chain assert.equal(chain.devnet, devnet) assert.equal(chain.id, 'foo') assert.equal(chain.url, 'http://example.com/') assert.equal(chain.mode, StubChain.Mode.Devnet) - assert.equal(chain.stopped, true) devnet.running = true assert.equal(chain.stopped, false) - assert.throws(()=>chain.id='asdf') assert.throws(()=>chain.url='asdf') assert.throws(()=>{ @@ -131,100 +98,37 @@ export async function testAgent () { assert.ok(agent.address, 'agent has address') assert.equal(agent.name, 'testing1', 'agent.name assigned') assert.equal(agent.chain, chain, 'agent.chain assigned') - const ready = agent.ready assert.ok(await ready) assert.ok(agent.ready === ready) - agent.defaultDenom agent.balance agent.height agent.nextBlock - await agent.getBalance('a','b') - await agent.query('', {}) - await agent.getCodeId('') - await agent.getHash('') await agent.getHash(0) - await agent.getLabel('') - - await agent.send('', [], {}) - await agent.sendMany([], {}) - + await agent.send('', []) + await agent.sendMany([]) await agent.upload(fixture('null.wasm'), {}) await agent.upload(new Uint8Array(), {}) await agent.uploadMany([], {}) await agent.uploadMany({}, {}) - - await agent.instantiate('1', { - label: 'foo', - initMsg: 'bar' - }) - - await agent.instantiate({ - codeId: '1' - }, { - label: 'foo', - initMsg: 'bar' - }) - + await agent.instantiate('1', { label: 'foo', initMsg: 'bar' }) + await agent.instantiate({ codeId: '1' }, { label: 'foo', initMsg: 'bar' }) await agent.instantiateMany([]) - await agent.instantiateMany({}) - await agent.execute('stub', {}, {}) } -export async function testAgentMeta () { - //client.address = 'someaddress' // FIXME - //assert.ok(client.codeHash = await fetchCodeHash(client, agent)) - ////assert.ok(client.codeId = await fetchCodeId(client, agent)) - //assert.ok(client.label = await fetchLabel(client, agent)) - - //assert.equal(client.codeHash, await fetchCodeHash(client, agent, client.codeHash)) - ////assert.equal(client.codeId, await fetchCodeId(client, agent, client.codeId)) - //assert.equal(client.label, await fetchLabel(client, agent, client.label)) - - //assert.rejects(fetchCodeHash(client, agent, 'unexpected')) - //assert.rejects(fetchCodeId(client, agent, 'unexpected')) - //assert.rejects(fetchLabel(client, agent, 'unexpected')) - - //import { assertCodeHash, codeHashOf } from '@fadroma/agent' - - //assert.ok(assertCodeHash({ codeHash: 'code-hash-stub' })) - //assert.throws(()=>assertCodeHash({})) - - //assert.equal(codeHashOf({ codeHash: 'hash' }), 'hash') - //assert.equal(codeHashOf({ code_hash: 'hash' }), 'hash') - //assert.throws(()=>codeHashOf({ code_hash: 'hash1', codeHash: 'hash2' })) -} - export async function testBatch () { - //import { Chain, Agent, Batch } from '@fadroma/agent' - //chain = new StubChain({ id: 'id', url: 'example.com', mode: 'mainnet' }) - //agent = await chain.getAgent() - //let batch: Batch - //import { Client } from '@fadroma/agent' - //batch = new Batch(agent) - - //assert(batch.getClient(Client, '') instanceof Client, 'Batch#getClient') - //assert.equal(await batch.execute({}), batch) - //assert.equal(batch.id, 1) - ////assert(await batch.instantiateMany({}, [])) - ////assert(await batch.instantiateMany({}, [['label', 'init']])) - ////assert(await batch.instantiate({}, 'label', 'init')) - //assert.equal(await batch.checkHash(), 'code-hash-stub') - - let chain: Chain = new StubChain({ id: 'stub' }) - let agent: Agent = await chain.getAgent({ name: 'job', address: 'testing1agent0' }) - let batch: Batch - + let agent: Agent = await new StubChain({ id: 'stub' }) + .getAgent({ name: 'test-batch', address: 'stub1testbatch' }) + .ready assert(agent.batch() instanceof Batch) - const batchedOperations = async (batch: Batch) => { assert(batch instanceof Batch) assert.rejects(()=>batch.query({} as any, {})) @@ -247,13 +151,11 @@ export async function testBatch () { assert.ok(await batch.getHash('addr')) assert.ok(await batch.checkHash('addr')) } - const batch1 = new StubBatch(agent, batchedOperations) assert.equal(await batch1.ready, batch1) - assert.equal(batch1.name, `job (batched)`) + assert.equal(batch1.name, `test-batch (batched)`) assert.equal(batch1.fees, agent.fees) assert.equal(batch1.defaultDenom, agent.defaultDenom) - const batch2 = new StubBatch(agent) assert.deepEqual(batch2.msgs, []) assert.equal(batch2.id, 0) @@ -262,48 +164,15 @@ export async function testBatch () { assert.deepEqual(batch2.msgs, [{}]) assert.equal(batch2.id, 1) assert.ok(batch2.assertMessages()) - const batch3 = new StubBatch(agent, batchedOperations) - assert.ok(await batch3.run("")) - assert.ok(await batch3.run("", true)) + assert.ok(await batch3.run()) + assert.ok(await batch3.run({ memo: "", save: true })) assert.equal(batch3.depth, 0) const batch3a = batch3.batch() assert.equal(batch3a.depth, 1) assert.equal(await batch3a.run(), null) } -export async function testAgentErrors () { - // Make sure each error subclass can be created with no arguments: - for (const key of Object.keys(Error)) { - const subtype = Error[key as keyof typeof Error] as any - if (typeof subtype ==='function') assert(new subtype() instanceof Error, `error ${key}`) - } -} - -export async function testAgentConsole () { - // Make sure each log message can be created with no arguments: - const log = new Console('(test message)') - for (const key of Object.keys(log)) { - const method = log[key as keyof typeof log] as any - if (typeof method==='function') try { method.bind(log)() } catch (e) { console.warn(e) } - } -} - -export async function testLabels () { - assert.equal( - new DeploymentContractLabel('foo', 'bar', 'baz').toString(), - 'foo/bar+baz' - ) - assert.deepEqual( - DeploymentContractLabel.parse('foo/bar+baz'), - { prefix: 'foo', name: 'bar', suffix: 'baz' } - ) - assert.deepEqual( - DeploymentContractLabel.parse('foo/bar+baz').toString(), - 'foo/bar+baz' - ) -} - export async function testClient () { const chain = new StubChain({ id: 'foo', mode: Chain.Mode.Testnet }) const agent = new StubAgent({ chain }) @@ -317,32 +186,128 @@ export async function testClient () { await client.execute({foo: 'bar'}) } +export async function testContracts () { + const contract = new Contract({ + source: {}, + binary: {}, + uploaded: {}, + instance: {}, + }) + + assert.rejects(()=>contract.compile()) + assert.rejects(()=>contract.upload()) + assert.rejects(()=>contract.deploy()) + + assert(contract.source instanceof SourceCode) + assert(contract.binary instanceof CompiledCode) + assert(contract.uploaded instanceof ContractUpload) + assert(contract.instance instanceof ContractInstance) + + assert(!(contract.source.isValid())) + assert(!(contract.binary.isValid())) + assert(!(contract.uploaded.isValid())) + assert(!(contract.instance.isValid())) + + assert.deepEqual(contract.source.toReceipt(), { + crate: undefined, + dirty: undefined, + features: undefined, + repository: undefined, + revision: undefined, + workspace: undefined, + }) + assert.deepEqual(contract.binary.toReceipt(), { + codeHash: undefined, + codePath: undefined, + buildInfo: undefined, + }) + assert.deepEqual(contract.uploaded.toReceipt(), { + codeHash: undefined, + chainId: undefined, + codeId: undefined, + uploadBy: undefined, + uploadTx: undefined + }) + assert.deepEqual(contract.instance.toReceipt(), { + codeHash: undefined, + chainId: undefined, + codeId: undefined, + label: undefined, + initMsg: undefined, + initBy: undefined, + initTx: undefined, + initGas: undefined, + address: undefined, + }) +} + export async function testDeployment () { - const store = new DeployStore() - assert.equal(store.get('name'), undefined) - assert.equal(store.set('name', {}), store) - assert.ok(store.get('name') instanceof Deployment) - for (const mode of [Chain.Mode.Mainnet, Chain.Mode.Testnet]) { - const deployment = new Deployment({ name: 'deployment' }) + // deploy store converts inputs to ContractUpload instances + const uploadStore = new UploadStore() + assert.equal(uploadStore.get('name'), undefined) + assert.equal(uploadStore.set('name', {}), uploadStore) + console.log(uploadStore.get('name')) + assert.ok(uploadStore.get('name') instanceof ContractUpload) + // deploy store converts inputs to Deployment instances + const deployStore = new DeployStore() + assert.equal(deployStore.get('name'), undefined) + assert.equal(deployStore.set('name', {}), deployStore) + assert.ok(deployStore.get('name') instanceof Deployment) + // deployment can define contracts and templates + const deployment = new Deployment({ name: 'deployment' }) + assert.deepEqual(deployment.toReceipt(), { name: 'deployment', units: {} }) + const template1 = deployment.template('template1', { + codeData: new Uint8Array([1]), + codeHash: "asdf" + }) + await deployment.upload({ builder: new StubBuilder(), uploader: new StubAgent() }) + const contract1 = deployment.contract('contract1', { + codeId: '2', + label: "contract1", + initMsg: {} + }) + await deployment.deploy({ uploader: new StubAgent(), deployer: new StubAgent() }) + // pretty print deployment + new Console().deployment(deployment) + // deployment label format + const label1 = new DeploymentContractLabel('foo', 'bar', 'baz') + assert.equal(label1.toString(), 'foo/bar+baz') + const label2 = DeploymentContractLabel.parse('foo/bar+baz') + assert(label2 instanceof DeploymentContractLabel) + assert.deepEqual(label2, { prefix: 'foo', name: 'bar', suffix: 'baz' }) + assert.deepEqual(label2.toString(), 'foo/bar+baz') +} + +export default new Suite([ + ['agent', testAgent], + ['batch', testBatch], + ['chain', testChain], + ['client', testClient], + ['collections', testCollections], + ['console', testConsole], + ['contract', testContracts], + ['decimals', testDecimals], + ['deploy', testDeployment], + ['devnet', testDevnet], + ['errors', testErrors], + ['labels', testLabels], + ['token', testToken], +]) - assert.deepEqual(deployment.toReceipt(), { name: 'deployment', units: {} }) +export async function testErrors () { + // Make sure each error subclass can be created with no arguments: + for (const key of Object.keys(Error)) { + const subtype = Error[key as keyof typeof Error] as any + if (typeof subtype ==='function') assert(new subtype() instanceof Error, `error ${key}`) + } +} - const template1 = deployment.template('template1', { - binary: { codeData: new Uint8Array([1]), codeHash: "asdf" } - }) - await deployment.upload({ - builder: new StubBuilder(), - uploader: new StubAgent() - }) - const contract1 = deployment.contract('contract1', { - template: { codeId: '2' }, - instance: { label: "contract1", initMsg: {} } - }) - await deployment.deploy({ - uploader: new StubAgent(), - deployer: new StubAgent() - }) - new Console().deployment(deployment) +export async function testConsole () { + // Make sure each log message can be created with no arguments: + const log = new Console('(test message)') + for (const key of Object.keys(log)) { + const method = log[key as keyof typeof log] as any + if (typeof method==='function') try { method.bind(log)() } catch (e) { console.warn(e) } } } @@ -403,3 +368,6 @@ export async function testCollections () { asyncFn: 4 }) } + +export async function testLabels () { +} diff --git a/agent/agent.ts b/agent/agent.ts index 7c4cf8faf67..68541f2fc63 100644 --- a/agent/agent.ts +++ b/agent/agent.ts @@ -1,6 +1,6 @@ /** - Fadroma: Core Agent Library + Fadroma Agent Copyright (C) 2023 Hack.bg This program is free software: you can redistribute it and/or modify diff --git a/agent/agentchain.ts.old b/agent/agentchain.ts.old new file mode 100644 index 00000000000..f287bdf5626 --- /dev/null +++ b/agent/agentchain.ts.old @@ -0,0 +1,543 @@ +import type { + Address, Message, ICoin, IFee, CodeHash, + Class, Name, Many, CodeId, + Into, TxHash, Label, + Batch, BatchClass, BatchCallback, + UploadStore +} from './agent' +import { + CompiledCode, + ContractUpload, + ContractInstance, + ContractClient, + ContractClientClass +} from './agent-contract' +import { + Error, Console, bold, into, mapAsync, hideProperties, randomBytes +} from './agent-base' + +/** A chain can be in one of the following modes: */ +export enum ChainMode { + Mainnet = 'Mainnet', + Testnet = 'Testnet', + Devnet = 'Devnet', + Mocknet = 'Mocknet' +} + +/** The unique ID of a chain. */ +export type ChainId = string + +/** A collection of functions that return Chain instances. */ +export type ChainRegistry = RecordChain> + +/** Interface for Devnet (implementation is in @hackbg/fadroma). */ +export interface DevnetHandle { + accounts: string[] + chainId: string + platform: string + running: boolean + stateDir: string + url: URL + + containerId?: string + imageTag?: string + port?: string|number + + start (): Promise + getAccount (name: string): Promise> + assertPresence (): Promise +} + +/** Represents a particular chain, identified by chain ID and connected by URL. + * The chain can be in one of several modes (mainnet or other), can optionally + * hold a reference to the managed devnet container, can query state, and can + * construct authorized agents. */ +export abstract class Chain { + /** The ChainMode enum. */ + static Mode = ChainMode + /** Create a mainnet instance of this chain. */ + static mainnet (options: Partial = {}): Chain & { mode: ChainMode.Mainnet } { + return new (this as any)({ ...options, mode: Chain.Mode.Mainnet }) + } + /** Create a testnet instance of this chain. */ + static testnet (options: Partial = {}): Chain & { mode: ChainMode.Testnet } { + return new (this as any)({ ...options, mode: Chain.Mode.Testnet }) + } + /** Create a devnet instance of this chain. */ + static devnet (options: Partial = {}): Chain & { mode: ChainMode.Devnet } { + return new (this as any)({ ...options, mode: Chain.Mode.Devnet }) + } + /** Create a mocknet instance of this chain. */ + static mocknet (options?: Partial): Chain & { mode: ChainMode.Mocknet } { + throw new Error('Mocknet is not enabled for this chain.') + } + /** The default Batch class used by this Agent. */ + static Batch: BatchClass // populated by bindChainSupport + + /** The Batch subclass to use. */ + Batch: BatchClass = (this.constructor as unknown as { Batch: BatchClass }).Batch + /** Logger. */ + log: Console = new Console(this.constructor.name) + /** The API URL to use. */ + url: string = '' + /** Instance of the underlying platform API (secretjs, cosmjs, etc.). */ + api?: unknown + /** If this is a devnet, this contains an interface to the devnet container. */ + devnet?: DevnetHandle + /** Whether this chain is stopped. */ + stopped: boolean = false + /** The friendly name of the agent. */ + name?: string + /** The address from which transactions are signed and sent. */ + address?: Address + /** Default fee maximums for send, upload, init, and execute. */ + fees?: { send?: IFee, upload?: IFee, init?: IFee, exec?: IFee } + + constructor ({ id, url, mode, devnet }: Partial = {}) { + + this.name = options.name ?? this.name + this.fees = options.fees ?? this.fees + this.address = options.address ?? this.address + hideProperties(this, 'chain', 'address', 'log', 'Batch') + + if (devnet) { + Object.defineProperties(this, { + id: { + enumerable: true, + configurable: true, + get: () => devnet.chainId, + set: () => { throw new Error("can't override chain id of devnet") } + }, + url: { + enumerable: true, + configurable: true, + get: () => devnet.url.toString(), + set: () => { throw new Error("can't override url of devnet") } + }, + 'mode': { + enumerable: true, + configurable: true, + get: () => Chain.Mode.Devnet, + set: () => { throw new Error("chain.mode: can't override") } + }, + 'devnet': { + enumerable: true, + configurable: true, + get: () => devnet, + set: () => { throw new Error("chain.devnet: can't override") } + }, + 'stopped': { + enumerable: true, + configurable: true, + get: () => !this.devnet!.running, + set: () => { throw new Error("chain.stopped: can't override") } + } + }) + if (id && id !== devnet.chainId) { + this.log.warn('chain.id: ignoring override (devnet)') + } + if (url && url.toString() !== devnet.url.toString()) { + this.log.warn('chain.url: ignoring override (devnet)') + } + if (mode && mode !== Chain.Mode.Devnet) { + this.log.warn('chain.mode: ignoring override (devnet)') + } + } else { + if (id) { + Object.defineProperty(this, 'id', { + enumerable: true, + writable: false, + value: id + }) + } + if (mode) { + Object.defineProperty(this, 'mode', { + enumerable: true, + writable: false, + value: mode + }) + } + this.url = url ?? this.url + } + + Object.defineProperty(this, 'log', { + enumerable: false, + writable: true, + }) + + Object.defineProperty(this, 'Agent', { + enumerable: false, + writable: true + }) + + } + + /** Compact string tag for console representation. */ + get [Symbol.toStringTag]() { + return this.address + ? `${this.id}: ${this.address}` + : `${this.id} (unauthenticated)` + } + + /** The unique chain id. */ + get id (): ChainId { + throw new Error("chain id not set") + } + set id (id: string) { + throw new Error("can't override chain id") + } + /** Whether this is mainnet, public testnet, local devnet, or mocknet. */ + get mode (): ChainMode { + throw new Error('chain mode not set') + } + set mode (id: string) { + throw new Error("can't override chain mode") + } + /** Whether this is a mainnet. */ + get isMainnet () { + return this.mode === ChainMode.Mainnet + } + /** Whether this is a testnet. */ + get isTestnet () { + return this.mode === ChainMode.Testnet + } + /** Whether this is a devnet. */ + get isDevnet () { + return this.mode === ChainMode.Devnet + } + /** Whether this is a mocknet. */ + get isMocknet () { + return this.mode === ChainMode.Mocknet + } + /** Whether this is a devnet or mocknet. */ + get devMode () { + return this.isDevnet || this.isMocknet + } + + get ready () { + if (this.isDevnet && !this.devnet) { + throw new Error("the chain is marked as a devnet but is missing the devnet handle") + } + type This = this + type ThisWithApi = This & { api: NonNullable } + const init = new Promise(async (resolve, reject)=>{ + if (this.isDevnet) { + await this.devnet!.start() + } + if (!this.api) { + if (!this.url) throw new Error("the chain's url property is not set") + this.api = await Promise.resolve(this.getApi()) + } + return resolve(this as ThisWithApi) + }) + Object.defineProperty(this, 'ready', { get () { return init } }) + return init + } + + /** Complete the asynchronous initialization of this Agent. */ + get ready (): Promise { + const init = new Promise(async (resolve, reject)=>{ + try { + if (this.chain?.devnet) await this.chain?.devnet.start() + if (!this.mnemonic && this.name && this.chain?.devnet) { + Object.assign(this, await this.chain?.devnet.getAccount(this.name)) + } + resolve(this) + } catch (e) { + reject(e) + } + }) + Object.defineProperty(this, 'ready', { get () { return init } }) + return init + } + + /** Wait for the block height to increment. */ + get nextBlock (): Promise { + return this.height.then(async startingHeight=>{ + startingHeight = Number(startingHeight) + if (isNaN(startingHeight)) { + this.log.warn('current block height undetermined. not waiting for next block') + return Promise.resolve(NaN) + } + this.log.waitingForBlock(startingHeight) + const t = + new Date() + return new Promise(async (resolve, reject)=>{ + try { + while (true && !this.stopped) { + await new Promise(ok=>setTimeout(ok, 250)) + this.log.waitingForBlock(startingHeight, + new Date() - t) + const height = await this.height + if (height > startingHeight) { + this.log.info(`block height incremented to ${height}, continuing`) + return resolve(height) + } + } + } catch (e) { + reject(e) + } + }) + }) + } + + abstract getApi (): unknown + + /** The default denomination of the chain's native token. */ + abstract defaultDenom: string + + /** Get the current block height. */ + abstract get height (): Promise + + abstract getAccountInfo (address?: Address|{ address?: Address }): Promise + + abstract getCodeInfo (id: CodeId|{ codeId?: CodeId }): Promise + + abstract getContractInfo (address: Address|{ address?: Address }): Promise + + /** Get the native balance of an address. */ + abstract getBalance (denom?: string): Promise + + /** Get the native balance of an address. */ + abstract getBalanceOf (address: Address, denom?: string): Promise + + /** Get the code id of a smart contract. */ + abstract getCodeId (address: Address): Promise + + /** Get the code hash of a smart contract. */ + abstract getCodeHash (hash: CodeHash): Promise + + /** Get the code hash of a smart contract. */ + abstract getContractHash (address: Address): Promise + + /** Get the label of a smart contract. */ + abstract getContractLabel (address: Address): Promise + + /** Send native tokens to 1 recipient. */ + abstract send (to: Address, amounts: ICoin[], opts?: unknown): Promise + + /** Send native tokens to multiple recipients. */ + abstract sendMany (outputs: [Address, ICoin[]][], opts?: unknown): Promise + + /** Upload a contract's code, generating a new code id/hash pair. */ + async upload ( + code: string|URL|Uint8Array|Partial, + options: { + reupload?: boolean, + uploadStore?: UploadStore, + uploadFee?: ICoin[]|'auto', + uploadMemo?: string + } = {}, + ): Promise { + let template: Uint8Array + if (code instanceof Uint8Array) { + template = code + } else { + if (typeof code === 'string' || code instanceof URL) { + code = new CompiledCode({ codePath: code }) + } else { + code = new CompiledCode(code) + } + const t0 = performance.now() + template = await (code as CompiledCode).fetch() + const t1 = performance.now() - t0 + this.log.log( + `Fetched in`, + bold(t1.toFixed(3)), + `msec:`, + bold(String(code.codeData?.length)), + `bytes` + ) + } + const t0 = performance.now() + const result = await this.doUpload(template, options) + const t1 = performance.now() - t0 + this.log.log( + `Uploaded in`, + bold(t1.toFixed(3)), + `msec: code id`, + bold(String(result.codeId)), + `on chain`, + bold(result.chainId) + ) + return new ContractUpload({ ...template, ...result }) as ContractUpload & { + chainId: ChainId, + codeId: CodeId, + } + } + + /** Upload multiple contracts. */ + async uploadMany ( + codes: Many>, + options: Parameters[1] + ) { + return mapAsync(codes, code => this.upload(code, options)) + } + + /** Chain-specific upload logic. */ + protected abstract doUpload ( + data: Uint8Array, + options: Parameters[1] + ): Promise> + + /** Create a new smart contract from a code id, label and init message. + * @example + * await agent.instantiate(template.define({ label, initMsg }) + * @returns + * ContractInstance with no `address` populated yet. + * This will be populated after executing the batch. */ + async instantiate ( + contract: CodeId|Partial, + options: Partial + ): Promise { + if (typeof contract === 'string') { + contract = new ContractUpload({ codeId: contract }) + } + if (isNaN(Number(contract.codeId))) { + throw new Error.Invalid('code id') + } + if (!contract.codeId) { + throw new Error.Missing.CodeId() + } + if (!options.label) { + throw new Error.Missing.Label() + } + if (!options.initMsg) { + throw new Error.Missing.InitMsg() + } + const t0 = performance.now() + const result = await this.doInstantiate(contract.codeId, { + ...options, + initMsg: await into(options.initMsg) + }) + const t1 = performance.now() - t0 + this.log.debug( + `Instantiated in`, + bold(t1.toFixed(3)), + `msec: code id`, + bold(String(contract.codeId)), + `address`, + bold(result.address) + ) + return new ContractInstance({ + ...options, + ...result + }) as ContractInstance & { + address: Address + } + } + + /** Chain-specific instantiate logic. */ + protected abstract doInstantiate ( + codeId: CodeId, + options: Partial + ): Promise> + + /** Create multiple smart contracts from a Template (providing code id) + * and a list or map of label/initmsg pairs. + * Uses this agent's Batch class to instantiate them in a single transaction. + * @example + * await agent.instantiateMany(template.instances({ + * One: { label, initMsg }, + * Two: { label, initMsg }, + * })) + * await agent.instantiateMany({ + * One: template1.instance({ label, initMsg }), + * Two: template2.instance({ label, initMsg }), + * })) + * @returns + * either an Array or a Record, + * depending on what is passed as inputs. */ + async instantiateMany > ( + contracts: M, + options: { + initFee?: ICoin[]|'auto', + initSend?: ICoin[], + initMemo?: string, + } = {} + ): Promise { + // Returns an array of TX results. + const batch = this.batch((batch: any)=>batch.instantiateMany(contracts)) + const response = await batch.run() + // Populate contracts with resulting addresses + for (const contract of Object.values(contracts)) { + if (contract.address) continue + // Find result corresponding to contract + const found = response.find(({ label }:any)=>label===contract.label) + if (found) { + const { address, tx, sender } = found // FIXME: implementation dependent + contract.address = address + contract.initTx = tx + contract.initBy = sender + } else { + this.log.warn(`Failed to find address for ${contract.label}.`) + continue + } + } + return contracts + } + + /** Get a client instance for talking to a specific smart contract as this executor. */ + getClient ( + $C: ContractClientClass = ContractClient as unknown as ContractClientClass, + contract: Partial = {} + ): C { + return new $C( + contract, + this + ) as C + } + + /** Call a transaction method on a smart contract. */ + async execute ( + contract: Address|Partial, + message: Message, + options?: { + execFee?: IFee + execSend?: ICoin[] + execMemo?: string + } + ): Promise { + if (typeof contract === 'string') contract = new ContractInstance({ address: contract }) + if (!contract.address) throw new Error("agent.execute: no contract address") + const t0 = performance.now() + const result = await this.doExecute(contract as { address: Address }, message, options) + const t1 = performance.now() - t0 + return result + } + + /** Chain-specific execute logic. */ + protected abstract doExecute ( + contract: Partial, + message: Message, + options: Parameters[2] + ): Promise + + /** Query a contract on the chain. */ + query ( + contract: Address|Partial, + message: Message + ): Promise { + if (typeof contract === 'string') contract = { address: contract } + return this.doQuery(contract, message) + } + + /** Chain-specific query logic. */ + protected abstract doQuery ( + contract: Partial, + message: Message + ): Promise + + /** Execute a transaction batch. + * @returns Batch if called with no arguments + * @returns Promise if called with Batch#wrap args */ + batch (cb?: BatchCallback): B { + return new this.Batch( + this, + cb as BatchCallback + ) as unknown as B + } +} diff --git a/connect/connect.ts b/connect/connect.ts index 58fdf3decd3..1f80f463252 100644 --- a/connect/connect.ts +++ b/connect/connect.ts @@ -1,8 +1,21 @@ /** - Fadroma Connect. Copyright (C) 2023 Hack.bg. Licensed under GNU AGPLv3 or exception. + Fadroma Connect + Copyright (C) 2023 Hack.bg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . **/ + import { Console, Error, Chain, ChainMode, ChainId, bold } from '@fadroma/agent' import type { Agent, ChainRegistry } from '@fadroma/agent' import * as Scrt from '@fadroma/scrt' diff --git a/connect/cw/cw.ts b/connect/cw/cw.ts index 2be2a59d954..39680156da1 100644 --- a/connect/cw/cw.ts +++ b/connect/cw/cw.ts @@ -1,2 +1,20 @@ +/** + Fadroma CW + Copyright (C) 2023 Hack.bg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +**/ + export * from './cw-base' export * as OKP4 from './okp4/okp4' diff --git a/connect/scrt/scrt.ts b/connect/scrt/scrt.ts index a76ff07bc34..b4adafa0d42 100644 --- a/connect/scrt/scrt.ts +++ b/connect/scrt/scrt.ts @@ -1,6 +1,6 @@ -/* - Fadroma Platform Package for Secret Network - Copyright (C) 2022 Hack.bg +/** + Fadroma SCRT + Copyright (C) 2023 Hack.bg This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -9,7 +9,7 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License diff --git a/fadroma.ts b/fadroma.ts index 8c75b5c4400..44e21ca456c 100644 --- a/fadroma.ts +++ b/fadroma.ts @@ -1,6 +1,21 @@ -/** Fadroma. Copyright (C) 2023 Hack.bg. License: GNU AGPLv3 or custom. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . **/ +/** + Fadroma + Copyright (C) 2023 Hack.bg + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +**/ + import type { ChainClass } from '@fadroma/connect' import { connectModes, CW, Scrt } from '@fadroma/connect' import { Config } from './ops/config'