Skip to content
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

Feature #58: Promisified API #63

Merged
merged 86 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
612f496
wip
mmv08 Oct 29, 2020
9840f97
move sendTransaction to TXs class
mmv08 Oct 29, 2020
a80b53a
dep bump
mmv08 Nov 2, 2020
379406e
promise based communicator wip
mmv08 Nov 2, 2020
2b3ea14
promise based communicator wip
mmv08 Nov 2, 2020
dc63153
types wip
mmv08 Nov 3, 2020
d7ae8f6
types wip
mmv08 Nov 3, 2020
613d083
first promisified method test - sendInitializationMessage
mmv08 Nov 3, 2020
33d77f4
build fix
mmv08 Nov 5, 2020
30b2563
update package.json
mmv08 Nov 6, 2020
901b0ee
use arrow functions to not deal with function binding
mmv08 Nov 6, 2020
8e89ef8
dep bump
mmv08 Nov 6, 2020
4a0fb0f
messageId -> method in send func
mmv08 Nov 6, 2020
be32962
incoming msg handler wip
mmv08 Nov 6, 2020
ca88eac
update incoming message handler
mmv08 Nov 6, 2020
06a4722
add known origins
mmv08 Nov 6, 2020
8458c6b
first promisified method was successfully called 🎉
mmv08 Nov 6, 2020
a0ea72d
sdk wip
mmv08 Nov 9, 2020
19731d2
fix build
mmv08 Nov 9, 2020
fbd3e62
communicator wip
mmv08 Nov 9, 2020
0c3b16f
fix handling incomming messages
mmv08 Nov 9, 2020
154588e
dep bump
mmv08 Nov 10, 2020
4db16c5
include sdk version, check method validity
mmv08 Nov 10, 2020
3a1982b
use fs.readfilesync instead of require
mmv08 Nov 10, 2020
2e621d4
bring back require and tweak pkg json
mmv08 Nov 10, 2020
87dbee5
add version check
mmv08 Nov 10, 2020
e125e67
Fix version export
mmv08 Nov 10, 2020
53454c2
fix version number
mmv08 Nov 10, 2020
3c1398c
fix version number & comparing function
mmv08 Nov 10, 2020
8e5c151
Fix semver check
mmv08 Nov 10, 2020
78c0f46
throw error in txs.send if tx was rejectted
mmv08 Nov 10, 2020
3921f52
add types
mmv08 Nov 11, 2020
bbcd844
extend types
mmv08 Nov 11, 2020
3238e96
more type improvements
mmv08 Nov 11, 2020
b4d3299
more type improvements
mmv08 Nov 11, 2020
dc27b58
more type improvements
mmv08 Nov 11, 2020
53fdfa2
update types for getStorageAt call
mmv08 Nov 12, 2020
f5ed2cf
fix getStorageAt test
mmv08 Nov 12, 2020
5e910b0
add numberToHex formatter
mmv08 Nov 12, 2020
b4cd34d
format number to hex in getBlockByNumber
mmv08 Nov 12, 2020
f17cc11
[breaks things]: add types to rpc calls
mmv08 Nov 12, 2020
295f762
types wip
mmv08 Nov 13, 2020
fde32ac
types fix
mmv08 Nov 16, 2020
6ad9b45
Fix tests compilation
mmv08 Nov 16, 2020
1177986
Fix types
mmv08 Nov 16, 2020
80dd993
Type updateS
mmv08 Nov 16, 2020
9aeb41e
types fix
mmv08 Nov 16, 2020
b12cf01
types fix
mmv08 Nov 16, 2020
969efcc
remove eslint comment
mmv08 Nov 16, 2020
ed8d0a2
fix send txs return type
mmv08 Nov 16, 2020
83e30a9
fix tests for transactions
mmv08 Nov 16, 2020
c5b5110
formatting
mmv08 Nov 16, 2020
55df940
remove interfaces deployed by gnosis from constructor
mmv08 Nov 17, 2020
160879c
add log
mmv08 Nov 17, 2020
f4c1cbb
import fix
mmv08 Nov 17, 2020
0e6bdc0
update message validation to check parent element
mmv08 Nov 17, 2020
c27c575
Check for window existance in communicator's send
mmv08 Nov 17, 2020
77043e7
types fix
mmv08 Nov 17, 2020
08d47ba
Fix rpc response
mmv08 Nov 17, 2020
7506f8b
add bootstrap function
mmv08 Nov 17, 2020
ac60622
remove log
mmv08 Nov 17, 2020
e7c85ed
add a test that checks getEnvInfo is called on init
mmv08 Nov 17, 2020
2e98395
add functions for generating requests/responses
mmv08 Nov 18, 2020
4119efa
import fix
mmv08 Nov 18, 2020
f3da4dc
import fix
mmv08 Nov 18, 2020
135a38b
add export
mmv08 Nov 18, 2020
3936afd
Add export for formatter
mmv08 Nov 18, 2020
cab5e96
fix make error response
mmv08 Nov 18, 2020
fdbd86f
dep bump
mmv08 Nov 19, 2020
491f5df
Add version to error response
mmv08 Nov 19, 2020
54ca2a4
resolve whole data object, not only response key
mmv08 Nov 19, 2020
1f371c5
Update types
mmv08 Nov 19, 2020
3e32a4d
fix txs.send method
mmv08 Nov 19, 2020
8cf9681
fix types & hex formatter
mmv08 Nov 19, 2020
2d9b62f
Remove origin check
rmeissner Nov 21, 2020
b1a2b7d
remove unused var
mmv08 Nov 24, 2020
d810979
Merge branch 'feature/58-promisified-api-2' of github.com:gnosis/safe…
mmv08 Nov 24, 2020
72ca8d5
readme change to run github actions
mmv08 Nov 24, 2020
93358b1
readme change to run github actions 2
mmv08 Nov 24, 2020
bbd466b
shorten import
mmv08 Nov 24, 2020
7e6b2aa
accept opts object
mmv08 Nov 24, 2020
3244e2b
update rpc arguments
mmv08 Nov 24, 2020
1fef70d
add docs for rpc calls
mmv08 Nov 24, 2020
e881992
fix valid origin check
mmv08 Nov 24, 2020
bd8868b
rename formatter
mmv08 Nov 24, 2020
12ed65a
remove ethBalance from SafeInfo
mmv08 Nov 24, 2020
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/node_modules
/dist
.DS_Store
.DS_Store
.idea
83 changes: 38 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,57 +28,41 @@ npm build

