Skip to content

Commit

Permalink
Feature: Ethers signer (#97)
Browse files Browse the repository at this point in the history
* signer wip

* signer wip

* signer wip

* wip

* da mvp

* dep bump

* Signer wip

* ethers signer

* remove yalc from signer

* remove console.logs

* change ethersprovider version

* add docs

* type fixes

* utils/poll: remove double increment

* dep updatE

* doc fix

* remove jsonrpc error check logic in checkError
  • Loading branch information
mmv08 committed Feb 18, 2021
1 parent d1c449e commit 1982677
Show file tree
Hide file tree
Showing 8 changed files with 1,052 additions and 858 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
"lerna": "^3.22.1"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react": "^17.0.1",
"@types/react-dom": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"eslint": "7.18.0",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.15.0",
"eslint": "7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.6.3",
"ts-jest": "^26.4.4",
"typescript": "^4.1.3"
"ts-jest": "^26.5.1",
"typescript": "^4.1.5"
}
}
51 changes: 50 additions & 1 deletion packages/safe-apps-ethers-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![npm](https://img.shields.io/npm/v/@gnosis.pm/safe-apps-ethers-provider)](https://www.npmjs.com/package/@gnosis.pm/safe-apps-ethers-provider)

This is an `ethers.js` provider to use with `ethers.js` contract instances.
This is an `ethers.js` provider and signer to use with `ethers.js` contract instances.

### How to use

Expand All @@ -14,6 +14,12 @@ yarn add @gnosis.pm/safe-apps-ethers-provider
npm i @gnosis.pm/safe-apps-ethers-provider
```

### SafeAppsSdkProvider (Read-only access)

From [ethers.js documentation](https://docs.ethers.io/v5/api/providers/):

> A **Provider** is an abstraction of a connection to the Ethereum network, providing a concise, consistent interface to standard Ethereum node functionality.
- Usage example with [safe-apps-react-sdk](https://github.com/gnosis/safe-apps-sdk/tree/master/packages/safe-apps-react-sdk)

```js
Expand All @@ -30,6 +36,11 @@ const App = () => {
safe,
]);

// calling read methods
const getSomething = async () => {
const balance = await contract.getBalance('0x000');
};

