Skip to content

Add Ethereum wallet support #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
"@ardrive/ardrive-promise-cache": "^1.1.4",
"@ardrive/turbo-sdk": "^1.0.1",
"@dha-team/arbundles": "^1.0.3",
"arweave": "^1.15.7",
"arweave": "2.0.0-ec.2",
"axios": "^0.21.1",
"axios-retry": "^3.6.0",
"base64-js": "^1.5.1",
"bignumber.js": "^9.1.2",
"bn.js": "^5.2.1",
"elliptic": "^6.6.1",
"futoin-hkdf": "^1.5.3",
"human-crypto-keys": "git+https://github.com/ardriveapp/js-human-crypto-keys.git#expose_lib",
"jwk-to-pem": "^2.0.5",
Expand All @@ -31,6 +32,7 @@
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/bn.js": "^5.1.2",
"@types/chai": "^4.3.6",
"@types/elliptic": "^6",
"@types/human-crypto-keys": "^0.1.2",
"@types/jwk-to-pem": "^2.0.0",
"@types/lodash": "^4",
Expand Down
5 changes: 2 additions & 3 deletions src/arfs/arfsdao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ import {
parseDriveSignatureType
} from '../exports';
import { Turbo } from './turbo';
import { ArweaveSigner } from '@dha-team/arbundles';
import { InvalidFileStateException } from '../types/exceptions';

