diff --git a/src/__tests__/unit/signature.test.ts b/src/__tests__/unit/signature.test.ts new file mode 100644 index 0000000..0674d14 --- /dev/null +++ b/src/__tests__/unit/signature.test.ts @@ -0,0 +1,155 @@ +import { Signature } from '../../contract/Signature'; +import { defaultCacheOptions, WarpFactory } from '../../core/WarpFactory'; + +describe('Wallet', () => { + const sampleFunction = async () => { + setTimeout(() => { + //test + }, 1000); + }; + + const signingFunction = `async (tx) => {await this.warp.arweave.transactions.sign(tx, walletOrSignature);}`.replace( + /\s+/g, + '' + ); + + describe('in local environment', () => { + const warp = WarpFactory.forLocal(); + + it(`should set correct signature for 'use_wallet'`, () => { + const sut = new Signature(warp, 'use_wallet'); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for jwk`, () => { + const sut = new Signature(warp, { + kty: '', + e: '', + n: '' + }); + + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for custom signing function and arweave signature type`, () => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'arweave' }); + + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(sampleFunction.toString().replace(/\s+/g, '')); + expect(sut.type).toEqual('arweave'); + }); + + it(`should throw for custom signing function and ethereum signature type`, () => { + expect(() => { + new Signature(warp, { signer: sampleFunction, type: 'ethereum' }); + }).toThrow( + `Unable to use signing function of type: ethereum when not in mainnet environment or bundling is disabled.` + ); + }); + }); + + describe('in testnet environment', () => { + const warp = WarpFactory.forTestnet(); + + it(`should set correct signature for 'use_wallet'`, () => { + const sut = new Signature(warp, 'use_wallet'); + + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for jwk`, () => { + const sut = new Signature(warp, { + kty: '', + e: '', + n: '' + }); + + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for custom signing function and arweave signature type`, () => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'arweave' }); + + expect(sut.signer).toEqual(sampleFunction); + expect(sut.type).toEqual('arweave'); + }); + + it(`should throw for custom signing function and arweave signature type`, () => { + expect(() => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'ethereum' }); + }).toThrow( + `Unable to use signing function of type: ethereum when not in mainnet environment or bundling is disabled.` + ); + }); + }); + + describe('in mainnet environment when bundling is disabled', () => { + const warp = WarpFactory.forMainnet(defaultCacheOptions, true); + + it(`should set correct signature for 'use_wallet'`, () => { + const sut = new Signature(warp, 'use_wallet'); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for jwk`, () => { + const sut = new Signature(warp, { + kty: '', + e: '', + n: '' + }); + + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for custom signing function and arweave signature type`, () => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'arweave' }); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(sampleFunction.toString().replace(/\s+/g, '')); + expect(sut.type).toEqual('arweave'); + }); + + it(`should throw for custom signing function and arweave signature type`, () => { + expect(() => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'ethereum' }); + }).toThrow( + `Unable to use signing function of type: ethereum when not in mainnet environment or bundling is disabled.` + ); + }); + }); + + describe('in mainnet environment when bundling is enabled', () => { + const warp = WarpFactory.forMainnet(); + + it(`should set correct signature for 'use_wallet'`, () => { + const sut = new Signature(warp, 'use_wallet'); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for jwk`, () => { + const sut = new Signature(warp, { + kty: '', + e: '', + n: '' + }); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(signingFunction); + expect(sut.type).toStrictEqual('arweave'); + }); + + it(`should set correct signature for custom signing function and arweave signature type`, () => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'arweave' }); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(sampleFunction.toString().replace(/\s+/g, '')); + expect(sut.type).toEqual('arweave'); + }); + + it(`should set correct signature for custom signing function and ethereum signature type`, () => { + const sut = new Signature(warp, { signer: sampleFunction, type: 'ethereum' }); + expect(sut.signer.toString().replace(/\s+/g, '')).toEqual(sampleFunction.toString().replace(/\s+/g, '')); + expect(sut.signer).toEqual(sampleFunction); + }); + }); +}); diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 8e3fe98..d9fcdd3 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -1,4 +1,3 @@ -import Transaction from 'arweave/node/lib/transaction'; import { SortKeyCacheResult } from '../cache/SortKeyCache'; import { ContractCallRecord } from '../core/ContractCallRecord'; import { InteractionResult } from '../core/modules/impl/HandlerExecutorFactory'; @@ -6,12 +5,11 @@ import { EvaluationOptions, EvalStateResult } from '../core/modules/StateEvaluat 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 }; export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number }; -export type SigningFunction = (tx: Transaction) => Promise; -export type Signature = { signer: SigningFunction; signatureType: 'arweave' | 'ethereum' }; export class ContractError extends Error { constructor(message) { super(message); @@ -85,7 +83,7 @@ export interface Contract extends Source { * * @param signer - either {@link ArWallet} that will be connected to this contract or custom {@link SigningFunction} */ - connect(signature: ArWallet | Signature): Contract; + connect(signature: ArWallet | SignatureType): Contract; /** * Allows to set ({@link EvaluationOptions}) diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index accc1cb..eb1055b 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -1,7 +1,6 @@ import { TransactionStatusResponse } from 'arweave/node/transactions'; import stringify from 'safe-stable-stringify'; import * as crypto from 'crypto'; -import Transaction from 'arweave/node/lib/transaction'; import { SortKeyCacheResult } from '../cache/SortKeyCache'; import { ContractCallRecord, InteractionCall } from '../core/ContractCallRecord'; import { ExecutionContext } from '../core/ExecutionContext'; @@ -29,13 +28,13 @@ import { CurrentTx, WriteInteractionOptions, WriteInteractionResponse, - InnerCallData, - Signature + InnerCallData } from './Contract'; import { Tags, ArTransfer, emptyTransfer, ArWallet } from './deploy/CreateContract'; import { SourceData, SourceImpl } from './deploy/impl/SourceImpl'; import { InnerWritesEvaluator } from './InnerWritesEvaluator'; import { generateMockVrf } from '../utils/vrf'; +import { Signature, SignatureType } from './Signature'; /** * An implementation of {@link Contract} that is backwards compatible with current style @@ -54,11 +53,7 @@ export class HandlerBasedContract implements Contract { private readonly _arweaveWrapper: ArweaveWrapper; private _sorter: InteractionsSorter; private _rootSortKey: string; - - /** - * wallet connected to this contract - */ - protected signature?: Signature; + private signature: Signature; constructor( private readonly _contractTxId: string, @@ -229,11 +224,7 @@ export class HandlerBasedContract implements Contract { const bundleInteraction = interactionsLoader.type() == 'warp' && !effectiveDisableBundling; - if (this.signature.signatureType !== 'arweave' && !bundleInteraction) { - throw new Error( - `Unable to use signing function of type: ${this.signature.signatureType} when not in mainnet environment or bundling is disabled.` - ); - } + this.signature.checkNonArweaveSigningAvailability(bundleInteraction); if ( bundleInteraction && @@ -404,29 +395,8 @@ export class HandlerBasedContract implements Contract { return this._callStack; } - connect(signature: ArWallet | Signature): Contract { - if (this.isSignatureType(signature)) { - if ( - signature.signatureType !== 'arweave' && - (!(this.warp.environment == 'mainnet') || !(this.warp.interactionsLoader.type() == 'warp')) - ) { - throw new Error( - `Unable to use signing function of type: ${signature.signatureType} when not in mainnet environment or bundling is disabled.` - ); - } else { - this.signature = { - signer: signature.signer, - signatureType: signature.signatureType - }; - } - } else { - this.signature = { - signer: async (tx: Transaction) => { - await this.warp.arweave.transactions.sign(tx, signature); - }, - signatureType: 'arweave' - }; - } + connect(signature: ArWallet | SignatureType): Contract { + this.signature = new Signature(this.warp, signature); return this; } @@ -788,10 +758,9 @@ export class HandlerBasedContract implements Contract { if (!this.signature) { throw new Error("Wallet not connected. Use 'connect' method first."); } - const { arweave } = this.warp; - const source = new SourceImpl(arweave); + const source = new SourceImpl(this.warp); - const srcTx = await source.save(sourceData, this.warp.environment, this.signature.signer); + const srcTx = await source.save(sourceData, this.warp.environment, this.signature); return srcTx.id; } @@ -799,8 +768,4 @@ export class HandlerBasedContract implements Contract { get rootSortKey(): string { return this._rootSortKey; } - - private isSignatureType(signature: ArWallet | Signature): signature is Signature { - return (signature as Signature).signer !== undefined; - } } diff --git a/src/contract/Signature.ts b/src/contract/Signature.ts new file mode 100644 index 0000000..c9975ff --- /dev/null +++ b/src/contract/Signature.ts @@ -0,0 +1,45 @@ +import Transaction from 'arweave/node/lib/transaction'; +import { Warp } from 'core/Warp'; +import { ArWallet } from './deploy/CreateContract'; + +export type SigningFunction = (tx: Transaction) => Promise; +export type SignatureType = { signer: SigningFunction; type: 'arweave' | 'ethereum' }; + +export class Signature { + readonly signer: SigningFunction; + readonly type: 'arweave' | 'ethereum'; + readonly warp: Warp; + + constructor(warp: Warp, walletOrSignature: ArWallet | SignatureType) { + this.warp = warp; + + if (this.isSignatureType(walletOrSignature)) { + if ( + walletOrSignature.type !== 'arweave' && + (!(this.warp.environment == 'mainnet') || !(this.warp.interactionsLoader.type() == 'warp')) + ) { + throw new Error( + `Unable to use signing function of type: ${walletOrSignature.type} when not in mainnet environment or bundling is disabled.` + ); + } else { + this.signer = walletOrSignature.signer; + this.type = walletOrSignature.type; + } + } else { + this.signer = async (tx: Transaction) => { + await this.warp.arweave.transactions.sign(tx, walletOrSignature); + }; + this.type = 'arweave'; + } + } + + checkNonArweaveSigningAvailability(bundling: boolean): void { + if (this.type !== 'arweave' && !bundling) { + throw new Error(`Unable to use signing function of type: ${this.type} when bundling is disabled.`); + } + } + + private isSignatureType(signature: ArWallet | SignatureType): signature is SignatureType { + return (signature as SignatureType).signer !== undefined; + } +} diff --git a/src/contract/deploy/CreateContract.ts b/src/contract/deploy/CreateContract.ts index 8b9da69..1776c3a 100644 --- a/src/contract/deploy/CreateContract.ts +++ b/src/contract/deploy/CreateContract.ts @@ -1,4 +1,5 @@ import { JWKInterface } from 'arweave/node/lib/wallet'; +import { SignatureType } from '../../contract/Signature'; export type Tags = { name: string; value: string }[]; @@ -17,7 +18,7 @@ export const emptyTransfer: ArTransfer = { }; export interface CommonContractData { - wallet: ArWallet; + wallet: ArWallet | SignatureType; initState: string; tags?: Tags; transfer?: ArTransfer; diff --git a/src/contract/deploy/Source.ts b/src/contract/deploy/Source.ts index 149dadf..cebb5a1 100644 --- a/src/contract/deploy/Source.ts +++ b/src/contract/deploy/Source.ts @@ -1,7 +1,7 @@ -import { SigningFunction } from '../../contract/Contract'; import { ArWallet } from './CreateContract'; import { SourceData } from './impl/SourceImpl'; import { WarpEnvironment } from '../../core/Warp'; +import { SignatureType } from '../../contract/Signature'; export interface Source { /** @@ -11,7 +11,7 @@ export interface Source { save( contractSource: SourceData, env: WarpEnvironment, - signer?: ArWallet | SigningFunction, + signer?: ArWallet | SignatureType, useBundler?: boolean ): Promise; } diff --git a/src/contract/deploy/impl/DefaultCreateContract.ts b/src/contract/deploy/impl/DefaultCreateContract.ts index 35b3b71..c6ac856 100644 --- a/src/contract/deploy/impl/DefaultCreateContract.ts +++ b/src/contract/deploy/impl/DefaultCreateContract.ts @@ -1,6 +1,7 @@ /* eslint-disable */ import Arweave from 'arweave'; import Transaction from 'arweave/node/lib/transaction'; +import { Signature } from '../../../contract/Signature'; import { SmartWeaveTags } from '../../../core/SmartWeaveTags'; import { Warp } from '../../../core/Warp'; import { WARP_GW_URL } from '../../../core/WarpFactory'; @@ -11,6 +12,7 @@ import { Buffer } from 'redstone-isomorphic'; export class DefaultCreateContract implements CreateContract { private readonly logger = LoggerFactory.INST.create('DefaultCreateContract'); + private signature: Signature; constructor(private readonly arweave: Arweave, private warp: Warp) { this.deployFromSourceTx = this.deployFromSourceTx.bind(this); @@ -22,7 +24,7 @@ export class DefaultCreateContract implements CreateContract { const effectiveUseBundler = disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling; - const source = new SourceImpl(this.arweave); + const source = new SourceImpl(this.warp); const srcTx = await source.save(contractData, this.warp.environment, wallet, effectiveUseBundler); this.logger.debug('Creating new contract'); @@ -48,22 +50,23 @@ export class DefaultCreateContract implements CreateContract { ): Promise { this.logger.debug('Creating new contract from src tx'); const { wallet, srcTxId, initState, tags, transfer, data } = contractData; + this.signature = new Signature(this.warp, wallet); + const signer = this.signature.signer; const effectiveUseBundler = disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling; - let contractTX = await this.arweave.createTransaction({ data: data?.body || initState }, wallet); + this.signature.checkNonArweaveSigningAvailability(effectiveUseBundler); + + let contractTX = await this.arweave.createTransaction({ data: data?.body || initState }); if (+transfer?.winstonQty > 0 && transfer.target.length) { this.logger.debug('Creating additional transaction with AR transfer', transfer); - contractTX = await this.arweave.createTransaction( - { - data: data?.body || initState, - target: transfer.target, - quantity: transfer.winstonQty - }, - wallet - ); + contractTX = await this.arweave.createTransaction({ + data: data?.body || initState, + target: transfer.target, + quantity: transfer.winstonQty + }); } if (tags?.length) { @@ -86,7 +89,7 @@ export class DefaultCreateContract implements CreateContract { contractTX.addTag(SmartWeaveTags.WARP_TESTNET, '1.0.0'); } - await this.arweave.transactions.sign(contractTX, wallet); + await signer(contractTX); let responseOk: boolean; let response: { status: number; statusText: string; data: any }; diff --git a/src/contract/deploy/impl/SourceImpl.ts b/src/contract/deploy/impl/SourceImpl.ts index 48845e1..d90e93f 100644 --- a/src/contract/deploy/impl/SourceImpl.ts +++ b/src/contract/deploy/impl/SourceImpl.ts @@ -1,16 +1,15 @@ /* eslint-disable */ import metering from 'redstone-wasm-metering'; -import Arweave from 'arweave'; import { Go } from '../../../core/modules/impl/wasm/go-wasm-imports'; import fs, { PathOrFileDescriptor } from 'fs'; import { matchMutClosureDtor } from '../../../core/modules/impl/wasm/wasm-bindgen-tools'; import { ArWallet, ContractType } from '../CreateContract'; -import { SigningFunction } from '../../../contract/Contract'; import { SmartWeaveTags } from '../../../core/SmartWeaveTags'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { Source } from '../Source'; import { Buffer } from 'redstone-isomorphic'; -import { WarpEnvironment } from '../../../core/Warp'; +import { Warp, WarpEnvironment } from '../../../core/Warp'; +import { Signature, SignatureType } from '../../../contract/Signature'; const wasmTypeMapping: Map = new Map([ [1, 'assemblyscript'], @@ -28,18 +27,25 @@ export interface SourceData { export class SourceImpl implements Source { private readonly logger = LoggerFactory.INST.create('Source'); - constructor(private readonly arweave: Arweave) {} + private signature: Signature; + + constructor(private readonly warp: Warp) {} async save( contractData: SourceData, env: WarpEnvironment, - signer: ArWallet | SigningFunction, + signature: ArWallet | SignatureType, useBundler = false ): Promise { this.logger.debug('Creating new contract source'); const { src, wasmSrcCodeDir, wasmGlueCode } = contractData; + this.signature = new Signature(this.warp, signature); + const signer = this.signature.signer; + + this.signature.checkNonArweaveSigningAvailability(useBundler); + const contractType: ContractType = src instanceof Buffer ? 'wasm' : 'js'; let srcTx; let wasmLang = null; @@ -102,11 +108,7 @@ export class SourceImpl implements Source { const allData = contractType == 'wasm' ? this.joinBuffers(data) : src; - if (typeof signer == 'function') { - srcTx = await this.arweave.createTransaction({ data: allData }); - } else { - srcTx = await this.arweave.createTransaction({ data: allData }, signer); - } + srcTx = await this.warp.arweave.createTransaction({ data: allData }); srcTx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContractSource'); // TODO: version should be taken from the current package.json version. @@ -124,11 +126,8 @@ export class SourceImpl implements Source { srcTx.addTag(SmartWeaveTags.WARP_TESTNET, '1.0.0'); } - if (typeof signer == 'function') { - await signer(srcTx); - } else { - await this.arweave.transactions.sign(srcTx, signer); - } + await signer(srcTx); + this.logger.debug('Posting transaction with source'); // note: in case of useBundler = true, we're posting both @@ -136,7 +135,7 @@ export class SourceImpl implements Source { let responseOk = true; let response: { status: number; statusText: string; data: any }; if (!useBundler) { - response = await this.arweave.transactions.post(srcTx); + response = await this.warp.arweave.transactions.post(srcTx); responseOk = response.status === 200 || response.status === 208; } diff --git a/src/legacy/create-interaction-tx.ts b/src/legacy/create-interaction-tx.ts index 449ca61..d5c6537 100644 --- a/src/legacy/create-interaction-tx.ts +++ b/src/legacy/create-interaction-tx.ts @@ -4,8 +4,8 @@ import { CreateTransactionInterface } from 'arweave/node/common'; import { BlockData } from 'arweave/node/blocks'; import { SmartWeaveTags } from '../core/SmartWeaveTags'; import { GQLNodeInterface } from './gqlResult'; -import { SigningFunction } from '../contract/Contract'; import { TagsParser } from '../core/modules/impl/TagsParser'; +import { SigningFunction } from '../contract/Signature'; export async function createInteractionTx( arweave: Arweave,