// calling write methods
const doSomething = async () => {
const { safeTxHash } = await sdk.txs.send({
Expand All @@ -49,6 +60,44 @@ const App = () => {
export default App;
```

### SafeAppsSdkSigner (Read/Write access)

From [ethers.js documentation](https://docs.ethers.io/v5/api/signer/):

> A **Signer** in ethers is an abstraction of an Ethereum Account, which can be used to sign messages and transactions and send signed transactions to the Ethereum Network to execute state changing operations.
- Usage example with [safe-apps-react-sdk](https://github.com/gnosis/safe-apps-sdk/tree/master/packages/safe-apps-react-sdk)

```js
import React, { useMemo, useState } from 'react';
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk';
import { ethers } from 'ethers';
import { SafeAppsSdkSigner } from '@gnosis.pm/safe-apps-ethers-provider';
import Contract from './contracts/DelayedTxModule.json';

const App = () => {
const { sdk, safe } = useSafeAppsSDK();
const contract = useMemo(() => ethers.Contract(Contract.address, Contract.abi, new SafeAppsSdkSigner(safe, sdk)), [
sdk,
safe,
]);

// calling read methods
const getSomething = async () => {
const balance = await contract.getBalance('0x000');
};

// calling write methods
const doSomething = async () => {
const { hash } = await contract.someFunc('someArg');
};

return;
};

export default App;
```

#### More scenarios

For the SDK overview documentation, please refer to the [safe-apps-sdk](https://github.com/gnosis/safe-apps-sdk/) documentation
13 changes: 8 additions & 5 deletions packages/safe-apps-ethers-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gnosis.pm/safe-apps-ethers-provider",
"version": "0.0.2",
"version": "0.1.0-beta.1",
"description": "An ethers.js provider wrapper of Safe Apps SDK",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand All @@ -20,10 +20,13 @@
},
"homepage": "https://github.com/gnosis/safe-apps-sdk#readme",
"dependencies": {
"@ethersproject/bytes": "^5.0.9",
"@ethersproject/logger": "^5.0.8",
"@ethersproject/properties": "^5.0.7",
"@ethersproject/providers": "^5.0.19",
"@ethersproject/abstract-provider": "^5.0.9",
"@ethersproject/abstract-signer": "^5.0.12",
"@ethersproject/bignumber": "^5.0.14",
"@ethersproject/bytes": "^5.0.10",
"@ethersproject/logger": "^5.0.9",
"@ethersproject/properties": "^5.0.8",
"@ethersproject/providers": "^5.0.22",
"@gnosis.pm/safe-apps-sdk": "2.0.0-beta.0"
}
}
2 changes: 1 addition & 1 deletion packages/safe-apps-ethers-provider/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { SafeAppsSdkProvider } from './provider';
export { SafeAppsSdkProvider, SafeAppsSdkSigner } from './provider';
140 changes: 123 additions & 17 deletions packages/safe-apps-ethers-provider/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BaseProvider, TransactionRequest, Network } from '@ethersproject/providers';
import { checkProperties, getStatic, shallowCopy } from '@ethersproject/properties';
import { BaseProvider, TransactionRequest, Network, TransactionResponse, BlockTag } from '@ethersproject/providers';
import { checkProperties, getStatic, shallowCopy, Deferrable, resolveProperties } from '@ethersproject/properties';
import { Signer } from '@ethersproject/abstract-signer';
import { hexlify, hexValue, isHexString } from '@ethersproject/bytes';
import SafeAppsSDK, { SafeInfo } from '@gnosis.pm/safe-apps-sdk';
import { Logger } from '@ethersproject/logger';
import { getLowerCase } from './utils';
import SafeAppsSDK, { SafeInfo } from '@gnosis.pm/safe-apps-sdk';
import { convertSafeTxToEthersTx, getLowerCase, poll, EthError } from './utils';

const logger = new Logger('safe_apps_sdk_ethers_provider');

Expand All @@ -18,7 +19,7 @@ const allowedTransactionKeys: { [key: string]: boolean } = {
};

// eslint-disable-next-line
function checkError(method: string, error: any): any {
function checkError(method: string, error: any, params?: any): any {
// Undo the "convenience" some nodes are attempting to prevent backwards
// incompatibility; maybe for v6 consider forwarding reverts as errors
if (method === 'call' && error.code === Logger.errors.SERVER_ERROR) {
Expand All @@ -28,9 +29,97 @@ function checkError(method: string, error: any): any {
}
}

// making typescript happy
console.log({ params });

throw error;
}

export class SafeAppsSdkSigner extends Signer {
readonly provider: SafeAppsSdkProvider;
readonly _address: string;

constructor(safe: SafeInfo, sdk: SafeAppsSDK) {
logger.checkNew(new.target, SafeAppsSdkSigner);

super();

this.provider = new SafeAppsSdkProvider(safe, sdk);
this._address = safe.safeAddress;
}

async getAddress(): Promise<string> {
const address = this.provider.formatter.address(this._address);

return address;
}

connect(): SafeAppsSdkSigner {
return logger.throwError('cannot create a new connection', Logger.errors.UNSUPPORTED_OPERATION, {
operation: 'connect',
});
}

async signMessage(): Promise<string> {
return logger.throwError('signing messages is not supported', Logger.errors.UNSUPPORTED_OPERATION, {
operation: 'connect',
});
}

signTransaction(): Promise<string> {
return logger.throwError('signing transactions is not supported', Logger.errors.UNSUPPORTED_OPERATION, {
operation: 'signTransaction',
});
}

sendUncheckedTransaction(transaction: Deferrable<TransactionRequest>): Promise<string> {
transaction = shallowCopy(transaction);

const fromAddress = this.getAddress().then((address) => getLowerCase(address));

return resolveProperties({
tx: resolveProperties(transaction),
sender: fromAddress,
}).then(({ tx, sender }) => {
if (tx.from != null) {
if (tx.from.toLowerCase() !== sender) {
logger.throwArgumentError('from address mismatch', 'transaction', transaction);
}
} else {
tx.from = sender;
}

if (typeof tx.value === 'undefined') {
tx.value = 0;
}

const hexTx = SafeAppsSdkProvider.hexlifyTransaction(tx, { from: true });

return this.provider.send('sendTransaction', [hexTx]).then(
(hash) => {
return hash;
},
(error) => {
return checkError('sendTransaction', error, hexTx);
},
);
});
}

sendTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
return this.sendUncheckedTransaction(transaction).then((hash) => {
return poll<TransactionResponse>(() => this.provider.getTransaction(hash))
.then((tx: TransactionResponse) => {
return this.provider._wrapTransaction(tx, hash);
})
.catch((error: EthError) => {
error.transactionHash = hash;
throw error;
});
});
}
}

export class SafeAppsSdkProvider extends BaseProvider {
_safe: SafeInfo;
_sdk: SafeAppsSDK;
Expand All @@ -55,9 +144,18 @@ export class SafeAppsSdkProvider extends BaseProvider {
return [this.formatter.address(this._safe.safeAddress)];
}

getSigner(): SafeAppsSdkSigner {
return new SafeAppsSdkSigner(this._safe, this._sdk);
}

// eslint-disable-next-line
async send(method: string, params: any): Promise<any> {
switch (method) {
case 'sendTransaction':
const tx = await this._sdk.txs.send({ txs: params });

return tx.safeTxHash;

case 'getBlockNumber':
const block = await this._sdk.eth.getBlockByNumber(['latest']);

Expand Down Expand Up @@ -115,6 +213,16 @@ export class SafeAppsSdkProvider extends BaseProvider {
}
}

async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag): Promise<string> {
return this.perform('call', { transaction, blockTag });
}

async getTransaction(safeTxHash: string): Promise<TransactionResponse> {
const tx = await this._sdk.txs.getBySafeTxHash(safeTxHash);

return convertSafeTxToEthersTx(tx);
}

// Convert an ethers.js transaction into a JSON-RPC transaction
// - gasLimit => gas
// - All values hexlified
Expand Down Expand Up @@ -142,28 +250,26 @@ export class SafeAppsSdkProvider extends BaseProvider {
const result: { [key: string]: string } = {};

// Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
['gasLimit', 'gasPrice', 'nonce', 'value'].forEach(function (key) {
// eslint-disable-next-line
if ((<any>transaction)[key] == null) {
(['gasLimit', 'gasPrice', 'nonce', 'value'] as const).forEach(function (key) {
if (transaction[key] == null) {
return;
}
// eslint-disable-next-line
const value = hexValue((<any>transaction)[key]);
if (key === 'gasLimit') {
key = 'gas';

const value = hexValue(transaction[key] ?? '');
let property: typeof key | 'gas' = key;
if (property === 'gasLimit') {
property = 'gas';
}

result[key] = value;
});

['from', 'to', 'data'].forEach(function (key) {
// eslint-disable-next-line
if ((<any>transaction)[key] == null) {
(['from', 'to', 'data'] as const).forEach(function (key) {
if (transaction[key] == null) {
return;
}

// eslint-disable-next-line
result[key] = hexlify((<any>transaction)[key]);
result[key] = hexlify(transaction[key] ?? '');
});

return result;
Expand Down
Loading

0 comments on commit 1982677

Please sign in to comment.