## Documentation

Apps built with this Sdk are meant to be run in an iframe inside the Safe Web UI.
This library exposes a single method called `initSdk` that receives a single optional parameter, an array of regular expressions. By default it's configured to accept messages from this URLs:
Apps built with the Safe Apps SDK are meant to be run in an iframe inside the Safe Web UI.
This library exposes a class as a default export. It accepts an optional options object:
`whitelistedDomains` - Array of regular expressions for origins you want to accept messages from. If not passed, accepts
messages from any origin (default).

- mainnet: https://gnosis-safe.io,
- mainnet-staging: https://safe-team.staging.gnosisdev.com,
- rinkeby: https://rinkeby.gnosis-safe.io,
- rinkeby-staging: https://safe-team-rinkeby.staging.gnosisdev.com,
- rinkeby-dev: https://safe-team.dev.gnosisdev.com
- localhost (for the desktop app)

By passing the argument to `initSdk` you can add more URLs to the list. It's useful when you are running your own instance of Safe Multisig.

```js
import initSdk from '@gnosis.pm/safe-apps-sdk';
import SafeAppsSDK from '@gnosis.pm/safe-apps-sdk';

const appsSdk = initSdk();
```
const opts = {
whitelistedDomains: [/gnosis-safe\\.io/]
}

It returns a SDK instance that allows you to interact with the Safe Multisig application.
const appsSdk = new SafeAppsSDK(opts);
```

### Subscribing to events
The instance allows you to interact with the Safe Multisig application.

Once you get the SDK instance, you will be able to subscribe to events from the Safe Multisig.

The SDK instance exposes a method called `addListeners` that receives an object with known keys, over these keys you will be able to subscribe to different events.
### Getting Safe information

- `onSafeInfo`: It will provide you first level information like the safeAddress, network, etc.
- `onTransactionConfirmation`: Fired when the user confirms the transaction inside his wallet. The response will include `requestId` and `safeTxHash` of the transaction.
Safe information can be obtained by calling `.getSafeInfo()`

```js
import { SafeInfo } from '@gnosis.pm/safe-apps-sdk';

const onSafeInfo = (safeInfo: SafeInfo): void => {
console.log(safeInfo);
};

const onTransactionConfirmation = ({ requestId, safeTxHash }) => {
console.log(requestId, safeTxHash);
};

appsSdk.addListeners({
onSafeInfo,
onTransactionConfirmation,
});
const safe = await appsSdk.getSafeInfo()
// {
// "safeAddress": "0x2fC97b3c7324EFc0BeC094bf75d5dCdFEb082C53",
// "ethBalance": "0",
mmv08 marked this conversation as resolved.
Show resolved Hide resolved
// "network": "RINKEBY"
// }
```

You can remove listeners by calling `appsSdk.removeListeners()`.

### Sending TXs

Sending a TX through the Safe Multisig is as simple as invoking `sendTransactionsWithParams` method with an array of TXs.
Sending a TX through the Safe Multisig is as simple as invoking `.txs.send()`

```js
// Create a web3 instance
Expand All @@ -102,24 +86,33 @@ const params = {
safeTxGas: 500000,
};

