feat: Add optional getAddress method to Signature

This commit is contained in:
Michał Konopka
2023-03-02 23:08:56 +01:00
committed by Mike
parent f25ac04a65
commit 618ea26fa3
8 changed files with 172 additions and 31 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# Dependency directories
node_modules/
# Optional eslint cache
@@ -26,3 +27,4 @@ yalc.lock
bundles/
.secrets
logs

View File

@@ -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);
});
});
});

View File

@@ -668,15 +668,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
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 = '';
}

View File

@@ -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<void>;
export type CustomSignature = { signer: SigningFunction; type: SignatureType };
export type CustomSignature = {
signer: SigningFunction;
type: SignatureType;
getAddress?: () => Promise<string>;
};
/**
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<string> {
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}`);
}
}

View File

@@ -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;

View File

@@ -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<Buffer>;
readonly data: ResolvesTo<string>;
abstract sign(signer: Signer): Promise<Buffer>;
abstract sign(signer: BundlerSigner): Promise<Buffer>;
abstract isValid(): Promise<boolean>;
static async verify(..._: any[]): Promise<boolean> {
throw new Error('You must implement `verify`');

View File

@@ -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<DataItem | Transaction>;

View File

@@ -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<unknown>;
@@ -118,7 +118,7 @@ export class Warp {
async createSource(
sourceData: SourceData,
wallet: ArWallet | CustomSignature | Signer
wallet: ArWallet | CustomSignature | BundlerSigner
): Promise<Transaction | DataItem> {
return await this.createContract.createSource(sourceData, wallet);
}