/** Utility class for holding the driveId and driveKey of a new drive */
Expand Down Expand Up @@ -257,7 +256,7 @@ export class ArFSDAO extends ArFSDAOAnonymous {
protected gatewayApi = new GatewayAPI({ gatewayUrl: gatewayUrlForArweave(arweave) }),
protected txPreparer = new TxPreparer({
arweave: arweave,
wallet: wallet as JWKWallet,
wallet: wallet,
arFSTagAssembler: new ArFSTagAssembler(arFSTagSettings)
}),
protected turbo = new Turbo({
Expand Down Expand Up @@ -436,7 +435,7 @@ export class ArFSDAO extends ArFSDAOAnonymous {
}

private async makeBdi(dataItems: DataItem[]): Promise<DataItem> {
const signer = new ArweaveSigner((this.wallet as JWKWallet).getPrivateKey());
const signer = this.wallet.getSigner();
const bundle = await bundleAndSignData(dataItems, signer);
const bdi = createData(bundle.getRaw(), signer, {
// baseBundleTags includes: Bundle Format/Version, App Name/Version
Expand Down
14 changes: 7 additions & 7 deletions src/arfs/tx/tx_preparer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { bundleAndSignData, createData, DataItem, ArweaveSigner } from '@dha-team/arbundles';
import { bundleAndSignData, createData, DataItem, Signer } from '@dha-team/arbundles';
import Arweave from 'arweave';
import Transaction from 'arweave/node/lib/transaction';
import { GQLTagInterface } from '../../exports';
import { JWKWallet } from '../../jwk_wallet';
import { Wallet } from '../../wallet';
import { ArFSTagAssembler } from '../tags/tag_assembler';
import { ArFSObjectTransactionData } from './arfs_tx_data_types';

Expand All @@ -19,15 +19,15 @@ import {

export class TxPreparer {
private readonly arweave: Arweave;
private readonly wallet: JWKWallet;
private readonly signer: ArweaveSigner;
private readonly wallet: Wallet;
private readonly signer: Signer;
private readonly tagAssembler: ArFSTagAssembler;

public constructor({ arweave, wallet, arFSTagAssembler }: TxPreparerParams) {
this.arweave = arweave;
this.wallet = wallet;
this.tagAssembler = arFSTagAssembler;
this.signer = new ArweaveSigner(this.wallet.getPrivateKey());
this.signer = wallet.getSigner();
}

public async prepareFileDataDataItem({ objectMetaData }: ArFSPrepareFileDataItemParams): Promise<DataItem> {
Expand Down Expand Up @@ -125,13 +125,13 @@ export class TxPreparer {
}

private async createAndSignTx(txAttributes: TxAttributesToAssemble, tags: GQLTagInterface[]): Promise<Transaction> {
const transaction = await this.arweave.createTransaction(txAttributes.assemble(), this.wallet.getPrivateKey());
const transaction = await this.arweave.createTransaction(txAttributes.assemble());

for (const tag of tags) {
transaction.addTag(tag.name, tag.value);
}

await this.arweave.transactions.sign(transaction, this.wallet.getPrivateKey());
await this.wallet.signTransaction(transaction);

return transaction;
}
Expand Down
4 changes: 2 additions & 2 deletions src/arfs/tx/tx_preparer_types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataItem } from '@dha-team/arbundles';
import Arweave from 'arweave';
import { CommunityTipSettings, RewardSettings, GQLTagInterface } from '../../exports';
import { JWKWallet } from '../../jwk_wallet';
import { Wallet } from '../../wallet';
import { ArFSTagAssembler } from '../tags/tag_assembler';
import { ArFSObjectMetadataPrototype, ArFSFileDataPrototype, ArFSEntityMetaDataPrototype } from './arfs_prototypes';

Expand Down Expand Up @@ -35,6 +35,6 @@ export type PrepareTxParams<T = string | Buffer> = { data: T; tags: GQLTagInterf

export interface TxPreparerParams {
arweave: Arweave;
wallet: JWKWallet;
wallet: Wallet;
arFSTagAssembler: ArFSTagAssembler;
}
91 changes: 91 additions & 0 deletions src/ethereum_wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Wallet as EthersWallet } from 'ethers';
import { EthereumSigner } from '@dha-team/arbundles';
import Transaction from 'arweave/node/lib/transaction';

import { PublicKey, ArweaveAddress, ADDR } from './types';
import { bufferTob64Url } from './utils/wallet_utils';
import { Wallet } from './wallet';
import { ec as EC } from 'elliptic';
import Arweave from 'arweave';
import { fromJWK, SECP256k1PublicKey } from 'arweave/node/lib/crypto/keys';
import { Base64String } from '@ardrive/turbo-sdk/lib/types/types';
import { createHash } from 'crypto';

export class EthereumWallet implements Wallet {
private readonly wallet: EthersWallet;
private readonly signer: EthereumSigner;
private readonly jwk: JsonWebKey;

constructor(privateKey: string) {
this.wallet = new EthersWallet(privateKey);
this.signer = new EthereumSigner(this.wallet.privateKey);
const ec = new EC('secp256k1');

const key = ec.keyFromPrivate(privateKey, 'hex');
const pubPoint = key.getPublic();

// Get x, y, d in base64url format
const x = bufferTob64Url(Uint8Array.from(pubPoint.getX().toArray('be', 32)));
const y = bufferTob64Url(Uint8Array.from(pubPoint.getY().toArray('be', 32)));
const d = bufferTob64Url(Uint8Array.from(key.getPrivate().toArray('be', 32)));

// JWK structure
const jwk = {
kty: 'EC',
crv: 'secp256k1',
x,
y,
d
};
this.jwk = jwk;
}

async getPublicKey(): Promise<PublicKey> {
const pubKey = await (await fromJWK(this.jwk)).public();
return bufferTob64Url(Buffer.from(await pubKey.identifier()));
}

async getAddress(): Promise<ArweaveAddress> {
const pubKey = await this.getPublicKey();
const address = sha256B64Url(Buffer.from(pubKey));
return ADDR(address);
}

async sign(data: Uint8Array): Promise<Uint8Array> {
return (await fromJWK(this.jwk)).sign({ payload: Buffer.from(data), isDigest: false });
}

async signTransaction(tx: Transaction): Promise<void> {
return Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https'
}).transactions.sign(tx, await fromJWK(this.jwk));
}

/** Returns ANS-104 Signer */
getSigner(): EthereumSigner {
return this.signer;
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function secp256k1OwnerFromTx(tx: any): Promise<string> {
if (tx.signature === null) {
throw new Error('secp256k1OwnerFromTx error: transaction has no signature, cannot recover owner');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transaction = new Transaction(tx);
const signatureData = await transaction.getSignatureData();
const publicKey = await SECP256k1PublicKey.recover({
payload: signatureData,
isDigest: false,
signature: Buffer.from(tx.signature, 'base64url')
});

return Buffer.from(await publicKey.identifier()).toString('base64url');
}

export function sha256B64Url(input: Buffer): Base64String {
return bufferTob64Url(createHash('sha256').update(input).digest());
}
1 change: 1 addition & 0 deletions src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './ardrive_factory';
export * from './wallet';
export * from './wallet_dao';
export * from './jwk_wallet';
export * from './ethereum_wallet';

// ArFSDao
export * from './arfs/arfsdao';
Expand Down
22 changes: 20 additions & 2 deletions src/jwk_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import jwkToPem, { JWK } from 'jwk-to-pem';
import { PublicKey, ArweaveAddress, ADDR } from './types';
import { b64UrlToBuffer, bufferTob64Url } from './utils/wallet_utils';
import { Wallet } from './wallet';
import { ArweaveSigner } from '@dha-team/arbundles';
import Transaction from 'arweave/node/lib/transaction';

export class JWKWallet implements Wallet {
constructor(private readonly jwk: JWKInterface) {}
Expand All @@ -25,15 +27,31 @@ export class JWKWallet implements Wallet {
}

// Use cases: generating drive keys, file keys, etc.
sign(data: Uint8Array): Promise<Uint8Array> {
sign(data: Uint8Array, saltLength = 0): Promise<Uint8Array> {
const sign = crypto.createSign('sha256');
sign.update(data);
const pem: string = jwkToPem(this.jwk as JWK, { private: true });
const signature = sign.sign({
key: pem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 0 // We do not need to salt the signature since we combine with a random UUID
saltLength // We do not need to salt the signature since we combine with a random UUID
});
return Promise.resolve(signature);
}

async signTransaction(tx: Transaction): Promise<void> {
tx.setOwner(this.jwk.n);
const data = await tx.getSignatureData();
const signature = await this.sign(data, 32); // pass 32 saltLength -- default for Arweave JS RSA Signatures
const id = crypto.createHash('sha256').update(signature).digest();
tx.setSignature({
id: bufferTob64Url(id),
owner: this.jwk.n,
signature: bufferTob64Url(signature)
});
}

getSigner(): ArweaveSigner {
return new ArweaveSigner(this.jwk);
}
}
4 changes: 4 additions & 0 deletions src/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { PublicKey, ArweaveAddress } from './types';
import { Signer } from '@dha-team/arbundles';
import Transaction from 'arweave/node/lib/transaction';

export interface Wallet {
getPublicKey(): Promise<PublicKey>;
getAddress(): Promise<ArweaveAddress>;
sign(data: Uint8Array): Promise<Uint8Array>;
signTransaction(tx: Transaction): Promise<void>;
getSigner(): Signer;
}
8 changes: 3 additions & 5 deletions src/wallet_dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ export class WalletDAO {
]: GQLTagInterface[],
assertBalance = false
): Promise<ARTransferResult> {
// TODO: Figure out how this works for other wallet types
const jwkWallet = fromWallet as JWKWallet;
const winston: Winston = arAmount.toWinston();

// Create transaction
Expand All @@ -96,7 +94,7 @@ export class WalletDAO {
if (process.env.NODE_ENV === 'test') {
txAttributes.last_tx = 'STUB';
}
const transaction = await this.arweave.createTransaction(txAttributes, jwkWallet.getPrivateKey());
const transaction = await this.arweave.createTransaction(txAttributes);
if (rewardSettings.feeMultiple?.wouldBoostReward()) {
transaction.reward = rewardSettings.feeMultiple.boostReward(transaction.reward);
}
Expand Down Expand Up @@ -133,7 +131,7 @@ export class WalletDAO {
assertTagLimits(transaction.tags);

// Sign file
await this.arweave.transactions.sign(transaction, jwkWallet.getPrivateKey());
await fromWallet.signTransaction(transaction);

// Submit the transaction
const response = await (async () => {
Expand All @@ -150,7 +148,7 @@ export class WalletDAO {
reward: W(transaction.reward)
});
} else {
throw new Error(`Transaction failed. Response: ${response}`);
throw new Error(`Transaction failed. Response: ${JSON.stringify(response, null, 2)}`);
}
}
}
35 changes: 33 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ __metadata:
languageName: node
linkType: hard

"@arweave/wasm-secp256k1@npm:^0.0.7":
version: 0.0.7
resolution: "@arweave/wasm-secp256k1@npm:0.0.7"
checksum: 5e2d492dd5b2aba74aec54a04b99269a0adfaf4c4c603c4eba41ce4bcada15dbf0965b6c25d8e81de26763a5fbbc8562efdadc4116e35409ad9b0b23f0ba7b0e
languageName: node
linkType: hard

"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/code-frame@npm:7.27.1"
Expand Down Expand Up @@ -1564,7 +1571,7 @@ __metadata:
languageName: node
linkType: hard

"@types/bn.js@npm:^5.1.2":
"@types/bn.js@npm:*, @types/bn.js@npm:^5.1.2":
version: 5.2.0
resolution: "@types/bn.js@npm:5.2.0"
dependencies:
Expand Down Expand Up @@ -1621,6 +1628,15 @@ __metadata:
languageName: node
linkType: hard

"@types/elliptic@npm:^6":
version: 6.4.18
resolution: "@types/elliptic@npm:6.4.18"
dependencies:
"@types/bn.js": "*"
checksum: c27613c530fb95441e5e6b456c8c9bc26568ca14c546aae6d7c1d8d46869f79a2272feaef266ac00bdb68b2671e6351ed01b91b82266eac30ca9092720825d16
languageName: node
linkType: hard

"@types/empower-core@npm:*":
version: 1.2.4
resolution: "@types/empower-core@npm:1.2.4"
Expand Down Expand Up @@ -2316,6 +2332,7 @@ __metadata:
"@istanbuljs/nyc-config-typescript": ^1.0.2
"@types/bn.js": ^5.1.2
"@types/chai": ^4.3.6
"@types/elliptic": ^6
"@types/human-crypto-keys": ^0.1.2
"@types/jwk-to-pem": ^2.0.0
"@types/lodash": ^4
Expand All @@ -2331,13 +2348,14 @@ __metadata:
"@types/uuid": ^8.3.2
"@typescript-eslint/eslint-plugin": ^6.2.1
"@typescript-eslint/parser": ^6.2.1
arweave: ^1.15.7
arweave: 2.0.0-ec.2
axios: ^0.21.1
axios-retry: ^3.6.0
base64-js: ^1.5.1
bignumber.js: ^9.1.2
bn.js: ^5.2.1
chai: ^4.3.3
elliptic: ^6.6.1
eslint: ^8.46.0
eslint-config-prettier: ^8.1.0
eslint-plugin-prettier: latest
Expand Down Expand Up @@ -2453,6 +2471,19 @@ __metadata:
languageName: node
linkType: hard

"arweave@npm:2.0.0-ec.2":
version: 2.0.0-ec.2
resolution: "arweave@npm:2.0.0-ec.2"
dependencies:
"@arweave/wasm-secp256k1": ^0.0.7
arconnect: ^0.4.2
asn1.js: ^5.4.1
base64-js: ^1.5.1
bignumber.js: ^9.0.2
checksum: 4da59ac0a7e9306127b441418a3c0fe01cdc4e6429f1cb50fbdfe60d129cfb06801268dce7738414801a2fd81e9a967f9bc136d4cec050f9e109b4eb55ae4bfc
languageName: node
linkType: hard

"arweave@npm:^1.10.13, arweave@npm:^1.11.4, arweave@npm:^1.13.7, arweave@npm:^1.15.1, arweave@npm:^1.15.7":
version: 1.15.7
resolution: "arweave@npm:1.15.7"
Expand Down