// Send to Safe-multisig
const message = appsSdk.sendTransactionsWithParams(txs, params);
console.log(message.requestId);
try {
const txs = await appsSdk.txs.send({ txs, params });
// { safeTxHash: '0x...' }
} catch (err) {
console.error(err.message)
}
```

`sendTransactionsWithParams` returns a message containing the requestId. You can use it to map transaction calls with `onTransactionConfirmation` events.

> Note: `value` accepts a number or a string as a decimal or hex number.

### Retrieving transaction's status

Once you received safe transaction hash from `onTransactionConfirmation` event listener, you might want to get the status of the transaction (was it executed? how many confirmations does it have?):
Once you received safe transaction hash, you might want to get the status of the transaction (was it executed? how many confirmations does it have?):

```js
const tx = sdk.txs.getBySafeTxHash(safeTxHash);
const tx = await sdk.txs.getBySafeTxHash(safeTxHash);
```

It will return the following structure https://github.com/gnosis/safe-apps-sdk/blob/development/src/types.ts#L157 or throw an error if the backend hasn't synced the transaction yet
It will return the following structure https://github.com/gnosis/safe-apps-sdk/blob/development/src/types.ts#L182 or throw an error if the backend hasn't synced the transaction yet

## RPC Calls

### getBalance

```
const balance = await safe.eth.getBalance({ params: ['0x...'] })
```

## Testing in the Safe Multisig application

Expand Down Expand Up @@ -165,7 +158,7 @@ For this we recommend to use [react-app-rewired](https://www.npmjs.com/package/r
},
```

Additionally you need to create the `config-overrides.js` file in the root of the project to confirgure the **CORS** headers. The content of the file should be:
Additionally, you need to create the `config-overrides.js` file in the root of the project to confirgure the **CORS** headers. The content of the file should be:

