diff --git a/src/__tests__/integration/basic/pst.test.ts b/src/__tests__/integration/basic/pst.test.ts index 1ae683b..a8a46b6 100644 --- a/src/__tests__/integration/basic/pst.test.ts +++ b/src/__tests__/integration/basic/pst.test.ts @@ -120,7 +120,8 @@ describe('Testing the Profit Sharing Token', () => { const newSource = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8'); - const newSrcTxId = await pst.save({ src: newSource }, warp.environment); + const srcTx = await warp.createSourceTx({ src: newSource }, wallet); + const newSrcTxId = await warp.saveSourceTx(srcTx); await mineBlock(warp); await pst.evolve(newSrcTxId); diff --git a/src/__tests__/integration/wasm/as-deploy-write-read.test.ts b/src/__tests__/integration/wasm/as-deploy-write-read.test.ts index 2666e34..f1bef7b 100644 --- a/src/__tests__/integration/wasm/as-deploy-write-read.test.ts +++ b/src/__tests__/integration/wasm/as-deploy-write-read.test.ts @@ -159,13 +159,15 @@ describe('Testing the Warp client for AssemblyScript WASM contract', () => { const newContractSrc = fs.readFileSync(path.join(__dirname, '../data/wasm/as/assemblyscript-counter-evolve.wasm')); - const newSrcTxId = await contract.save( + const srcTx = await warp.createSourceTx( { src: newContractSrc, wasmSrcCodeDir: path.join(__dirname, '../data/wasm/as/assembly-evolve') }, - warp.environment + wallet ); + const newSrcTxId = await warp.saveSourceTx(srcTx); + await mineBlock(warp); await contract.evolve(newSrcTxId); diff --git a/src/__tests__/integration/wasm/go-deploy-write-read.test.ts b/src/__tests__/integration/wasm/go-deploy-write-read.test.ts index d3db1ac..deed2bc 100644 --- a/src/__tests__/integration/wasm/go-deploy-write-read.test.ts +++ b/src/__tests__/integration/wasm/go-deploy-write-read.test.ts @@ -197,13 +197,14 @@ describe('Testing the Go WASM Profit Sharing Token', () => { const newContractSrc = fs.readFileSync(path.join(__dirname, '../data/wasm/go/go-pst-evolve.wasm')); - const newSrcTxId = await pst.save( + const srcTx = await warp.createSourceTx( { src: newContractSrc, wasmSrcCodeDir: path.join(__dirname, '../data/wasm/go/src-evolve') }, - warp.environment + wallet ); + const newSrcTxId = await warp.saveSourceTx(srcTx); await mineBlock(warp); diff --git a/src/__tests__/integration/wasm/rust-deploy-write-read.test.ts b/src/__tests__/integration/wasm/rust-deploy-write-read.test.ts index 79d5ec2..04909b4 100644 --- a/src/__tests__/integration/wasm/rust-deploy-write-read.test.ts +++ b/src/__tests__/integration/wasm/rust-deploy-write-read.test.ts @@ -223,14 +223,15 @@ describe('Testing the Rust WASM Profit Sharing Token', () => { const newContractSrc = fs.readFileSync(path.join(__dirname, '../data/wasm/rust/rust-pst-evolve_bg.wasm')); - const newSrcTxId = await pst.save( + const srcTx = await warp.createSourceTx( { src: newContractSrc, wasmSrcCodeDir: path.join(__dirname, '../data/wasm/rust/src-evolve'), wasmGlueCode: path.join(__dirname, '../data/wasm/rust/rust-pst-evolve.js') }, - warp.environment + wallet ); + const newSrcTxId = await warp.saveSourceTx(srcTx); await mineBlock(warp); diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index d9fcdd3..7664642 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -4,7 +4,6 @@ import { InteractionResult } from '../core/modules/impl/HandlerExecutorFactory'; import { EvaluationOptions, EvalStateResult } from '../core/modules/StateEvaluator'; import { GQLNodeInterface } from '../legacy/gqlResult'; import { ArTransfer, Tags, ArWallet } from './deploy/CreateContract'; -import { Source } from './deploy/Source'; import { SignatureType } from './Signature'; export type CurrentTx = { interactionTxId: string; contractTxId: string }; @@ -69,7 +68,7 @@ export type InnerCallData = { callingInteraction: GQLNodeInterface; callType: In * A base interface to be implemented by SmartWeave Contracts clients * - contains "low-level" methods that allow to interact with any contract */ -export interface Contract extends Source { +export interface Contract { /** * Returns the Arweave transaction id of this contract. */ diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index eb1055b..5ab438f 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -754,17 +754,6 @@ export class HandlerBasedContract implements Contract { return await this.writeInteraction({ function: 'evolve', value: newSrcTxId }, options); } - async save(sourceData: SourceData): Promise { - if (!this.signature) { - throw new Error("Wallet not connected. Use 'connect' method first."); - } - const source = new SourceImpl(this.warp); - - const srcTx = await source.save(sourceData, this.warp.environment, this.signature); - - return srcTx.id; - } - get rootSortKey(): string { return this._rootSortKey; } diff --git a/src/contract/deploy/CreateContract.ts b/src/contract/deploy/CreateContract.ts index 1776c3a..67a5105 100644 --- a/src/contract/deploy/CreateContract.ts +++ b/src/contract/deploy/CreateContract.ts @@ -1,5 +1,6 @@ import { JWKInterface } from 'arweave/node/lib/wallet'; import { SignatureType } from '../../contract/Signature'; +import { Source } from './Source'; export type Tags = { name: string; value: string }[]; @@ -43,7 +44,7 @@ export interface ContractDeploy { srcTxId?: string; } -export interface CreateContract { +export interface CreateContract extends Source { deploy(contractData: ContractData, disableBundling?: boolean): Promise; deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise; diff --git a/src/contract/deploy/Source.ts b/src/contract/deploy/Source.ts index cebb5a1..4fe3279 100644 --- a/src/contract/deploy/Source.ts +++ b/src/contract/deploy/Source.ts @@ -1,17 +1,19 @@ import { ArWallet } from './CreateContract'; import { SourceData } from './impl/SourceImpl'; -import { WarpEnvironment } from '../../core/Warp'; import { SignatureType } from '../../contract/Signature'; - +import Transaction from 'arweave/node/lib/transaction'; export interface Source { /** - * allows to post contract source on Arweave - * @param contractSource - contract source... + * allows to create contract source + * @param sourceData - contract source data + * @param wallet - either Arweave wallet or custom signature type */ - save( - contractSource: SourceData, - env: WarpEnvironment, - signer?: ArWallet | SignatureType, - useBundler?: boolean - ): Promise; + createSourceTx(sourceData: SourceData, wallet: ArWallet | SignatureType): Promise; + + /** + * allows to save contract source + * @param sourceTx - contract source transaction + * @param disableBundling = whether source should be deployed through bundlr using Warp Gateway + */ + saveSourceTx(sourceTx: Transaction, disableBundling?: boolean): Promise; } diff --git a/src/contract/deploy/impl/DefaultCreateContract.ts b/src/contract/deploy/impl/DefaultCreateContract.ts index c6ac856..3c404f3 100644 --- a/src/contract/deploy/impl/DefaultCreateContract.ts +++ b/src/contract/deploy/impl/DefaultCreateContract.ts @@ -1,21 +1,24 @@ /* eslint-disable */ import Arweave from 'arweave'; import Transaction from 'arweave/node/lib/transaction'; -import { Signature } from '../../../contract/Signature'; +import { Signature, SignatureType } from '../../../contract/Signature'; import { SmartWeaveTags } from '../../../core/SmartWeaveTags'; import { Warp } from '../../../core/Warp'; import { WARP_GW_URL } from '../../../core/WarpFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory'; -import { CreateContract, ContractData, ContractDeploy, FromSrcTxContractData } from '../CreateContract'; -import { SourceImpl } from './SourceImpl'; +import { CreateContract, ContractData, ContractDeploy, FromSrcTxContractData, ArWallet } from '../CreateContract'; +import { SourceData, SourceImpl } from './SourceImpl'; import { Buffer } from 'redstone-isomorphic'; export class DefaultCreateContract implements CreateContract { private readonly logger = LoggerFactory.INST.create('DefaultCreateContract'); + private readonly source: SourceImpl; + private signature: Signature; constructor(private readonly arweave: Arweave, private warp: Warp) { this.deployFromSourceTx = this.deployFromSourceTx.bind(this); + this.source = new SourceImpl(this.warp); } async deploy(contractData: ContractData, disableBundling?: boolean): Promise { @@ -24,9 +27,11 @@ export class DefaultCreateContract implements CreateContract { const effectiveUseBundler = disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling; - const source = new SourceImpl(this.warp); + const srcTx = await this.source.createSourceTx(contractData, wallet); + if (!effectiveUseBundler) { + await this.source.saveSourceTx(srcTx, true); + } - const srcTx = await source.save(contractData, this.warp.environment, wallet, effectiveUseBundler); this.logger.debug('Creating new contract'); return await this.deployFromSourceTx( @@ -94,7 +99,7 @@ export class DefaultCreateContract implements CreateContract { let responseOk: boolean; let response: { status: number; statusText: string; data: any }; if (effectiveUseBundler) { - const result = await this.post(contractTX, srcTx); + const result = await this.postContract(contractTX, srcTx); this.logger.debug(result); responseOk = true; } else { @@ -136,7 +141,15 @@ export class DefaultCreateContract implements CreateContract { } } - private async post(contractTx: Transaction, srcTx: Transaction = null): Promise { + async createSourceTx(sourceData: SourceData, wallet: ArWallet | SignatureType): Promise { + return this.source.createSourceTx(sourceData, wallet); + } + + async saveSourceTx(srcTx: Transaction, disableBundling?: boolean): Promise { + return this.source.saveSourceTx(srcTx, disableBundling); + } + + private async postContract(contractTx: Transaction, srcTx: Transaction = null): Promise { let body: any = { contractTx }; diff --git a/src/contract/deploy/impl/SourceImpl.ts b/src/contract/deploy/impl/SourceImpl.ts index d90e93f..4769833 100644 --- a/src/contract/deploy/impl/SourceImpl.ts +++ b/src/contract/deploy/impl/SourceImpl.ts @@ -8,8 +8,11 @@ import { SmartWeaveTags } from '../../../core/SmartWeaveTags'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { Source } from '../Source'; import { Buffer } from 'redstone-isomorphic'; -import { Warp, WarpEnvironment } from '../../../core/Warp'; +import { Warp } from '../../../core/Warp'; import { Signature, SignatureType } from '../../../contract/Signature'; +import Transaction from 'arweave/node/lib/transaction'; +import { WARP_GW_URL } from '../../../core/WarpFactory'; +import { TagsParser } from '../../../core/modules/impl/TagsParser'; const wasmTypeMapping: Map = new Map([ [1, 'assemblyscript'], @@ -31,21 +34,14 @@ export class SourceImpl implements Source { constructor(private readonly warp: Warp) {} - async save( - contractData: SourceData, - env: WarpEnvironment, - signature: ArWallet | SignatureType, - useBundler = false - ): Promise { + async createSourceTx(sourceData: SourceData, wallet: ArWallet | SignatureType): Promise { this.logger.debug('Creating new contract source'); - const { src, wasmSrcCodeDir, wasmGlueCode } = contractData; + const { src, wasmSrcCodeDir, wasmGlueCode } = sourceData; - this.signature = new Signature(this.warp, signature); + this.signature = new Signature(this.warp, wallet); const signer = this.signature.signer; - this.signature.checkNonArweaveSigningAvailability(useBundler); - const contractType: ContractType = src instanceof Buffer ? 'wasm' : 'js'; let srcTx; let wasmLang = null; @@ -122,7 +118,7 @@ export class SourceImpl implements Source { srcTx.addTag(SmartWeaveTags.WASM_META, JSON.stringify(metadata)); } - if (env === 'testnet') { + if (this.warp.environment === 'testnet') { srcTx.addTag(SmartWeaveTags.WARP_TESTNET, '1.0.0'); } @@ -130,17 +126,40 @@ export class SourceImpl implements Source { this.logger.debug('Posting transaction with source'); - // note: in case of useBundler = true, we're posting both - // src tx and contract tx in one request. - let responseOk = true; + return srcTx; + } + + async saveSourceTx(srcTx: Transaction, disableBundling: boolean = false): Promise { + this.logger.debug('Saving contract source', srcTx.id); + + if (this.warp.environment == 'local') { + disableBundling = true; + } + + const effectiveUseBundler = + disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling; + + const tagsParser = new TagsParser(); + const signatureTag = tagsParser.getTag(srcTx, SmartWeaveTags.SIGNATURE_TYPE); + + if (signatureTag && signatureTag != 'arweave' && !effectiveUseBundler) { + throw new Error(`Unable to save source with signature type: ${signatureTag} when bundling is disabled.`); + } + + let responseOk: boolean; let response: { status: number; statusText: string; data: any }; - if (!useBundler) { + + if (!disableBundling) { + const result = await this.postSource(srcTx); + this.logger.debug(result); + responseOk = true; + } else { response = await this.warp.arweave.transactions.post(srcTx); responseOk = response.status === 200 || response.status === 208; } if (responseOk) { - return srcTx; + return srcTx.id; } else { throw new Error( `Unable to write Contract Source. Arweave responded with status ${response.status}: ${response.statusText}` @@ -187,6 +206,26 @@ export class SourceImpl implements Source { return outputStreamBuffer.getContents(); } + + private async postSource(srcTx: Transaction = null): Promise { + const response = await fetch(`${WARP_GW_URL}/gateway/sources/deploy`, { + method: 'POST', + body: JSON.stringify({ srcTx }), + headers: { + 'Accept-Encoding': 'gzip, deflate, br', + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + if (response.ok) { + return response.json(); + } else { + throw new Error( + `Error while posting contract source. Sequencer responded with status ${response.status} ${response.statusText}` + ); + } + } } function dummyImports(moduleImports: WebAssembly.ModuleImportDescriptor[]) { diff --git a/src/core/Warp.ts b/src/core/Warp.ts index 5b744a3..19b722b 100644 --- a/src/core/Warp.ts +++ b/src/core/Warp.ts @@ -1,6 +1,12 @@ import Arweave from 'arweave'; import { Contract, InnerCallData } from '../contract/Contract'; -import { CreateContract } from '../contract/deploy/CreateContract'; +import { + ArWallet, + ContractData, + ContractDeploy, + CreateContract, + FromSrcTxContractData +} from '../contract/deploy/CreateContract'; import { DefaultCreateContract } from '../contract/deploy/impl/DefaultCreateContract'; import { HandlerBasedContract } from '../contract/HandlerBasedContract'; import { PstContract } from '../contract/PstContract'; @@ -15,6 +21,9 @@ import { WarpBuilder } from './WarpBuilder'; import { WarpPluginType, WarpPlugin, knownWarpPlugins } from './WarpPlugin'; import { SortKeyCache } from '../cache/SortKeyCache'; import { ContractDefinition } from './ContractDefinition'; +import { SignatureType } from '../contract/Signature'; +import { SourceData } from '../contract/deploy/impl/SourceImpl'; +import Transaction from 'arweave/node/lib/transaction'; export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom'; @@ -27,6 +36,9 @@ export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom'; * contract and perform operations on them (see {@link Contract}) */ export class Warp { + /** + * @deprecated createContract will be a private field, please use its methods directly e.g. await warp.deploy(...) + */ readonly createContract: CreateContract; readonly testing: Testing; @@ -61,6 +73,26 @@ export class Warp { return new HandlerBasedContract(contractTxId, this, callingContract, innerCallData); } + async deploy(contractData: ContractData, disableBundling?: boolean): Promise { + return await this.createContract.deploy(contractData, disableBundling); + } + + async deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise { + return await this.createContract.deployFromSourceTx(contractData, disableBundling); + } + + async deployBundled(rawDataItem: Buffer): Promise { + return await this.createContract.deployBundled(rawDataItem); + } + + async createSourceTx(sourceData: SourceData, wallet: ArWallet | SignatureType): Promise { + return await this.createContract.createSourceTx(sourceData, wallet); + } + + async saveSourceTx(srcTx: Transaction, disableBundling?: boolean): Promise { + return await this.createContract.saveSourceTx(srcTx, disableBundling); + } + /** * Allows to connect to a contract that conforms to the Profit Sharing Token standard * @param contractTxId diff --git a/tools/data/js/token-evolve.js b/tools/data/js/token-evolve.js new file mode 100644 index 0000000..fdf04d5 --- /dev/null +++ b/tools/data/js/token-evolve.js @@ -0,0 +1,66 @@ +export function handle(state, action) { + const balances = state.balances; + const canEvolve = state.canEvolve; + const input = action.input; + const caller = action.caller; + + if (input.function === 'transfer') { + const target = input.target; + const qty = input.qty; + + if (!Number.isInteger(qty)) { + throw new ContractError('Invalid value for "qty". Must be an integer'); + } + + if (!target) { + throw new ContractError('No target specified'); + } + + if (qty <= 0 || caller === target) { + throw new ContractError('Invalid token transfer'); + } + + if (balances[caller] < qty) { + throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`); + } + + // Lower the token balance of the caller + balances[caller] -= qty; + if (target in balances) { + // Wallet already exists in state, add new tokens + balances[target] += qty; + } else { + // Wallet is new, set starting balance + balances[target] = qty; + } + + return { state }; + } + + if (input.function === 'balance') { + const target = input.target; + const ticker = state.ticker; + + if (typeof target !== 'string') { + throw new ContractError('Must specify target to get balance for'); + } + + if (typeof balances[target] !== 'number') { + throw new ContractError('Cannot get balance, target does not exist'); + } + + return { result: { target, ticker, balance: balances[target] + 555 } }; + } + + if (input.function === 'evolve' && canEvolve) { + if (state.owner !== caller) { + throw new ContractError('Only the owner can evolve a contract.'); + } + + state.evolve = input.value; + + return { state }; + } + + throw new ContractError(`No function supplied or function not recognised: "${input.function}"`); +} diff --git a/tools/evolve.ts b/tools/evolve.ts new file mode 100644 index 0000000..480242c --- /dev/null +++ b/tools/evolve.ts @@ -0,0 +1,69 @@ +/* eslint-disable */ +import { defaultCacheOptions, LoggerFactory, WarpFactory } from '../src'; +import fs from 'fs'; +import path from 'path'; +import { JWKInterface } from 'arweave/node/lib/wallet'; + +async function main() { + let wallet: JWKInterface = readJSON('./.secrets/jwk.json'); + LoggerFactory.INST.logLevel('info'); + const logger = LoggerFactory.INST.create('evolve'); + + try { + const warp = WarpFactory.forMainnet({ ...defaultCacheOptions, inMemory: true }); + + const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.js'), 'utf8'); + const newJsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-evolve.js'), 'utf8'); + const owner = await warp.arweave.wallets.jwkToAddress(wallet); + const initialState = { + ticker: 'EXAMPLE_PST_TOKEN', + owner, + canEvolve: true, + balances: { + 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M': 10000000, + '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA': 23111222 + }, + wallets: {} + }; + + const { contractTxId } = await warp.deploy({ + wallet, + initState: JSON.stringify(initialState), + src: jsContractSrc + }); + + const contract = warp.contract(contractTxId).connect(wallet); + + const { result } = await contract.viewState({ + function: 'balance', + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M' + }); + + console.log('Original result', result); + + const srcTx = await warp.createSourceTx({ src: newJsContractSrc }, wallet); + const newSrcTxId = await warp.saveSourceTx(srcTx); + console.log('Save result', newSrcTxId); + await contract.evolve(newSrcTxId); + + const { result: evolvedResult } = await contract.viewState({ + function: 'balance', + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M' + }); + + console.log('Evolved result', evolvedResult); + } catch (e) { + logger.error(e); + } +} + +export function readJSON(path: string): JWKInterface { + const content = fs.readFileSync(path, 'utf-8'); + try { + return JSON.parse(content); + } catch (e) { + throw new Error(`File "${path}" does not contain a valid JSON`); + } +} + +main().catch((e) => console.error(e));