diff --git a/.gitignore b/.gitignore index b4fc739..52ad2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Dependency directories + node_modules/ # Optional eslint cache @@ -26,3 +27,4 @@ yalc.lock bundles/ .secrets +logs \ No newline at end of file diff --git a/src/__tests__/unit/signature.test.ts b/src/__tests__/unit/signature.test.ts index 0674d14..06c4be6 100644 --- a/src/__tests__/unit/signature.test.ts +++ b/src/__tests__/unit/signature.test.ts @@ -1,4 +1,4 @@ -import { Signature } from '../../contract/Signature'; +import { CustomSignature, Signature } from '../../contract/Signature'; import { defaultCacheOptions, WarpFactory } from '../../core/WarpFactory'; describe('Wallet', () => { @@ -152,4 +152,64 @@ describe('Wallet', () => { expect(sut.signer).toEqual(sampleFunction); }); }); + + describe('getAddress', () => { + + it('should getAddress for ArWallet signer', async () => { + const warp = WarpFactory.forMainnet(); + const arWallet = await warp.generateWallet(); + + const signature = new Signature(warp, arWallet.jwk); + + const address = await signature.getAddress(); + expect(address).toStrictEqual(arWallet.address); + }); + + it('should call getAddress for customSignature, if getAddress provided', async () => { + const warp = WarpFactory.forMainnet(); + const customSignature: CustomSignature = { + type: 'ethereum', + signer: sampleFunction, + getAddress: () => Promise.resolve("owner") + } + + const signature = new Signature(warp, customSignature); + + const address = await signature.getAddress(); + expect(address).toStrictEqual("owner"); + }); + + it('should call getAddress for customSignature, if getAddress NOT provided', async () => { + const warp = WarpFactory.forMainnet(); + const customSignature: CustomSignature = { + type: 'ethereum', + signer: async (tx) => { tx.owner = "owner" }, + } + + const signature = new Signature(warp, customSignature); + + const address = await signature.getAddress(); + expect(address).toStrictEqual("owner"); + }); + + it('should use cached valued from getAddress', async () => { + const warp = WarpFactory.forMainnet(); + const mockedSigner = jest.fn(async (tx) => { tx.owner = "owner" }); + const customSignature: CustomSignature = { + type: 'ethereum', + signer: mockedSigner, + } + + const signature = new Signature(warp, customSignature); + + const address = await signature.getAddress(); + expect(address).toStrictEqual("owner"); + + const cachedAddress = await signature.getAddress(); + expect(cachedAddress).toStrictEqual("owner"); + + expect(mockedSigner).toBeCalledTimes(1); + }); + + }); }); diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 10a536b..78c9311 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -668,15 +668,7 @@ export class HandlerBasedContract implements Contract { if (caller) { effectiveCaller = caller; } else if (this.signature) { - // we're creating this transaction just to call the signing function on it - // - and retrieve the caller/owner - const dummyTx = await arweave.createTransaction({ - data: Math.random().toString().slice(-4), - reward: '72600854', - last_tx: 'p7vc1iSP6bvH_fCeUFa9LqoV5qiyW-jdEKouAT0XMoSwrNraB9mgpi29Q10waEpO' - }); - await this.signature.signer(dummyTx); - effectiveCaller = await arweave.wallets.ownerToAddress(dummyTx.owner); + effectiveCaller = await this.signature.getAddress(); } else { effectiveCaller = ''; } diff --git a/src/contract/Signature.ts b/src/contract/Signature.ts index 5765ca8..d61660b 100644 --- a/src/contract/Signature.ts +++ b/src/contract/Signature.ts @@ -1,11 +1,15 @@ import { Warp } from '../core/Warp'; import { ArWallet } from './deploy/CreateContract'; import { Transaction } from '../utils/types/arweave-types'; -import { Signer } from './deploy/DataItem'; +import { BundlerSigner } from './deploy/DataItem'; export type SignatureType = 'arweave' | 'ethereum'; export type SigningFunction = (tx: Transaction) => Promise; -export type CustomSignature = { signer: SigningFunction; type: SignatureType }; +export type CustomSignature = { + signer: SigningFunction; + type: SignatureType; + getAddress?: () => Promise; +}; /** Different types which can be used to sign transaction or data item @@ -15,22 +19,79 @@ Different types which can be used to sign transaction or data item - Signer - arbundles specific class which allows to sign data items (only this type can be used when bundling is enabled and data items are being created) */ -export type SignatureProvider = ArWallet | CustomSignature | Signer; +export type SignatureProvider = ArWallet | CustomSignature | BundlerSigner; export class Signature { signer: SigningFunction; - type: SignatureType; + readonly type: SignatureType; readonly warp: Warp; + private readonly signatureProviderType: 'CustomSignature' | 'ArWallet' | 'BundlerSigner'; + private readonly wallet; + private cachedAddress?: string; - constructor(warp: Warp, walletOrSignature: ArWallet | CustomSignature) { + constructor(warp: Warp, walletOrSignature: SignatureProvider) { this.warp = warp; if (this.isCustomSignature(walletOrSignature)) { - this.assertEnvForCustomSigner(walletOrSignature); + this.assertEnvForCustomSigner(walletOrSignature.type); this.signer = walletOrSignature.signer; this.type = walletOrSignature.type; + this.signatureProviderType = 'CustomSignature'; + } else if (this.isValidBundlerSignature(walletOrSignature)) { + this.signatureProviderType = 'BundlerSigner'; + this.type = decodeBundleSignatureType(walletOrSignature.signatureType); } else { - this.assignDefaultSigner(walletOrSignature); + this.assignArweaveSigner(walletOrSignature); + this.signatureProviderType = 'ArWallet'; + this.type = 'arweave'; + } + this.wallet = walletOrSignature; + } + + async getAddress(): Promise { + if (this.cachedAddress) { + return this.cachedAddress; + } + + switch (this.signatureProviderType) { + case 'CustomSignature': { + if (this.wallet.getAddress) { + this.cachedAddress = await this.wallet.getAddress(); + } else { + this.cachedAddress = await this.deduceSignerBySigning(); + } + return this.cachedAddress; + } + case 'ArWallet': { + this.cachedAddress = await this.deduceSignerBySigning(); + return this.cachedAddress; + } + case 'BundlerSigner': { + // If we can parse publicKey to `signatureType` address, we don't have to call it + this.cachedAddress = await this.deduceSignerBySigning(); + return this.cachedAddress; + } + default: + throw Error('Unknown Signature::signatureProvider : ' + this.signatureProviderType); + } + } + + private async deduceSignerBySigning() { + const { arweave } = this.warp; + + const dummyTx = await arweave.createTransaction({ + data: Math.random().toString().slice(-4), + reward: '72600854', + last_tx: 'p7vc1iSP6bvH_fCeUFa9LqoV5qiyW-jdEKouAT0XMoSwrNraB9mgpi29Q10waEpO' + }); + await this.signer(dummyTx); + + if (this.type === 'ethereum') { + return dummyTx.owner; + } else if (this.type === 'arweave') { + return arweave.wallets.ownerToAddress(dummyTx.owner); + } else { + throw Error('Unknown Signature::type'); } } @@ -40,25 +101,51 @@ export class Signature { } } - private assignDefaultSigner(walletOrSignature) { + private assignArweaveSigner(walletOrSignature) { this.signer = async (tx: Transaction) => { await this.warp.arweave.transactions.sign(tx, walletOrSignature); }; - this.type = 'arweave'; } - private assertEnvForCustomSigner(walletOrSignature: CustomSignature) { + private assertEnvForCustomSigner(signatureType: SignatureType) { if ( - walletOrSignature.type !== 'arweave' && + signatureType !== '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.` + `Unable to use signing function of type: ${signatureType} when not in mainnet environment or bundling is disabled.` ); } } - private isCustomSignature(signature: ArWallet | CustomSignature): signature is CustomSignature { + private isCustomSignature(signature: SignatureProvider): signature is CustomSignature { return (signature as CustomSignature).signer !== undefined; } + + private isValidBundlerSignature(signature: SignatureProvider): signature is BundlerSigner { + const bundlerSignature = signature as BundlerSigner; + + // "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck" + const isBundlerSignature = + !!bundlerSignature.signatureType && !!bundlerSignature.ownerLength && !!bundlerSignature.signatureLength; + + if (isBundlerSignature && !bundlerSignature.publicKey) { + throw new Error( + `It seems that you are using BundlerSigner, but publicKey is not set! Maybe try calling await bundlerSigner.setPublicKey() before using it.` + ); + } + + return isBundlerSignature; + } +} + +function decodeBundleSignatureType(bundlerSignatureType: BundlerSigner['signatureType']): SignatureType { + // enum: https://github.com/Bundlr-Network/arbundles/blob/9fafdbfec6fbfcbcb538b92ae9bd0d9fbe413fb8/src/constants.ts#L1 + if (bundlerSignatureType === 3) { + return 'ethereum'; + } else if (bundlerSignatureType === 1) { + return 'arweave'; + } else { + throw Error(`Not supported arbundle SignatureType : ${bundlerSignatureType}`); + } } diff --git a/src/contract/deploy/CreateContract.ts b/src/contract/deploy/CreateContract.ts index 981e610..77df7f1 100644 --- a/src/contract/deploy/CreateContract.ts +++ b/src/contract/deploy/CreateContract.ts @@ -2,7 +2,7 @@ import { JWKInterface } from 'arweave/node/lib/wallet'; import { WarpPluginType } from '../../core/WarpPlugin'; import { EvaluationOptions } from '../../core/modules/StateEvaluator'; import { Source } from './Source'; -import { Signer } from './DataItem'; +import { BundlerSigner } from './DataItem'; import { CustomSignature } from 'contract/Signature'; export type Tags = { name: string; value: string }[]; @@ -30,7 +30,7 @@ export const BUNDLR_NODES = ['node1', 'node2'] as const; export type BundlrNodeType = (typeof BUNDLR_NODES)[number]; export interface CommonContractData { - wallet: ArWallet | CustomSignature | Signer; + wallet: ArWallet | CustomSignature | BundlerSigner; initState: string; tags?: Tags; transfer?: ArTransfer; diff --git a/src/contract/deploy/DataItem.ts b/src/contract/deploy/DataItem.ts index 30c212b..9b5d0df 100644 --- a/src/contract/deploy/DataItem.ts +++ b/src/contract/deploy/DataItem.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -export declare abstract class Signer { +export declare abstract class BundlerSigner { readonly publicKey: Buffer; readonly signatureType: number; readonly signatureLength: number; @@ -28,7 +28,7 @@ export abstract class DataItem { readonly tags: ResolvesTo<{ name: string; value: string }[]>; readonly rawData: ResolvesTo; readonly data: ResolvesTo; - abstract sign(signer: Signer): Promise; + abstract sign(signer: BundlerSigner): Promise; abstract isValid(): Promise; static async verify(..._: any[]): Promise { throw new Error('You must implement `verify`'); diff --git a/src/contract/deploy/Source.ts b/src/contract/deploy/Source.ts index 9ccd5f5..be95444 100644 --- a/src/contract/deploy/Source.ts +++ b/src/contract/deploy/Source.ts @@ -1,7 +1,7 @@ import { ArWallet } from './CreateContract'; import { CustomSignature } from '../../contract/Signature'; import { Transaction } from '../../utils/types/arweave-types'; -import { Signer, DataItem } from './DataItem'; +import { BundlerSigner, DataItem } from './DataItem'; export interface SourceData { src: string | Buffer; @@ -23,7 +23,7 @@ export interface Source { */ createSource( sourceData: SourceData, - wallet: ArWallet | CustomSignature | Signer, + wallet: ArWallet | CustomSignature | BundlerSigner, disableBundling?: boolean ): Promise; diff --git a/src/core/Warp.ts b/src/core/Warp.ts index 81ad540..d5fd574 100644 --- a/src/core/Warp.ts +++ b/src/core/Warp.ts @@ -32,7 +32,7 @@ import { Transaction } from '../utils/types/arweave-types'; import { DEFAULT_LEVEL_DB_LOCATION } from './WarpFactory'; import { LevelDbCache } from '../cache/impl/LevelDbCache'; import { SourceData } from '../contract/deploy/Source'; -import { Signer, DataItem } from '../contract/deploy/DataItem'; +import { BundlerSigner, DataItem } from '../contract/deploy/DataItem'; export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom'; export type KVStorageFactory = (contractTxId: string) => SortKeyCache; @@ -118,7 +118,7 @@ export class Warp { async createSource( sourceData: SourceData, - wallet: ArWallet | CustomSignature | Signer + wallet: ArWallet | CustomSignature | BundlerSigner ): Promise { return await this.createContract.createSource(sourceData, wallet); }