```js
/* config-overrides.js */
Expand Down
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@gnosis.pm/safe-apps-sdk",
"version": "0.4.2",
"version": "1.0.0",
"description": "SDK developed to integrate third-party apps with Safe-Multisig app.",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"files": [
"dist/**/*",
"README.md"
Expand All @@ -23,22 +23,24 @@
"author": "Gnosis (https://gnosis.pm)",
"license": "MIT",
"dependencies": {
"semver": "^7.3.2",
"web3-core": "^1.3.0"
},
"devDependencies": {
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"eslint": "^7.12.1",
"@types/node": "^14.14.8",
"@types/semver": "^7.3.4",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1",
"eslint": "^7.13.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.6.1",
"lint-staged": "^10.5.0",
"jest": "^26.6.3",
"lint-staged": "^10.5.1",
"prettier": "^2.1.2",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.3",
"ts-jest": "^26.4.4",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.0.5"
Expand Down
133 changes: 47 additions & 86 deletions src/communication/index.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,69 @@
import {
InterfaceMessageIds,
InterfaceMessageEvent,
SentSDKMessage,
SDKMessageIds,
SDKMessageToPayload,
RequestId,
InterfaceMessageToPayload,
Communicator,
} from '../types';
import { INTERFACE_MESSAGES } from './messageIds';
import semver from 'semver';
import { InterfaceMessageEvent, Communicator, Methods, Response } from '../types';
import { MessageFormatter } from './messageFormatter';

class InterfaceCommunicator implements Communicator {
private allowedOrigins: RegExp[] = [];
// eslint-disable-next-line
type Callback = (response: any) => void;

constructor(allowedOrigins: RegExp[]) {
class PostMessageCommunicator implements Communicator {
private readonly allowedOrigins: RegExp[] | null = null;
private callbacks = new Map<string, Callback>();

constructor(allowedOrigins: RegExp[] | null = null) {
this.allowedOrigins = allowedOrigins;

window.addEventListener('message', this.onParentMessage);
}

private isValidMessage({ origin, data }: InterfaceMessageEvent): boolean {
const emptyOrMalformed = !data || !data.messageId;
const unknownOrigin = this.allowedOrigins?.find((regExp) => regExp.test(origin)) === undefined;
const sameOrigin = origin === window.origin;
private isValidMessage = ({ origin, data, source }: InterfaceMessageEvent): boolean => {
const emptyOrMalformed = !data;
const sentFromParentEl = source === window.parent;
const allowedSDKVersion = typeof data.version !== 'undefined' ? semver.gte(data.version, '1.0.0') : false;
mmv08 marked this conversation as resolved.
Show resolved Hide resolved
let validOrigin = true;
if (Array.isArray(this.allowedOrigins)) {
validOrigin = this.allowedOrigins.find((regExp) => regExp.test(origin)) === undefined;
mmv08 marked this conversation as resolved.
Show resolved Hide resolved
}

return !emptyOrMalformed && !unknownOrigin && !sameOrigin;
}
return !emptyOrMalformed && sentFromParentEl && allowedSDKVersion && validOrigin;
};

private logIncomingMessage(origin: string, payload: InterfaceMessageToPayload[InterfaceMessageIds]): void {
console.info(`SafeConnector: A message was received from origin ${origin}. `, payload);
}

private onParentMessage(msg: InterfaceMessageEvent): void {
this.logIncomingMessage(msg.origin, msg.data);
private logIncomingMessage = (msg: InterfaceMessageEvent): void => {
console.info(`Safe Apps SDK v1: A message was received from origin ${msg.origin}. `, msg.data);
};

private onParentMessage = (msg: InterfaceMessageEvent): void => {
if (this.isValidMessage(msg)) {
this.handleIncomingMessage(msg.data.messageId, msg.data.data, msg.data.requestId);
this.logIncomingMessage(msg);
this.handleIncomingMessage(msg.data);
}
}

private handleIncomingMessage(
messageId: InterfaceMessageIds,
payload: InterfaceMessageToPayload[InterfaceMessageIds],
requestId: RequestId,
): void {
console.log(payload, requestId);
switch (messageId) {
case INTERFACE_MESSAGES.ENV_INFO:
// const typedPayload = payload as InterfaceMessageToPayload[typeof INTERFACE_MESSAGES.ENV_INFO];
};

break;
private handleIncomingMessage = (payload: InterfaceMessageEvent['data']): void => {
const { id } = payload;

case INTERFACE_MESSAGES.ON_SAFE_INFO: {
/* tslint:disable-next-line:no-shadowed-variable */
// const typedPayload = payload as InterfaceMessageToPayload[typeof INTERFACE_MESSAGES.ON_SAFE_INFO];
const cb = this.callbacks.get(id);
if (cb) {
cb(payload);

break;
}

case INTERFACE_MESSAGES.TRANSACTION_CONFIRMED: {
/* tslint:disable-next-line:no-shadowed-variable */
// const typedPayload = payload as InterfaceMessageToPayload[typeof INTERFACE_MESSAGES.TRANSACTION_CONFIRMED];

break;
}

case INTERFACE_MESSAGES.TRANSACTION_REJECTED: {
break;
}

default: {
console.warn(
`SafeConnector: A message was received from origin ${origin} with an unknown message id: ${messageId}`,
);
break;
}
this.callbacks.delete(id);
}
}
};

public send<T extends SDKMessageIds, D = SDKMessageToPayload[T]>(
messageId: T,
data: D,
requestId?: RequestId,
): SentSDKMessage<T, D> {
if (!requestId) {
if (typeof window !== 'undefined') {
requestId = Math.trunc(window?.performance.now());
} else {
requestId = Math.trunc(Date.now());
}
}
const message = {
messageId,
requestId,
data,
};
public send = <M extends Methods, P, R>(method: M, params: P): Promise<Response<R>> => {
const request = MessageFormatter.makeRequest(method, params);

if (typeof window !== 'undefined') {
window.parent.postMessage(message, '*');
if (typeof window === 'undefined') {
throw new Error("Window doesn't exist");
}

return message;
}
window.parent.postMessage(request, '*');
return new Promise((resolve) => {
this.callbacks.set(request.id, (response: Response<R>) => {
resolve(response);
});
});
};
}

export default InterfaceCommunicator;
export * from './messageIds';
export default PostMessageCommunicator;
export * from './methods';
34 changes: 34 additions & 0 deletions src/communication/messageFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ErrorResponse, SDKRequestData, Methods, RequestId, SuccessResponse, MethodToResponse } from '../types';
import { generateRequestId } from './utils';
import { getSDKVersion } from '../utils';

class MessageFormatter {
static makeRequest = <M extends Methods = Methods, P = unknown>(method: M, params: P): SDKRequestData<M, P> => {
const id = generateRequestId();

return {
id,
method,
params,
env: {
sdkVersion: getSDKVersion(),
},
};
};

static makeResponse = (id: RequestId, data: MethodToResponse[Methods], version: string): SuccessResponse => ({
id,
success: true,
version,
data,
});

static makeErrorResponse = (id: RequestId, error: string, version: string): ErrorResponse => ({
id,
success: false,
error,
version,
});
}

export { MessageFormatter };
14 changes: 0 additions & 14 deletions src/communication/messageIds.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/communication/methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const METHODS = {
getEnvInfo: 'getEnvInfo',
sendTransactions: 'sendTransactions',
rpcCall: 'rpcCall',
getSafeInfo: 'getSafeInfo',
} as const;
Loading