├── .circleci └── config.yml ├── .dependabot └── config.yml ├── .eslintrc.js ├── .gitignore ├── @types ├── @smontero__eosio-signing-tools │ └── index.d.ts ├── @zondax__filecoin-signing-tools │ └── index.d.ts └── uint8arrays │ └── index.d.ts ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── RELEASE-NOTES.md ├── babel.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.test.ts ├── blockchain-handler.ts ├── blockchains │ ├── __tests__ │ │ ├── ContractWallet.sol │ │ ├── __snapshots__ │ │ │ ├── eosio.test.ts.snap │ │ │ ├── ethereum.test.ts.snap │ │ │ ├── filecoin.test.ts.snap │ │ │ └── polkadot.test.ts.snap │ │ ├── eosio.test.ts │ │ ├── ethereum.test.ts │ │ ├── filecoin.test.ts │ │ ├── fixtures.ts │ │ └── polkadot.test.ts │ ├── eosio.ts │ ├── ethereum.ts │ ├── filecoin.ts │ └── polkadot.ts ├── index.ts └── utils.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/js-3id-blockchain-utils 5 | docker: 6 | - image: circleci/node:14 7 | steps: 8 | - checkout 9 | 10 | # Download and cache dependencies 11 | - restore_cache: 12 | keys: 13 | - dependencies-cache-{{ checksum "package.json" }} 14 | 15 | - run: 16 | name: install dependencies 17 | command: | 18 | sudo npm i -g codecov node-gyp 19 | npm ci 20 | 21 | - run: 22 | name: lint 23 | command: npm run lint 24 | 25 | - run: 26 | name: test 27 | command: npm test && codecov 28 | 29 | - run: 30 | name: code-coverage 31 | command: bash <(curl -s https://codecov.io/bash) 32 | 33 | - save_cache: 34 | key: dependency-cache-{{ checksum "package.json" }} 35 | paths: 36 | - ./node_modules 37 | 38 | workflows: 39 | version: 2 40 | build-and-deploy: 41 | jobs: 42 | - build 43 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: "javascript" 4 | target_branch: "develop" 5 | directory: "/" 6 | update_schedule: "weekly" 7 | default_reviewers: 8 | - "oed" 9 | default_assignees: 10 | - "oed" 11 | allowed_updates: 12 | - match: 13 | dependency_type: "production" 14 | update_type: "all" 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'jest' 7 | ], 8 | rules: { 9 | "@typescript-eslint/no-explicit-any": "off" 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:@typescript-eslint/eslint-recommended', 14 | 'plugin:@typescript-eslint/recommended', 15 | ], 16 | env: { 17 | "jest/globals": true 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /@types/@smontero__eosio-signing-tools/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@smontero/eosio-signing-tools" { 2 | export class SigningTools { 3 | static verifySignature( 4 | params: VerifySignatureParams 5 | ): boolean; 6 | } 7 | 8 | export interface VerifySignatureParams { 9 | chainId: string; 10 | account: string; 11 | signature: string; 12 | data: string; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /@types/@zondax__filecoin-signing-tools/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@zondax/filecoin-signing-tools/nodejs/filecoin_signer_wasm" { 2 | export * from "@zondax/filecoin-signing-tools"; 3 | } 4 | 5 | declare module "@zondax/filecoin-signing-tools" { 6 | export interface ExtendedKey { 7 | address: string; 8 | public_raw: Uint8Array; 9 | private_raw: Uint8Array; 10 | public_hexstring: string; 11 | private_hexstring: string; 12 | public_base64: string; 13 | private_base64: string; 14 | } 15 | 16 | export interface MessageParams { 17 | From: string; 18 | To: string; 19 | Value: string; 20 | GasPrice: string; 21 | GasLimit: number; 22 | GasFeeCap: string; 23 | GasPremium: string; 24 | Nonce: number; 25 | Method: number; 26 | Params: string; 27 | } 28 | 29 | export interface TransactionSignLotusResponse { 30 | Message: MessageParams; 31 | Signature: { 32 | Type: number; 33 | Data: string; 34 | }; 35 | } 36 | 37 | export function keyRecover( 38 | privateKey: string, 39 | testnet?: boolean 40 | ): ExtendedKey; 41 | export function keyRecoverBLS( 42 | privateKey: string, 43 | testnet?: boolean 44 | ): ExtendedKey; 45 | export function transactionSignLotus( 46 | message: MessageParams, 47 | privateKey: string 48 | ): TransactionSignLotusResponse; 49 | export function verifySignature(signature: any, message: any): any; 50 | export function transactionSerialize(message: any): any; 51 | } 52 | -------------------------------------------------------------------------------- /@types/uint8arrays/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "uint8arrays" { 2 | export function compare(a: Uint8Array, b: Uint8Array): -1 | 1 | 0; 3 | export function concat(arrays: Uint8Array[], length?: number): Uint8Array; 4 | export function equals(a: Uint8Array, b: Uint8Array): boolean; 5 | export function fromString(input: string, encoding?: string): Uint8Array; 6 | export function toString(array: Uint8Array, encoding?: string): string; 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated! 2 | This repo has been deprecated in favour of `@ceramicnetwork/blockchain-utils-linking` and `@ceramicnetwork/blockchain-utils-validation`. See [ceramicnetwork/js-ceramic](https://github.com/ceramicnetwork/js-ceramic) for more information. 3 | 4 | 5 | 6 | 7 | # 3id-blockchain-utils 8 | [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 9 | [![CircleCI](https://img.shields.io/circleci/project/github/ceramicnetwork/js-3id-blockchain-utils.svg?style=for-the-badge)](https://circleci.com/gh/ceramicnetwork/js-3id-blockchain-utils) 10 | [![npm](https://img.shields.io/npm/dt/3id-blockchain-utils.svg?style=for-the-badge)](https://www.npmjs.com/package/3id-blockchain-utils) 11 | [![npm](https://img.shields.io/npm/v/3id-blockchain-utils.svg?style=for-the-badge)](https://www.npmjs.com/package/3id-blockchain-utils) 12 | [![Codecov](https://img.shields.io/codecov/c/github/ceramicnetwork/js-3id-blockchain-utils.svg?style=for-the-badge)](https://codecov.io/gh/ceramicnetwork/js-3id-blockchain-utils) 13 | 14 | This package contains a bunch of utilities that is used by 3ID and 3Box in order to create and verify links from blockchain addresses. 15 | 16 | ## Tabel of Contents 17 | - [Install](#install) 18 | - [Usage](#usage) 19 | - [Supported blockchains](#supported-blockchains) 20 | - [Contributing](#contributing) 21 | - [Test](#Test) 22 | - [License](#license) 23 | 24 | ## Install 25 | ``` 26 | $ npm install --save 3id-blockchain-utils 27 | ``` 28 | 29 | ## Usage 30 | Import the package into your project 31 | ```js 32 | import { createLink, validateLink, authenticate } from '3id-blockchain-utils' 33 | ``` 34 | 35 | Use the library to create and verify links: 36 | ```js 37 | const did = 'did:3:bafypwg9834gf...' 38 | const proof = await createLink(did, '0x123abc...', ethereumProvider) 39 | console.log(proof) 40 | 41 | const verified = await validateLink(proof) 42 | if (verified) { 43 | console.log('Proof is valid', proof) 44 | } else { 45 | console.log('Proof is invalid') 46 | } 47 | ``` 48 | 49 | Use the library for 3ID authenticate: 50 | 51 | ```js 52 | await authenticate(message, '0x123abc...', ethereumProvider) 53 | ``` 54 | 55 | ## Supported blockchains 56 | 57 | Below you can see a table which lists supported blockchains and their provider objects. 58 | 59 | | Blockchain | CAIP-2 namespace | Supported providers | 60 | |------------|-----------|---------------------------------| 61 | | Ethereum | [eip155](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-3.md) | metamask-like ethereum provider | 62 | | Filecoin | fil | [Filecoin Wallet Provider](https://github.com/openworklabs/filecoin-wallet-provider) | 63 | 64 | ## Maintainers 65 | [@oed](https://github.com/oed) 66 | 67 | ## Adding support for a blockchain 68 | If you want to add support for a new blockchain to 3ID this is the place to do so. This library uses [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) to represent accounts in a blockchain agnostic way. If the blockchain you want to add isn't already part of the [CAIP](https://github.com/ChainAgnostic/CAIPs) standards you shold make sure to add it there. 69 | 70 | To begin adding support for a given blockchain add a file with the path: `src/blockchains/.js`. This module needs to export three functions: 71 | 72 | * `createLink` - creates a LinkProof object which associates the specified AccountID with the DID 73 | * `validateLink` - validates the given LinkProof 74 | * `authenticate` - signs a message and returns some entropy based on the signature. Needs to be deterministic 75 | 76 | It also needs to export a constant called `namespace`. This constant is a string which contains the [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) chainId namespace. 77 | 78 | Please see `src/blockchains/ethereum.js` for an example of how this is implemented for the `eip155` (ethereum) CAIP-2 namespace. 79 | 80 | 81 | Finally add support for your blockchain in `src/index.js`. Simply add it to the `handlers` array. 82 | 83 | ### Test 84 | Test the code by running: 85 | ``` 86 | $ npm test 87 | ``` 88 | 89 | ## Licence 90 | Apache-2.0 OR MIT 91 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v1.1.0 - 2020-11-18 4 | feat: add support for filecoin 5 | feat: support general link message 6 | 7 | ## v1.0.0 - 2020-08-10 8 | ref: use typescript 9 | ref: use caip10 accountIds 10 | 11 | ## v0.4.1 - 2020-08-06 12 | chore: up ether 13 | 14 | ## v0.4.0 - 2020-05-30 15 | feat: auth function, reconciled type detection 16 | 17 | ## v0.3.4 - 2020-05-18 18 | fix: strict ethersjs package versions 19 | 20 | ## v0.3.3 - 2020-03-17 21 | fix: hex encode message send 22 | chore: update eth-sig-util package 23 | fix: eth-sig-util import 24 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', {targets: {node: '12'}}], 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3id-blockchain-utils", 3 | "version": "1.3.1", 4 | "description": "Blockchain utils for 3ID", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "./node_modules/.bin/jest --coverage", 9 | "build": "./node_modules/.bin/tsc -p tsconfig.json", 10 | "prepublishOnly": "npm run build", 11 | "prepare": "npm run build", 12 | "prebuild": "npm run clean", 13 | "lint": "./node_modules/.bin/eslint ./src --ext .js,.jsx,.ts,.tsx", 14 | "clean": "rm -rf ./lib" 15 | }, 16 | "author": "oed", 17 | "license": "(Apache-2.0 OR MIT)", 18 | "dependencies": { 19 | "@babel/runtime": "^7.9.2", 20 | "@ethersproject/contracts": "^5.0.1", 21 | "@ethersproject/providers": "^5.0.4", 22 | "@ethersproject/wallet": "^5.0.1", 23 | "caip": "^0.9.2", 24 | "js-sha256": "^0.9.0", 25 | "uint8arrays": "^1.1.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.9.0", 29 | "@babel/preset-env": "^7.8.4", 30 | "@babel/preset-typescript": "^7.9.0", 31 | "@polkadot/api": "^2.4.1", 32 | "@polkadot/keyring": "^3.6.1", 33 | "@polkadot/types": "^2.7.1", 34 | "@polkadot/util-crypto": "^3.6.1", 35 | "@glif/filecoin-address": "^1.1.0-beta.1", 36 | "@glif/local-managed-provider": "^1.1.0-beta.1", 37 | "@smontero/eosio-local-provider": "0.0.3", 38 | "@smontero/eosio-signing-tools": "0.0.6", 39 | "@types/jest": "^25.2.3", 40 | "@typescript-eslint/eslint-plugin": "^2.19.0", 41 | "@typescript-eslint/parser": "^2.19.0", 42 | "@zondax/filecoin-signing-tools": "^0.12.0", 43 | "babel-jest": "^25.1.0", 44 | "eslint": "^6.8.0", 45 | "eslint-plugin-jest": "^23.8.2", 46 | "eth-sig-util": "^2.5.3", 47 | "ganache-core": "^2.13.1", 48 | "jest": "^25.1.0", 49 | "typescript": "^3.9.3" 50 | }, 51 | "optionalDependencies": { 52 | "@polkadot/util-crypto": "^3.6.1", 53 | "@smontero/eosio-signing-tools": "0.0.6" 54 | }, 55 | "jest": { 56 | "testPathIgnorePatterns": [ 57 | "node_modules", 58 | "lib" 59 | ], 60 | "moduleNameMapper": { 61 | "^@zondax/filecoin-signing-tools$": "@zondax/filecoin-signing-tools/nodejs" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`3ID Blockchain Utils authenticate: should throw if namespace not supported 1`] = `[Error: authenticate with namespace 'not-supported' not supported]`; 4 | 5 | exports[`3ID Blockchain Utils createLink: should throw if namespace not supported 1`] = `[Error: creating link with namespace 'not-supported' is not supported]`; 6 | 7 | exports[`3ID Blockchain Utils validateLink: should throw if namespace not supported 1`] = `[Error: proof with namespace 'not-supported' not supported]`; 8 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountID } from 'caip' 2 | import { createLink, validateLink, authenticate } from '../index' 3 | 4 | jest.mock('../blockchains/ethereum', () => ({ 5 | createLink: jest.fn(), 6 | validateLink: jest.fn(proof => proof), 7 | authenticate: jest.fn(), 8 | namespace: 'eip155' 9 | })) 10 | import ethereum from '../blockchains/ethereum' 11 | 12 | 13 | describe('3ID Blockchain Utils', () => { 14 | 15 | const testDid = 'did:3:bafysdfwefwe' 16 | const testAccount = new AccountID('0x12345abcde@eip155:1337') 17 | const notSupportedAccount = new AccountID('0x12345abcde@not-supported:1337') 18 | const mockProvider = 'fake provider' 19 | 20 | it('validateLink: should throw if namespace not supported', async () => { 21 | const proof = { 22 | version: 2, 23 | account: notSupportedAccount 24 | } 25 | await expect(validateLink(proof)).rejects.toMatchSnapshot() 26 | }) 27 | 28 | it('validateLink: ethereum namespace', async () => { 29 | const proof = { 30 | version: 2, 31 | message: 'verifying did: ' + testDid, 32 | account: testAccount 33 | } 34 | const proofWithDid = Object.assign(proof, { did: testDid }) 35 | expect(await validateLink(proof)).toEqual(proofWithDid) 36 | expect(ethereum.validateLink).toHaveBeenCalledWith(proof) 37 | ethereum.validateLink.mockClear() 38 | // version < 2 should be interpreted as ethereum 39 | delete proof.version 40 | expect(await validateLink(proof)).toEqual(proofWithDid) 41 | expect(ethereum.validateLink).toHaveBeenCalledWith(proof) 42 | }) 43 | 44 | it('createLink: should throw if namespace not supported', async () => { 45 | await expect(createLink(testDid, notSupportedAccount, mockProvider)).rejects.toMatchSnapshot() 46 | }) 47 | 48 | it('createLink: should create link with ethereum namespace correctly', async () => { 49 | const proof = { 50 | version: 2, 51 | message: 'verifying did: ' + testDid, 52 | account: testAccount 53 | } 54 | ethereum.createLink.mockImplementation(() => proof) 55 | expect(await createLink(testDid, testAccount, mockProvider)).toEqual(proof) 56 | expect(ethereum.createLink).toHaveBeenCalledWith(testDid, testAccount, mockProvider, {}) 57 | }) 58 | 59 | it('authenticate: should throw if namespace not supported', async () => { 60 | await expect(authenticate('msg', notSupportedAccount, mockProvider)).rejects.toMatchSnapshot() 61 | }) 62 | 63 | it('authenticate: should create link with ethereum namespace correctly', async () => { 64 | ethereum.authenticate.mockImplementation(() => 'entropy') 65 | expect(await authenticate('msg', testAccount, mockProvider)).toEqual('entropy') 66 | expect(ethereum.authenticate).toHaveBeenCalledWith('msg', testAccount, mockProvider) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/blockchain-handler.ts: -------------------------------------------------------------------------------- 1 | import {AccountID} from "caip"; 2 | import {LinkProof} from "./utils"; 3 | 4 | export interface BlockchainHandlerOpts { 5 | skipTimestamp?: boolean; 6 | } 7 | 8 | export interface BlockchainHandler { 9 | namespace: string; 10 | authenticate(message: string, account: AccountID, provider: any): Promise; 11 | validateLink (proof: LinkProof): Promise; 12 | createLink (did: string, account: AccountID, provider: any, opts?: BlockchainHandlerOpts): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/ContractWallet.sol: -------------------------------------------------------------------------------- 1 | contract ContractWallet { 2 | 3 | bool isValid = false; 4 | 5 | function isValidSignature(bytes memory _messageHash, bytes memory _signature) public view returns (bytes4 magicValue) { 6 | if (isValid) { 7 | return 0x20c13b0b; 8 | } 9 | return 0x0; 10 | } 11 | 12 | function setIsValid(bool valid) public { 13 | isValid = valid; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/__snapshots__/eosio.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`authenticate Jungle 1`] = `"0xda6a3b61f0849251071ebceb94c874a3517772cc2a865c8c5e078c2e6646a0c2"`; 4 | 5 | exports[`authenticate Telos Testnet 1`] = `"0x521674ea7031af056c7fcf69e2c3ff6f4bea888158941a7b6dc789d0fe44cf53"`; 6 | 7 | exports[`createLink generate proof on jungle testnet 1`] = ` 8 | Object { 9 | "account": "idx3idctest1@eosio:2a02a0053e5a8cf73a56ba0fda11e4d9", 10 | "message": "Create a new account link to your identity. 11 | 12 | did:3:bafysdfwefwe", 13 | "signature": "SIG_K1_JyGLbHtvMUbnntVkkTwmndV1biVHGERwawiyAWwEz2Mng7Q4At9FFbboYgJUhMjA6GBhvBDEHjLXLZHS4B53FxFHmSbqDC", 14 | "type": "eosio", 15 | "version": 1, 16 | } 17 | `; 18 | 19 | exports[`createLink generate proof on telos testnet 1`] = ` 20 | Object { 21 | "account": "testuser1111@eosio:1eaa0824707c8c16bd25145493bf062a", 22 | "message": "Create a new account link to your identity. 23 | 24 | did:3:bafysdfwefwe", 25 | "signature": "SIG_K1_KXEPcYBUAigcwcZJR6eRgynzizmb78aodN8y7kk5bnvN6uSn8SYWxbFPzzQUZoM2CDwju8HMfhQDRTVoFtWVzGNMmz32Nm", 26 | "type": "eosio", 27 | "version": 1, 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/__snapshots__/ethereum.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Blockchain: Ethereum authenticate: correctly signs auth message 1`] = `"0xcf54e7560501fa53e3f7092672a920c346034ba93d001919f0cb5445851b5685"`; 4 | 5 | exports[`Blockchain: Ethereum createLink: should create erc1271 proof correctly 1`] = ` 6 | Object { 7 | "account": "0xbe6c27620b271dd76f1787cdef1f4375cff3fa1f@eip155:1337", 8 | "message": "Create a new account link to your identity. 9 | 10 | did:3:bafysdfwefwe", 11 | "signature": "0x9feb4c429a673415edb63f42b5a302b669b2e479e3b44d4ac41ec7f09d53d6494063bf53dddaed16c1d229ca4cb0841b2360d52777289c57ba539c1ddff9144f1b", 12 | "type": "erc1271", 13 | "version": 2, 14 | } 15 | `; 16 | 17 | exports[`Blockchain: Ethereum createLink: should create ethereumEOA proof correctly 1`] = ` 18 | Object { 19 | "account": "0x8fe2c4516e920425e177658aaac451ca0463ed69@eip155:1", 20 | "message": "Create a new account link to your identity. 21 | 22 | did:3:bafysdfwefwe", 23 | "signature": "0x9feb4c429a673415edb63f42b5a302b669b2e479e3b44d4ac41ec7f09d53d6494063bf53dddaed16c1d229ca4cb0841b2360d52777289c57ba539c1ddff9144f1b", 24 | "type": "ethereum-eoa", 25 | "version": 2, 26 | } 27 | `; 28 | 29 | exports[`Blockchain: Ethereum createLink: should throw if erc1271 is on wrong chain 1`] = `[Error: ChainId in provider (1337) is different from AccountID (123)]`; 30 | 31 | exports[`Blockchain: Ethereum validateLink: validate v0 and v1 proofs 1`] = ` 32 | Object { 33 | "account": "0x8fe2c4516e920425e177658aaac451ca0463ed69@eip155:1", 34 | "message": "Create a new 3Box profile 35 | 36 | - 37 | Your unique profile ID is did:3:bafysdfwefwe", 38 | "signature": "0xfa69ccf4a94db61325429d37c58c6de534b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c", 39 | "type": "ethereum-eoa", 40 | "version": 2, 41 | } 42 | `; 43 | 44 | exports[`Blockchain: Ethereum validateLink: validate v0 and v1 proofs 2`] = `[Error: invalid point]`; 45 | 46 | exports[`Blockchain: Ethereum validateLink: validate v0 and v1 proofs 3`] = ` 47 | Object { 48 | "account": "0x8fe2c4516e920425e177658aaac451ca0463ed69@eip155:1", 49 | "message": "Create a new 3Box profile 50 | 51 | - 52 | Your unique profile ID is did:3:bafysdfwefwe", 53 | "signature": "0xfa69ccf4a94db61325429d37c58c6de534b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c", 54 | "type": "ethereum-eoa", 55 | "version": 2, 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/__snapshots__/filecoin.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`authenticate mainnet 1`] = `"F5gy3IHoKUNJOUEQQMHg1iTZGa4Ef3TeT+deaBKkkYFK3Aur5y3XuOL4NNygsn9lYPnPsvXUvtBiyy4+yZ6geQE="`; 4 | 5 | exports[`authenticate mainnet BLS 1`] = `"r1QYs+G633VEW/i5EUZVMm/SqdH5fvsylvql0chOeMJAEB6jsD9NKfgeap6mVepVE+IA2iqFdqi+F0lTkRicrxvOLs7siyPXAQZPivx34oxSEdN55LNbXL6EAIE+3p4W"`; 6 | 7 | exports[`authenticate testnet 1`] = `"APHUqaCLuyqunqfYJl8YAq3/p6oO9iiEAGkGQILzZAEMR/L28/F+iN2ismhMJCm9yr8VjIqgneiaFIkr65fimgA="`; 8 | 9 | exports[`createLink generate proof on mainnet 1`] = ` 10 | Object { 11 | "account": "f1ymbkm43ysdiruibji6c6m2iy5bxwbuv5d6cdqmy@fil:f", 12 | "message": "Create a new account link to your identity. 13 | 14 | did:3:bafysdfwefwe", 15 | "signature": "ozDCg49gdxLlfvZ+3TzsmXWk/ubtmNKFn4XmF23qWzk+NG2KYrp9dDJbdJO1VuoiiHmcDHf9Dds1/pxAWJU6oQA=", 16 | "type": "eoa-tx", 17 | "version": 2, 18 | } 19 | `; 20 | 21 | exports[`createLink generate proof on mainnet for BLS key 1`] = ` 22 | Object { 23 | "account": "f3vbrwhphivdxyvs3pxpp54w73664bgxmb7ed4du4ohhu3dc6f5y264cs72yluw7mjbwnlrtzq543ys57plzka@fil:f", 24 | "message": "Create a new account link to your identity. 25 | 26 | did:3:bafysdfwefwe", 27 | "signature": "rXuSytpa48/cGn9m4Hncxguoz7KmYkx4qN+Fos/PELXbVcsaer85Lytz1//LSVlVDG6B+3zM3x4waL0xRDSgW15VPeLL3N2c1Yski9mO6OVVdEMS0b87vPZE8I/CvMpw", 28 | "type": "eoa-tx", 29 | "version": 2, 30 | } 31 | `; 32 | 33 | exports[`createLink generate proof on testnet 1`] = ` 34 | Object { 35 | "account": "t17lxg2i2otnl7mmpw2ocd6o4e3b4un3272vny6ka@fil:t", 36 | "message": "Create a new account link to your identity. 37 | 38 | did:3:bafysdfwefwe", 39 | "signature": "9duC0YnGDAv6rSaki+/0FDLLiuNEHWJ3gsy92zaX6EliD0kQnCzHa5QKOeYRQzO51eamD72wSYY4Knl4WvBDeAE=", 40 | "type": "eoa-tx", 41 | "version": 2, 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/__snapshots__/polkadot.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Blockchain: Polkadot createLink create proof with ed25519 1`] = ` 4 | Object { 5 | "account": "5EEvMbuaJQxxPorGtzY2qUYqM3akRrnD36bZD4PomfpLdrtx@polkadot:b0a8d493285c2df73290dfb7e61f870f", 6 | "message": "0x4372656174652061206e6577206163636f756e74206c696e6b20746f20796f7572206964656e746974792e0a0a6469643a333a626166797364667765667765", 7 | "signature": "0xf457de8941e928ab8dfe6ac3d6b1fdc3b736b839b83fcd4f5e4697d20c3ff3d5c79654fc36dc401e34c311593b577753077f18474182f576c0215461277a1e00", 8 | "type": "eoa", 9 | "version": 2, 10 | } 11 | `; 12 | 13 | exports[`Blockchain: Polkadot createLink create proof with secp256k 1`] = ` 14 | Object { 15 | "account": "5GuAnwx9uKSQxKZB8moxoaX4ceehoCYu4XYMjJkKtm2QDgqC@polkadot:b0a8d493285c2df73290dfb7e61f870f", 16 | "message": "0x4372656174652061206e6577206163636f756e74206c696e6b20746f20796f7572206964656e746974792e0a0a6469643a333a626166797364667765667765", 17 | "signature": "0x78e60152aacd9ab05ce3ad951a640d69f1ed0e24f8b81b78f695b9b1e3f2c83d5446b76dd7cbe5e18db41ddee3ffaef2b100003b8263e93d1d2a4ffd37b9cef601", 18 | "type": "eoa", 19 | "version": 2, 20 | } 21 | `; 22 | 23 | exports[`Blockchain: Polkadot createLink create proof with sr25519 1`] = `"5CXU7u72Gz9Jqo6FdAwzSsxX5JhdRwMj2UZ4xrAEoo3AyySx@polkadot:b0a8d493285c2df73290dfb7e61f870f"`; 24 | 25 | exports[`Blockchain: Polkadot createLink create proof with sr25519 2`] = `"0x4372656174652061206e6577206163636f756e74206c696e6b20746f20796f7572206964656e746974792e0a0a6469643a333a626166797364667765667765"`; 26 | 27 | exports[`Blockchain: Polkadot createLink create proof with sr25519 3`] = `"eoa"`; 28 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/eosio.test.ts: -------------------------------------------------------------------------------- 1 | import {EOSIOProvider} from "@smontero/eosio-local-provider"; 2 | import {authenticate, createLink, validateLink} from "../eosio"; 3 | import {AccountID} from "caip"; 4 | 5 | jest.setTimeout(15000) 6 | 7 | const did = 'did:3:bafysdfwefwe' 8 | const telosTestnetChainId = '1eaa0824707c8c16bd25145493bf062aecddfeb56c736f6ba6397f3195f33c9f' 9 | const jungleChainId = '2a02a0053e5a8cf73a56ba0fda11e4d92e0238a4a2aa74fccf46d5a910746840' 10 | const telosTestnetCAIPChainId = '1eaa0824707c8c16bd25145493bf062a' 11 | const jungleCAIPChainId = '2a02a0053e5a8cf73a56ba0fda11e4d9' 12 | const invalidCAIPChainId = '11111111111111111111111111111111' 13 | const telosTestnetAccount = 'testuser1111' 14 | const jungleAccount = 'idx3idctest1' 15 | const telosTestnetProvider = new EOSIOProvider({ 16 | chainId: telosTestnetChainId, 17 | account: telosTestnetAccount, 18 | keys: { 19 | EOS6uUc8fYoCdyz7TUAXqHvRbU7QnVirFuvcAW6NMQqBabdME6FnL: '5KFFFvKioakMpt8zWnyGKnLaDzzUSqy5V33PHHoxEam47pLJmo2' 20 | } 21 | }) 22 | const jungleProvider = new EOSIOProvider({ 23 | chainId: jungleChainId, 24 | account: jungleAccount, 25 | keys: { 26 | EOS7f7hdusWKXY1cymDLvUL3m6rTLKmdyPi4e6kquSnmfVxxEwVcC: '5JRzDcbMqvTJxjHeP8vZqZbU9PwvaaTsoQhoVTAs3xBVSZaPB9U' 27 | } 28 | }) 29 | 30 | describe('createLink', () => { 31 | test('generate proof on telos testnet', async () => { 32 | const account = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 33 | const proof = await createLink(did, account, telosTestnetProvider, {skipTimestamp: true}) 34 | expect(proof).toMatchSnapshot() 35 | }) 36 | 37 | // test('generate proof on telos testnet with timestamp', async () => { 38 | // const account = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 39 | // const proof = await createLink(did, account, telosTestnetProvider, {skipTimestamp: false}) 40 | // expect(proof).toMatchSnapshot() 41 | // }) 42 | 43 | test('generate proof on jungle testnet', async () => { 44 | const account = new AccountID(`${jungleAccount}@eosio:${jungleCAIPChainId}`) 45 | const proof = await createLink(did, account, jungleProvider, {skipTimestamp: true}) 46 | expect(proof).toMatchSnapshot() 47 | }) 48 | 49 | test('fail on telos testnet account for jungle provider', async () => { 50 | const account = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 51 | await expect(createLink(did, account, jungleProvider, {skipTimestamp: true})).rejects.toThrow() 52 | }) 53 | 54 | test('fail on telos testnet account for jungle chain', async () => { 55 | const account = new AccountID(`${telosTestnetAccount}@eosio:${jungleCAIPChainId}`) 56 | await expect(createLink(did, account, jungleProvider, {skipTimestamp: true})).rejects.toThrow() 57 | }) 58 | 59 | test('fail on jungle account for telos testnet provider', async () => { 60 | const account = new AccountID(`${jungleAccount}@eosio:${jungleCAIPChainId}`) 61 | await expect(createLink(did, account, telosTestnetProvider, {skipTimestamp: true})).rejects.toThrow() 62 | }) 63 | 64 | test('fail on jungle account for telos testnet chain', async () => { 65 | const account = new AccountID(`${jungleAccount}@eosio:${telosTestnetCAIPChainId}`) 66 | await expect(createLink(did, account, telosTestnetProvider, {skipTimestamp: true})).rejects.toThrow() 67 | }) 68 | 69 | }) 70 | 71 | describe('validateLink', () => { 72 | test('Telos testnet', async () => { 73 | const account = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 74 | const proof = await createLink(did, account, telosTestnetProvider, {skipTimestamp: true}) 75 | await expect(validateLink(proof)).resolves.toEqual(proof) 76 | 77 | let testAccount = new AccountID(`${jungleAccount}@eosio:${jungleCAIPChainId}`) 78 | proof.account = testAccount.toString() 79 | await expect(validateLink(proof)).resolves.toEqual(null) 80 | 81 | testAccount = new AccountID(`${jungleAccount}@eosio:${telosTestnetCAIPChainId}`) 82 | proof.account = testAccount.toString() 83 | await expect(validateLink(proof)).resolves.toEqual(null) 84 | 85 | testAccount = new AccountID(`${jungleAccount}@eosio:${invalidCAIPChainId}`) 86 | proof.account = testAccount.toString() 87 | await expect(validateLink(proof)).resolves.toEqual(null) 88 | }) 89 | 90 | test('Jungle', async () => { 91 | const account = new AccountID(`${jungleAccount}@eosio:${jungleCAIPChainId}`) 92 | const proof = await createLink(did, account, jungleProvider, {skipTimestamp: true}) 93 | await expect(validateLink(proof)).resolves.toEqual(proof) 94 | 95 | let testAccount = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 96 | proof.account = testAccount.toString() 97 | await expect(validateLink(proof)).resolves.toEqual(null) 98 | 99 | testAccount = new AccountID(`${telosTestnetAccount}@eosio:${jungleCAIPChainId}`) 100 | proof.account = testAccount.toString() 101 | await expect(validateLink(proof)).resolves.toEqual(null) 102 | 103 | testAccount = new AccountID(`${telosTestnetAccount}@eosio:${invalidCAIPChainId}`) 104 | proof.account = testAccount.toString() 105 | await expect(validateLink(proof)).resolves.toEqual(null) 106 | 107 | }) 108 | }) 109 | 110 | describe('authenticate', () => { 111 | test('Telos Testnet', async () => { 112 | const account = new AccountID(`${telosTestnetAccount}@eosio:${telosTestnetCAIPChainId}`) 113 | expect(await authenticate('msg', account, telosTestnetProvider)).toMatchSnapshot() 114 | }) 115 | 116 | test('Jungle', async () => { 117 | const account = new AccountID(`${jungleAccount}@eosio:${jungleCAIPChainId}`) 118 | expect(await authenticate('msg', account, jungleProvider)).toMatchSnapshot() 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/ethereum.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountID } from 'caip' 2 | import ganache from 'ganache-core' 3 | import * as sigUtils from 'eth-sig-util' 4 | import { ContractFactory, Contract } from "@ethersproject/contracts" 5 | import * as providers from '@ethersproject/providers' 6 | import { encodeRpcMessage } from '../../utils' 7 | import ethereum from '../ethereum' 8 | import proofs from './fixtures' 9 | 10 | const CONTRACT_WALLET_ABI = [ { "constant": false, "inputs": [ { "internalType": "bool", "name": "valid", "type": "bool" } ], "name": "setIsValid", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "internalType": "bytes", "name": "_messageHash", "type": "bytes" }, { "internalType": "bytes", "name": "_signature", "type": "bytes" } ], "name": "isValidSignature", "outputs": [ { "internalType": "bytes4", "name": "magicValue", "type": "bytes4" } ], "payable": false, "stateMutability": "view", "type": "function" } ] 11 | const CONTRACT_WALLET_BYTECODE = { "linkReferences": {}, "object": "608060405260008060006101000a81548160ff02191690831515021790555034801561002a57600080fd5b506102938061003a6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806317c136771461003b57806320c13b0b1461006b575b600080fd5b6100696004803603602081101561005157600080fd5b8101908080351515906020019092919050505061020f565b005b6101bb6004803603604081101561008157600080fd5b810190808035906020019064010000000081111561009e57600080fd5b8201836020820111156100b057600080fd5b803590602001918460018302840111640100000000831117156100d257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192908035906020019064010000000081111561013557600080fd5b82018360208201111561014757600080fd5b8035906020019184600183028401116401000000008311171561016957600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929050505061022b565b60405180827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19167bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916815260200191505060405180910390f35b806000806101000a81548160ff02191690831515021790555050565b60008060009054906101000a900460ff1615610250576320c13b0b60e01b9050610258565b600060e01b90505b9291505056fea265627a7a723158209d7aa06b7443aa12cee8b1ba4356af624de6b912d6af47b494c9b9d621b883ac64736f6c634300050b0032", "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 PUSH1 0x0 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH1 0xFF MUL NOT AND SWAP1 DUP4 ISZERO ISZERO MUL OR SWAP1 SSTORE POP CALLVALUE DUP1 ISZERO PUSH2 0x2A JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x293 DUP1 PUSH2 0x3A PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH2 0x36 JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x17C13677 EQ PUSH2 0x3B JUMPI DUP1 PUSH4 0x20C13B0B EQ PUSH2 0x6B JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0x69 PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH2 0x51 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD ISZERO ISZERO SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH2 0x20F JUMP JUMPDEST STOP JUMPDEST PUSH2 0x1BB PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x40 DUP2 LT ISZERO PUSH2 0x81 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 PUSH5 0x100000000 DUP2 GT ISZERO PUSH2 0x9E JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP3 ADD DUP4 PUSH1 0x20 DUP3 ADD GT ISZERO PUSH2 0xB0 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP2 DUP5 PUSH1 0x1 DUP4 MUL DUP5 ADD GT PUSH5 0x100000000 DUP4 GT OR ISZERO PUSH2 0xD2 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST SWAP2 SWAP1 DUP1 DUP1 PUSH1 0x1F ADD PUSH1 0x20 DUP1 SWAP2 DIV MUL PUSH1 0x20 ADD PUSH1 0x40 MLOAD SWAP1 DUP2 ADD PUSH1 0x40 MSTORE DUP1 SWAP4 SWAP3 SWAP2 SWAP1 DUP2 DUP2 MSTORE PUSH1 0x20 ADD DUP4 DUP4 DUP1 DUP3 DUP5 CALLDATACOPY PUSH1 0x0 DUP2 DUP5 ADD MSTORE PUSH1 0x1F NOT PUSH1 0x1F DUP3 ADD AND SWAP1 POP DUP1 DUP4 ADD SWAP3 POP POP POP POP POP POP POP SWAP2 SWAP3 SWAP2 SWAP3 SWAP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 PUSH5 0x100000000 DUP2 GT ISZERO PUSH2 0x135 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP3 ADD DUP4 PUSH1 0x20 DUP3 ADD GT ISZERO PUSH2 0x147 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP2 DUP5 PUSH1 0x1 DUP4 MUL DUP5 ADD GT PUSH5 0x100000000 DUP4 GT OR ISZERO PUSH2 0x169 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST SWAP2 SWAP1 DUP1 DUP1 PUSH1 0x1F ADD PUSH1 0x20 DUP1 SWAP2 DIV MUL PUSH1 0x20 ADD PUSH1 0x40 MLOAD SWAP1 DUP2 ADD PUSH1 0x40 MSTORE DUP1 SWAP4 SWAP3 SWAP2 SWAP1 DUP2 DUP2 MSTORE PUSH1 0x20 ADD DUP4 DUP4 DUP1 DUP3 DUP5 CALLDATACOPY PUSH1 0x0 DUP2 DUP5 ADD MSTORE PUSH1 0x1F NOT PUSH1 0x1F DUP3 ADD AND SWAP1 POP DUP1 DUP4 ADD SWAP3 POP POP POP POP POP POP POP SWAP2 SWAP3 SWAP2 SWAP3 SWAP1 POP POP POP PUSH2 0x22B JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 PUSH28 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF NOT AND PUSH28 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF NOT AND DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST DUP1 PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH1 0xFF MUL NOT AND SWAP1 DUP4 ISZERO ISZERO MUL OR SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH1 0x0 DUP1 PUSH1 0x0 SWAP1 SLOAD SWAP1 PUSH2 0x100 EXP SWAP1 DIV PUSH1 0xFF AND ISZERO PUSH2 0x250 JUMPI PUSH4 0x20C13B0B PUSH1 0xE0 SHL SWAP1 POP PUSH2 0x258 JUMP JUMPDEST PUSH1 0x0 PUSH1 0xE0 SHL SWAP1 POP JUMPDEST SWAP3 SWAP2 POP POP JUMP INVALID LOG2 PUSH6 0x627A7A723158 KECCAK256 SWAP14 PUSH27 0xA06B7443AA12CEE8B1BA4356AF624DE6B912D6AF47B494C9B9D621 0xb8 DUP4 0xac PUSH5 0x736F6C6343 STOP SDIV SIGNEXTEND STOP ORIGIN ", "sourceMap": "0:350:0:-;;;46:5;31:20;;;;;;;;;;;;;;;;;;;;0:350;8:9:-1;5:2;;;30:1;27;20:12;5:2;0:350:0;;;;;;;" } 12 | 13 | const GANACHE_CONF = { 14 | seed: '0xd30553e27ba2954e3736dae1342f5495798d4f54012787172048582566938f6f', 15 | } 16 | const GANACHE_CHAIN_ID = 1337 17 | const send = (provider, data): Promise => new Promise((resolve, reject) => provider.send(data, (err, res) => { 18 | if (err) reject(err) 19 | else resolve(res.result) 20 | })) 21 | 22 | describe('Blockchain: Ethereum', () => { 23 | let provider, addresses, eoaProof, contractAddress, erc1271Proof 24 | 25 | const testDid = 'did:3:bafysdfwefwe' 26 | 27 | beforeAll(async () => { 28 | provider = ganache.provider(GANACHE_CONF) 29 | addresses = await send(provider, encodeRpcMessage('eth_accounts')) 30 | // ganache-core doesn't support personal_sign -.- 31 | provider.manager.personal_sign = (data, address, callback): void => { // eslint-disable-line @typescript-eslint/camelcase 32 | // next line is hack to make contract address to personal sign 33 | if (address === contractAddress.toLowerCase()) address = addresses[0] 34 | const account = provider.manager.state.accounts[address.toLowerCase()] 35 | const result = sigUtils.personalSign(account.secretKey, { data }) 36 | callback(null, result) 37 | } 38 | // deploy contract wallet 39 | const factory = new ContractFactory(CONTRACT_WALLET_ABI, CONTRACT_WALLET_BYTECODE) 40 | const unsignedTx = Object.assign(factory.getDeployTransaction(), { from: addresses[0], gas: 4712388, gasPrice: 100000000000, nonce: 0 }) 41 | await send(provider, encodeRpcMessage('eth_sendTransaction', [unsignedTx])) 42 | contractAddress = Contract.getContractAddress(unsignedTx) 43 | // mock ethers providers 44 | providers.getNetwork = (): any => { 45 | return { 46 | _defaultProvider: (): any => { 47 | return new providers.Web3Provider(provider) 48 | } 49 | } 50 | } 51 | }) 52 | 53 | it('isEthAddress: should detect eth address correctly', async () => { 54 | const notEthAddr = '0xabc123' 55 | expect(await ethereum.isEthAddress(notEthAddr, provider)).toBeFalsy() 56 | expect(await ethereum.isEthAddress(addresses[0], provider)).toBeTruthy() 57 | }) 58 | 59 | it('isERC1271: should detect erc1271 address', async () => { 60 | const acc1 = new AccountID({ address: addresses[0], chainId: 'eip155:1' }) 61 | expect(await ethereum.isERC1271(acc1, provider)).toEqual(false) 62 | const acc2 = new AccountID({ address: contractAddress, chainId: 'eip155:1' }) 63 | expect(await ethereum.isERC1271(acc2, provider)).toEqual(true) 64 | }) 65 | 66 | it('createLink: should create ethereumEOA proof correctly', async () => { 67 | const acc = new AccountID({ address: addresses[0], chainId: 'eip155:1' }) 68 | // skip timestamp because it's a pain to test 69 | eoaProof = await ethereum.createLink(testDid, acc, provider, { skipTimestamp: true }) 70 | expect(eoaProof).toMatchSnapshot() 71 | }) 72 | 73 | it('createLink: should create erc1271 proof correctly', async () => { 74 | // In reality personal_sign is implemented differently by each contract wallet. 75 | // However the correct signature should still be returned. Here we simply test 76 | // that the proof is constructed correctly. 77 | const acc = new AccountID({ address: contractAddress, chainId: 'eip155:' + GANACHE_CHAIN_ID }) 78 | expect(await ethereum.createLink(testDid, acc, provider, { skipTimestamp: true })).toMatchSnapshot() 79 | }) 80 | 81 | it('createLink: should throw if erc1271 is on wrong chain', async () => { 82 | // In reality personal_sign is implemented differently by each contract wallet. 83 | // However the correct signature should still be returned. Here we simply test 84 | // that the proof is constructed correctly. 85 | const acc = new AccountID({ address: contractAddress, chainId: 'eip155:123' }) 86 | await expect(ethereum.createLink(testDid, acc, provider, { skipTimestamp: true })).rejects.toMatchSnapshot() 87 | }) 88 | 89 | it('validateLink: invalid ethereumEOA proof should return null', async () => { 90 | // wrong address 91 | const account = new AccountID({ address: addresses[1], chainId: 'eip155:1' }) 92 | let invalidProof = Object.assign({}, eoaProof, { account }) 93 | expect(await ethereum.validateLink(invalidProof)).toBeFalsy() 94 | // invalid signature 95 | invalidProof = Object.assign({}, eoaProof, { signature: '0xfa69ccf4a94db6132542abcabcabcab234b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4babc' }) 96 | expect(await ethereum.validateLink(invalidProof)).toBeFalsy() 97 | }) 98 | 99 | it('validateLink: valid ethereumEOA proof should return proof', async () => { 100 | expect(await ethereum.validateLink(eoaProof)).toEqual(eoaProof) 101 | }) 102 | 103 | it('validateLink: validate v0 and v1 proofs', async () => { 104 | expect(await ethereum.validateLink(proofs.v0.valid)).toMatchSnapshot() 105 | await expect(ethereum.validateLink(proofs.v0.invalid)).rejects.toMatchSnapshot() 106 | expect(await ethereum.validateLink(proofs.v1.valid)).toMatchSnapshot() 107 | expect(await ethereum.validateLink(proofs.v1.invalid)).toEqual(null) 108 | }) 109 | 110 | it('validateLink: invalid erc1271 proof should return null', async () => { 111 | // the contract wallet we deployed should just return false by default 112 | // when trying to validate signature 113 | const account = new AccountID({ address: contractAddress, chainId: 'eip155:' + GANACHE_CHAIN_ID }) 114 | erc1271Proof = Object.assign(eoaProof, { account, type: 'erc1271' }) 115 | expect(await ethereum.validateLink(erc1271Proof)).toBeFalsy() 116 | }) 117 | 118 | it('validateLink: valid erc1271 proof should return proof', async () => { 119 | // tell the contract wallet contract to return valid signature instead 120 | const contract = new Contract(contractAddress, CONTRACT_WALLET_ABI, new providers.Web3Provider(provider)) 121 | let tx = await contract.populateTransaction.setIsValid(true) 122 | tx = Object.assign(tx, { from: addresses[0], gas: 4712388, gasPrice: 100000000000 }) 123 | await send(provider, encodeRpcMessage('eth_sendTransaction', [tx])) 124 | expect(await ethereum.validateLink(erc1271Proof)).toEqual(erc1271Proof) 125 | }) 126 | 127 | it('authenticate: correctly signs auth message', async () => { 128 | const account = new AccountID({ address: addresses[1], chainId: 'eip155:1' }) 129 | expect(await ethereum.authenticate('msg', account, provider)).toMatchSnapshot() 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/filecoin.test.ts: -------------------------------------------------------------------------------- 1 | import {LocalManagedProvider} from "@glif/local-managed-provider"; 2 | import {Network} from "@glif/filecoin-address" 3 | import {authenticate, createLink, validateLink} from "../filecoin"; 4 | import {AccountID} from "caip"; 5 | 6 | const did = 'did:3:bafysdfwefwe' 7 | const testnetPrivateKey = '7b2254797065223a22736563703235366b31222c22507269766174654b6579223a2257587362654d5176487a366f5668344b637262633045642b31362b3150766a6a504f3753514931355031343d227d' 8 | const mainnetPrivateKey = '7b2254797065223a22736563703235366b31222c22507269766174654b6579223a2257587362654d5176487a366f5668344b637262633045642b31362b3150766a6a554f3753514931355031343d227d' 9 | const blsPrivateKey = "7b2254797065223a22626c73222c22507269766174654b6579223a226e586841424f4163796856504b48326155596261796f4475752f4c6f32515a2b6662622f6f736a2f34456f3d227d"; 10 | const testnetProvider = new LocalManagedProvider(testnetPrivateKey, Network.TEST) 11 | const mainnetProvider = new LocalManagedProvider(mainnetPrivateKey, Network.MAIN) 12 | const blsMainnetProvider = new LocalManagedProvider(blsPrivateKey, Network.MAIN) 13 | 14 | describe('createLink', () => { 15 | test('generate proof on testnet', async () => { 16 | const addresses = await testnetProvider.getAccounts() 17 | const account = new AccountID(`${addresses[0]}@fil:t`) 18 | const proof = await createLink(did, account, testnetProvider, {skipTimestamp: true}) 19 | expect(proof).toMatchSnapshot() 20 | }) 21 | 22 | test('generate proof on mainnet', async () => { 23 | const addresses = await mainnetProvider.getAccounts() 24 | const account = new AccountID(`${addresses[0]}@fil:f`) 25 | const proof = await createLink(did, account, mainnetProvider, {skipTimestamp: true}) 26 | expect(proof).toMatchSnapshot() 27 | }) 28 | 29 | test('generate proof on mainnet for BLS key', async () => { 30 | const addresses = await blsMainnetProvider.getAccounts() 31 | const account = new AccountID(`${addresses[0]}@fil:f`) 32 | const proof = await createLink(did, account, blsMainnetProvider, {skipTimestamp: true}) 33 | expect(proof).toMatchSnapshot() 34 | }) 35 | 36 | test('fail on mainnet address for testnet provider', async () => { 37 | const addresses = await mainnetProvider.getAccounts() 38 | const account = new AccountID(`${addresses[0]}@fil:f`) 39 | await expect(createLink(did, account, testnetProvider, {skipTimestamp: true})).rejects.toThrow() 40 | }) 41 | test('fail on testnet address for mainnet provider', async () => { 42 | const addresses = await testnetProvider.getAccounts() 43 | const account = new AccountID(`${addresses[0]}@fil:t`) 44 | await expect(createLink(did, account, mainnetProvider, {skipTimestamp: true})).rejects.toThrow() 45 | }) 46 | }) 47 | 48 | describe('validateLink', () => { 49 | test('testnet', async () => { 50 | const addresses = await testnetProvider.getAccounts() 51 | const account = new AccountID(`${addresses[0]}@fil:t`) 52 | const proof = await createLink(did, account, testnetProvider, {skipTimestamp: true}) 53 | await expect(validateLink(proof)).resolves.toEqual(proof) 54 | }) 55 | 56 | test('mainnet', async () => { 57 | const addresses = await mainnetProvider.getAccounts() 58 | const account = new AccountID(`${addresses[0]}@fil:f`) 59 | const proof = await createLink(did, account, mainnetProvider, {skipTimestamp: true}) 60 | await expect(validateLink(proof)).resolves.toEqual(proof) 61 | 62 | const testAddr = await testnetProvider.getAccounts() 63 | const testAcc = new AccountID(`${testAddr[0]}@fil:t`) 64 | proof.account = testAcc.toString() 65 | await expect(validateLink(proof)).resolves.toEqual(null) 66 | }) 67 | 68 | test('mainnet BLS', async () => { 69 | const addresses = await blsMainnetProvider.getAccounts() 70 | const account = new AccountID(`${addresses[0]}@fil:f`) 71 | const proof = await createLink(did, account, blsMainnetProvider, {skipTimestamp: true}) 72 | await expect(validateLink(proof)).resolves.toEqual(proof) 73 | 74 | const testAddr = await testnetProvider.getAccounts() 75 | const testAcc = new AccountID(`${testAddr[0]}@fil:t`) 76 | proof.account = testAcc.toString() 77 | await expect(validateLink(proof)).resolves.toEqual(null) 78 | }) 79 | }) 80 | 81 | describe('authenticate', () => { 82 | test('testnet', async () => { 83 | const addresses = await testnetProvider.getAccounts() 84 | const account = new AccountID(`${addresses[0]}@fil:t`) 85 | expect(await authenticate('msg', account, testnetProvider)).toMatchSnapshot() 86 | }) 87 | 88 | test('mainnet', async () => { 89 | const addresses = await mainnetProvider.getAccounts() 90 | const account = new AccountID(`${addresses[0]}@fil:m`) 91 | expect(await authenticate('msg', account, mainnetProvider)).toMatchSnapshot() 92 | }) 93 | 94 | test('mainnet BLS', async () => { 95 | const addresses = await blsMainnetProvider.getAccounts() 96 | const account = new AccountID(`${addresses[0]}@fil:m`) 97 | expect(await authenticate('msg', account, blsMainnetProvider)).toMatchSnapshot() 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/fixtures.ts: -------------------------------------------------------------------------------- 1 | const proofs = { 2 | v0: { 3 | valid: { 4 | type: 'ethereum-eoa', 5 | message: 'Create a new 3Box profile\n\n- \nYour unique profile ID is did:3:bafysdfwefwe', 6 | signature: '0xfa69ccf4a94db61325429d37c58c6de534b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c', 7 | }, 8 | invalid: { 9 | type: 'ethereum-eoa', 10 | message: 'Create a new 3Box profile\n\n- \nYour unique profile ID is did:3:bafysdfwefwe', 11 | signature: '0xfa69ccf4a94db6139837459873459873498759834500fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c', 12 | } 13 | }, 14 | v1: { 15 | valid: { 16 | version: 1, 17 | type: 'ethereum-eoa', 18 | message: 'Create a new 3Box profile\n\n- \nYour unique profile ID is did:3:bafysdfwefwe', 19 | signature: '0xfa69ccf4a94db61325429d37c58c6de534b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c', 20 | address: '0x8fe2c4516e920425e177658aaac451ca0463ed69' 21 | }, 22 | invalid: { 23 | version: 1, 24 | type: 'ethereum-eoa', 25 | message: 'Create a new 3Box profile\n\n- \nYour unique profile ID is did:3:bafysdfwefwe', 26 | signature: '0xfa69ccf4a94db61325429d37c58c6de534b73f439700fbb748209890a5780f3365a5335f82d424d7f9a63ee41b637c116e64ef2f32c761bb065e4409f978c4bb1c', 27 | address: '0x8fe2c4516e920425e177658aaac451ca0463ed87' 28 | } 29 | } 30 | } 31 | 32 | export default proofs 33 | 34 | it.skip('', () => {}) // eslint-disable-line @typescript-eslint/no-empty-function 35 | -------------------------------------------------------------------------------- /src/blockchains/__tests__/polkadot.test.ts: -------------------------------------------------------------------------------- 1 | import polkadot from "../polkadot"; 2 | import { AccountID } from "caip"; 3 | import { Signer, SignerResult } from '@polkadot/api/types'; 4 | import { KeyringPair } from '@polkadot/keyring/types'; 5 | import { SignerPayloadRaw } from '@polkadot/types/types'; 6 | import { TypeRegistry } from '@polkadot/types/create'; 7 | import createTestKeyring from '@polkadot/keyring/testing'; 8 | import { assert, hexToU8a, u8aToHex } from '@polkadot/util'; 9 | import { waitReady } from '@polkadot/wasm-crypto'; 10 | 11 | const did = 'did:3:bafysdfwefwe' 12 | const net = 'polkadot:b0a8d493285c2df73290dfb7e61f870f' 13 | const seed = hexToU8a('0xabf8e00000000000000000000000000000000000000000000000000000000000') 14 | 15 | // primary and supported by polkadot extension 16 | const keyringSr25519 = createTestKeyring({ type: 'sr25519' }) 17 | const keyringEd25519 = createTestKeyring({ type: 'ed25519' }) 18 | const keyringSecp256k = createTestKeyring({ type: 'ecdsa' }) 19 | 20 | const addressToAccountID = (address: string): AccountID => new AccountID(`${address}@${net}`) 21 | 22 | class SingleAccountSigner implements Signer { 23 | readonly #keyringPair: KeyringPair; 24 | 25 | constructor (registry: TypeRegistry, keyringPair: KeyringPair) { 26 | this.#keyringPair = keyringPair 27 | } 28 | 29 | public async signRaw ({ address, data }: SignerPayloadRaw): Promise { 30 | assert(address === this.#keyringPair.address, 'Signer does not have the keyringPair'); 31 | 32 | return new Promise((resolve): void => { 33 | const signature = u8aToHex(this.#keyringPair.sign(hexToU8a(data))); 34 | resolve({ id: 1, signature }) 35 | }) 36 | } 37 | } 38 | 39 | describe('Blockchain: Polkadot', () => { 40 | const registry = new TypeRegistry() 41 | let keyPairSr25519, keyPairEd25519, keyPairSecp256k 42 | 43 | beforeAll(async () => { 44 | await waitReady(); 45 | keyPairSr25519 = keyringSr25519.addFromSeed(seed) 46 | keyPairEd25519 = keyringEd25519.addFromSeed(seed) 47 | keyPairSecp256k = keyringSecp256k.addFromSeed(seed) 48 | }) 49 | 50 | describe('createLink', () => { 51 | test('create proof with sr25519', async () => { 52 | const account = addressToAccountID(keyPairSr25519.address) 53 | const provider = new SingleAccountSigner(registry, keyPairSr25519) 54 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 55 | expect(proof.account).toMatchSnapshot() 56 | expect(proof.message).toMatchSnapshot() 57 | expect(proof.type).toMatchSnapshot() 58 | }) 59 | test('create proof with ed25519', async () => { 60 | const account = addressToAccountID(keyPairEd25519.address) 61 | const provider = new SingleAccountSigner(registry, keyPairEd25519) 62 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 63 | expect(proof).toMatchSnapshot() 64 | }) 65 | test('create proof with secp256k', async () => { 66 | const account = addressToAccountID(keyPairSecp256k.address) 67 | const provider = new SingleAccountSigner(registry, keyPairSecp256k) 68 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 69 | expect(proof).toMatchSnapshot() 70 | }) 71 | }) 72 | 73 | describe('validateLink', () => { 74 | test('validate proof with sr25519', async () => { 75 | const account = addressToAccountID(keyPairSr25519.address) 76 | const provider = new SingleAccountSigner(registry, keyPairSr25519) 77 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 78 | await expect(polkadot.validateLink(proof)).resolves.toEqual(proof) 79 | }) 80 | test('validate proof with ed25519', async () => { 81 | const account = addressToAccountID(keyPairEd25519.address) 82 | const provider = new SingleAccountSigner(registry, keyPairEd25519) 83 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 84 | await expect(polkadot.validateLink(proof)).resolves.toEqual(proof) 85 | }) 86 | test('validate proof with secp256k', async () => { 87 | const account = addressToAccountID(keyPairSecp256k.address) 88 | const provider = new SingleAccountSigner(registry, keyPairSecp256k) 89 | const proof = await polkadot.createLink(did, account, provider, {skipTimestamp: true}) 90 | await expect(polkadot.validateLink(proof)).resolves.toEqual(proof) 91 | }) 92 | }) 93 | 94 | // describe('authenticate', () => { 95 | // test('authenticate with sr25519', async () => { 96 | // const account = addressToAccountID(keyPairSr25519.address) 97 | // const provider = new SingleAccountSigner(registry, keyPairSr25519) 98 | // const authSecret = await polkadot.authenticate('msg', account, provider) 99 | // expect(authSecret).toMatchSnapshot() 100 | // }) 101 | // test('authenticate with ed25519', async () => { 102 | // const account = addressToAccountID(keyPairEd25519.address) 103 | // const provider = new SingleAccountSigner(registry, keyPairEd25519) 104 | // const authSecret = await polkadot.authenticate('msg', account, provider) 105 | // expect(authSecret).toMatchSnapshot() 106 | // }) 107 | // test('authenticate with secp256k', async () => { 108 | // const account = addressToAccountID(keyPairSecp256k.address) 109 | // const provider = new SingleAccountSigner(registry, keyPairSecp256k) 110 | // const authSecret = await polkadot.authenticate('msg', account, provider) 111 | // expect(authSecret).toMatchSnapshot() 112 | // }) 113 | // }) 114 | 115 | }) 116 | -------------------------------------------------------------------------------- /src/blockchains/eosio.ts: -------------------------------------------------------------------------------- 1 | import { AccountID } from 'caip' 2 | import {SigningTools} from '@smontero/eosio-signing-tools' 3 | import {BlockchainHandler, BlockchainHandlerOpts} from "../blockchain-handler"; 4 | import { getConsentMessage, LinkProof } from '../utils' 5 | import { sha256 } from 'js-sha256' 6 | 7 | const maxWordLength = 12 8 | const namespace = 'eosio' 9 | 10 | function normalizeAccountId (account: AccountID): AccountID { 11 | account.address = account.address.toLowerCase() 12 | return account 13 | } 14 | 15 | function toCAIPChainId(chainId: string): string{ 16 | return chainId.substr(0,32) 17 | } 18 | 19 | function sanitize (str: string, size: number): string{ 20 | return str.replace(/\s/g, ' ').replace(new RegExp(`(\\S{${size}})`, 'g'), '$1 ') 21 | } 22 | 23 | function toPayload(message: string, accountID: AccountID ): string { 24 | const { 25 | address, 26 | chainId 27 | } = accountID 28 | const payload = `${message} [For: ${address} on chain: ${chainId}]` 29 | return sanitize(payload, maxWordLength) 30 | } 31 | 32 | async function toSignedPayload( 33 | message: string, 34 | accountID: AccountID, 35 | provider: any 36 | ): Promise { 37 | accountID = normalizeAccountId(accountID) 38 | const { 39 | chainId:{ 40 | reference:requestedChainId 41 | }, 42 | address 43 | } = accountID 44 | const accountName = await provider.getAccountName() 45 | const chainId = toCAIPChainId(await provider.getChainId()) 46 | 47 | if(chainId !== requestedChainId){ 48 | throw new Error(`Provider returned a different chainId than requested [returned: ${chainId}, requested: ${requestedChainId}]`) 49 | } 50 | if(accountName !== address){ 51 | throw new Error(`Provider returned a different account than requested [returned: ${accountName}, requested: ${address}]`) 52 | } 53 | const payload = toPayload(message, accountID) 54 | const [key] = await provider.getKeys() 55 | return provider.signArbitrary(key, payload) 56 | } 57 | 58 | export async function authenticate( 59 | message: string, 60 | accountID: AccountID, 61 | provider: any 62 | ): Promise { 63 | const signedPayload = await toSignedPayload(message, accountID, provider) 64 | return `0x${sha256(signedPayload)}` 65 | } 66 | 67 | export async function createLink (did: string, accountID: AccountID, provider: any, opts: BlockchainHandlerOpts): Promise { 68 | const consentMessage = getConsentMessage(did, !opts.skipTimestamp) 69 | const signedPayload = await toSignedPayload(consentMessage.message, accountID, provider) 70 | const proof: LinkProof = { 71 | version: 1, 72 | type: 'eosio', 73 | message: consentMessage.message, 74 | signature: signedPayload, 75 | account: accountID.toString() 76 | } 77 | if (!opts.skipTimestamp) proof.timestamp = consentMessage.timestamp 78 | return proof 79 | } 80 | 81 | export async function validateLink (proof: LinkProof): Promise { 82 | const { 83 | message, 84 | signature, 85 | account 86 | } = proof 87 | const accountID = new AccountID(account) 88 | const {address, chainId} = accountID 89 | try { 90 | const success = await SigningTools.verifySignature({ 91 | chainId: chainId.reference, 92 | account: address, 93 | signature, 94 | data: toPayload(message, accountID) 95 | }) 96 | return success ? proof : null 97 | } catch (error) { 98 | console.warn(error) 99 | return null 100 | } 101 | } 102 | 103 | const Handler: BlockchainHandler = { 104 | namespace, 105 | authenticate, 106 | validateLink, 107 | createLink 108 | } 109 | 110 | export default Handler 111 | -------------------------------------------------------------------------------- /src/blockchains/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { getConsentMessage, encodeRpcMessage, RpcMessage, LinkProof } from '../utils' 2 | import { verifyMessage } from '@ethersproject/wallet' 3 | import { Contract } from '@ethersproject/contracts' 4 | import * as providers from "@ethersproject/providers" 5 | import { AccountID } from 'caip' 6 | import { sha256 } from 'js-sha256' 7 | import * as uint8arrays from 'uint8arrays' 8 | 9 | const ADDRESS_TYPES = { 10 | ethereumEOA: 'ethereum-eoa', 11 | erc1271: 'erc1271' 12 | } 13 | const ERC1271_ABI = [ 'function isValidSignature(bytes _messageHash, bytes _signature) public view returns (bytes4 magicValue)' ] 14 | const MAGIC_ERC1271_VALUE = '0x20c13b0b' 15 | const isEthAddress = (address: string): boolean => /^0x[a-fA-F0-9]{40}$/.test(address) 16 | const namespace = 'eip155' 17 | 18 | 19 | function normalizeAccountId (account: AccountID): AccountID { 20 | account.address = account.address.toLowerCase() 21 | return account 22 | } 23 | 24 | function utf8toHex(message: string): string { 25 | const bytes = uint8arrays.fromString(message) 26 | const hex = uint8arrays.toString(bytes, 'base16') 27 | return '0x' + hex 28 | } 29 | 30 | async function safeSend (data: RpcMessage, provider: any): Promise { 31 | const send = (provider.sendAsync ? provider.sendAsync : provider.send).bind(provider) 32 | return new Promise((resolve, reject) => { 33 | send(data, function(err: any, result: any) { 34 | if (err) reject(err) 35 | else if (result.error) reject(result.error) 36 | else resolve(result.result) 37 | }) 38 | }) 39 | } 40 | 41 | function getEthersProvider (chainId: string): any { 42 | const network = providers.getNetwork(chainId) 43 | if (!network._defaultProvider) throw new Error(`Network with chainId ${chainId} is not supported`) 44 | return network._defaultProvider(providers) 45 | } 46 | 47 | async function getCode (address: string, provider: any): Promise { 48 | const payload = encodeRpcMessage('eth_getCode', [address, 'latest']) 49 | const code = await safeSend(payload, provider) 50 | return code 51 | } 52 | 53 | async function validateChainId (account: AccountID, provider: any): Promise { 54 | const payload = encodeRpcMessage('eth_chainId', []) 55 | const chainIdHex = await safeSend(payload, provider) 56 | const chainId = parseInt(chainIdHex, 16) 57 | if (chainId !== parseInt(account.chainId.reference)) { 58 | throw new Error(`ChainId in provider (${chainId}) is different from AccountID (${account.chainId.reference})`) 59 | } 60 | } 61 | 62 | async function createEthLink ( 63 | did: string, 64 | account: AccountID, 65 | provider: any, 66 | opts: any = {} 67 | ): Promise { 68 | const { message, timestamp } = getConsentMessage(did, !opts.skipTimestamp) 69 | const hexMessage = utf8toHex(message) 70 | const payload = encodeRpcMessage('personal_sign', [hexMessage, account.address]) 71 | const signature = await safeSend(payload, provider) 72 | const proof: LinkProof = { 73 | version: 2, 74 | type: ADDRESS_TYPES.ethereumEOA, 75 | message, 76 | signature, 77 | account: account.toString() 78 | } 79 | if (!opts.skipTimestamp) proof.timestamp = timestamp 80 | return proof 81 | } 82 | 83 | async function createErc1271Link ( 84 | did: string, 85 | account: AccountID, 86 | provider: any, 87 | opts: any 88 | ): Promise { 89 | const ethLinkAccount = opts?.eoaSignAccount || account 90 | const res = await createEthLink(did, ethLinkAccount, provider, opts) 91 | await validateChainId(account, provider) 92 | return Object.assign(res, { 93 | type: ADDRESS_TYPES.erc1271, 94 | account: account.toString() 95 | }) 96 | } 97 | 98 | async function isERC1271 (account: AccountID, provider: any): Promise { 99 | const bytecode = await getCode(account.address, provider).catch(() => null) 100 | return Boolean(bytecode && bytecode !== '0x' && bytecode !== '0x0' && bytecode !== '0x00') 101 | } 102 | 103 | async function createLink ( 104 | did: string, 105 | account: AccountID, 106 | provider: any, 107 | opts: any 108 | ): Promise { 109 | account = normalizeAccountId(account) 110 | if (await isERC1271(account, provider)) { 111 | return createErc1271Link(did, account, provider, opts) 112 | } else { 113 | return createEthLink(did, account, provider, opts) 114 | } 115 | } 116 | 117 | function toV2Proof (proof: LinkProof, address?: string): LinkProof { 118 | proof.account = new AccountID({ 119 | address: ((proof.version === 1) ? proof.address : address) || '', 120 | chainId: { namespace, reference: proof.chainId ? proof.chainId.toString() : '1' } 121 | }).toString() 122 | delete proof.address 123 | delete proof.chainId 124 | proof.version = 2 125 | return proof 126 | } 127 | 128 | async function validateEoaLink (proof: LinkProof): Promise { 129 | const recoveredAddr = verifyMessage(proof.message, proof.signature).toLowerCase() 130 | if (proof.version !== 2) proof = toV2Proof(proof, recoveredAddr) 131 | const account = new AccountID(proof.account) 132 | if (account.address !== recoveredAddr) { 133 | return null 134 | } 135 | return proof 136 | } 137 | 138 | async function validateErc1271Link (proof: LinkProof): Promise { 139 | if (proof.version === 1) proof = toV2Proof(proof) 140 | const account = new AccountID(proof.account) 141 | const provider = getEthersProvider(account.chainId.reference) 142 | const contract = new Contract(account.address, ERC1271_ABI, provider) 143 | const message = utf8toHex(proof.message) 144 | const returnValue = await contract.isValidSignature(message, proof.signature) 145 | 146 | return returnValue === MAGIC_ERC1271_VALUE ? proof : null 147 | } 148 | 149 | async function validateLink (proof: LinkProof): Promise { 150 | if (proof.type === ADDRESS_TYPES.erc1271) { 151 | return validateErc1271Link(proof) 152 | } 153 | return validateEoaLink(proof) 154 | } 155 | 156 | async function authenticate( 157 | message: string, 158 | account: AccountID, 159 | provider: any 160 | ): Promise { 161 | if (account) account = normalizeAccountId(account) 162 | if (provider.isAuthereum) return provider.signMessageWithSigningKey(message) 163 | const hexMessage = utf8toHex(message) 164 | const payload = encodeRpcMessage('personal_sign', [hexMessage, account.address]) 165 | const signature = await safeSend(payload, provider) 166 | if (account) { 167 | const recoveredAddr = verifyMessage(message, signature).toLowerCase() 168 | if (account.address !== recoveredAddr) throw new Error('Provider returned signature from different account than requested') 169 | } 170 | return `0x${sha256(signature.slice(2))}` 171 | } 172 | 173 | export default { 174 | authenticate, 175 | validateLink, 176 | createLink, 177 | namespace, 178 | isERC1271, 179 | isEthAddress 180 | } 181 | -------------------------------------------------------------------------------- /src/blockchains/filecoin.ts: -------------------------------------------------------------------------------- 1 | import {BlockchainHandler, BlockchainHandlerOpts} from "../blockchain-handler"; 2 | import {AccountID} from "caip"; 3 | import {ConsentMessage, getConsentMessage, LinkProof} from "../utils"; 4 | import type { MessageParams } from '@zondax/filecoin-signing-tools' 5 | import * as uint8arrays from 'uint8arrays' 6 | 7 | const namespace = 'fil' 8 | 9 | function asTransaction(address: string, message: string): MessageParams { 10 | const messageParams = uint8arrays.toString(uint8arrays.fromString(message), 'base64') 11 | return { 12 | From: address, 13 | To: address, 14 | Value: "0", 15 | Method: 0, 16 | GasPrice: "1", 17 | GasLimit: 1000, 18 | Nonce: 0, 19 | Params: messageParams, 20 | GasFeeCap: "1", 21 | GasPremium: "1" 22 | } 23 | } 24 | 25 | export async function createLink (did: string, account: AccountID, provider: any, opts: BlockchainHandlerOpts): Promise { 26 | const { message, timestamp } = getConsentMessage(did, !opts?.skipTimestamp) 27 | const addresses = await provider.getAccounts() 28 | const payload = asTransaction(addresses[0], message) 29 | const signatureResponse = await provider.sign(account.address, payload) 30 | const proof: LinkProof = { 31 | version: 2, 32 | type: 'eoa-tx', 33 | message: message, 34 | signature: signatureResponse.Signature.Data, 35 | account: account.toString() 36 | } 37 | if (!opts?.skipTimestamp) proof.timestamp = timestamp 38 | return proof 39 | } 40 | 41 | export async function authenticate(message: string, account: AccountID, provider: any): Promise { 42 | const addresses = await provider.getAccounts() 43 | const payload = asTransaction(addresses[0], JSON.stringify(message)) 44 | const signatureResponse = await provider.sign(account.address, payload) 45 | return signatureResponse.Signature.Data 46 | } 47 | 48 | export async function validateLink (proof: LinkProof): Promise { 49 | const signingTools = await import("@zondax/filecoin-signing-tools") 50 | const account = new AccountID(proof.account) 51 | const payload = asTransaction(account.address, proof.message) 52 | const transaction = signingTools.transactionSerialize(payload); 53 | try { 54 | const recover = signingTools.verifySignature(proof.signature, transaction); 55 | if (recover) { 56 | return proof 57 | } else { 58 | return null 59 | } 60 | } catch { 61 | return null 62 | } 63 | } 64 | 65 | const Handler: BlockchainHandler = { 66 | namespace, 67 | authenticate, 68 | validateLink, 69 | createLink 70 | } 71 | 72 | export default Handler 73 | -------------------------------------------------------------------------------- /src/blockchains/polkadot.ts: -------------------------------------------------------------------------------- 1 | import {BlockchainHandler, BlockchainHandlerOpts} from "../blockchain-handler"; 2 | import {AccountID} from "caip"; 3 | import { getConsentMessage, LinkProof} from "../utils"; 4 | import { signatureVerify } from '@polkadot/util-crypto' 5 | import * as uint8arrays from 'uint8arrays' 6 | 7 | const namespace = 'polkadot' 8 | 9 | const stringHex = (str: string): string => `0x${uint8arrays.toString(uint8arrays.fromString(str), 'base16')}` 10 | 11 | async function createLink (did: string, account: AccountID, signer: any, opts: BlockchainHandlerOpts): Promise { 12 | const { message, timestamp } = getConsentMessage(did, !opts?.skipTimestamp) 13 | const linkMessageHex = stringHex(message) 14 | const res = await signer.signRaw({address: account.address, data: linkMessageHex}) 15 | const proof: LinkProof = { 16 | version: 2, 17 | type: 'eoa', 18 | message: linkMessageHex, 19 | signature: res.signature, 20 | account: account.toString() 21 | } 22 | if (!opts?.skipTimestamp) proof.timestamp = timestamp 23 | return proof 24 | } 25 | 26 | // polkadot sr25519 signatures inlcude randomness, need deterministic function to currently implment authentication 27 | async function authenticate(message: string, account: AccountID, signer: any): Promise { 28 | throw new Error('authenticate: polkadot authentication not yet supported') 29 | // const res = await signer.signRaw({address: account.address, data: stringHex(message)}) 30 | // return res.signature 31 | } 32 | 33 | async function validateLink (proof: LinkProof): Promise { 34 | const address = new AccountID(proof.account).address 35 | const res = await signatureVerify(proof.message, proof.signature, address) 36 | return res.isValid ? proof : null 37 | } 38 | 39 | const Handler: BlockchainHandler = { 40 | namespace, 41 | authenticate, 42 | validateLink, 43 | createLink 44 | } 45 | 46 | export default Handler 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AccountID } from 'caip' 2 | import { LinkProof } from './utils' 3 | import ethereum from './blockchains/ethereum' 4 | // import filecoin from './blockchains/filecoin' 5 | import polkadot from './blockchains/polkadot' 6 | import eosio from './blockchains/eosio' 7 | 8 | const findDID = (did: string): string | undefined => did.match(/(did:(3|muport):[a-zA-Z0-9])\w+/)?.[0] 9 | 10 | const handlers = { 11 | [ethereum.namespace]: ethereum, 12 | // [filecoin.namespace]: filecoin, 13 | [polkadot.namespace]: polkadot, 14 | [eosio.namespace]: eosio 15 | } 16 | 17 | async function createLink ( 18 | did: string, 19 | account: AccountID | string, 20 | provider: any, 21 | opts: any = {} 22 | ): Promise { 23 | if (typeof account === 'string') account = new AccountID(account) 24 | const handler = handlers[account.chainId.namespace] 25 | if (!handler) throw new Error(`creating link with namespace '${account.chainId.namespace}' is not supported`) 26 | const proof = await handler.createLink(did, account, provider, opts) 27 | if (proof) { 28 | return proof 29 | } else { 30 | throw new Error(`Unable to create proof with namespace '${account.chainId.namespace}'`) 31 | } 32 | } 33 | 34 | async function validateLink (proof: LinkProof): Promise { 35 | // version < 2 are always eip155 namespace 36 | let namespace = ethereum.namespace 37 | if (proof.version >= 2) { 38 | namespace = (new AccountID(proof.account)).chainId.namespace 39 | } 40 | const handler = handlers[namespace] 41 | if (!handler) throw new Error(`proof with namespace '${namespace}' not supported`) 42 | const validProof = await handler.validateLink(proof) 43 | if (validProof) { 44 | validProof.did = findDID(validProof.message) 45 | return validProof 46 | } else { 47 | return null 48 | } 49 | } 50 | 51 | async function authenticate ( 52 | message: string, 53 | account: AccountID | string, 54 | provider: any 55 | ): Promise { 56 | if (typeof account === 'string') account = new AccountID(account) 57 | const handler = handlers[account.chainId.namespace] 58 | if (!handler) throw new Error(`authenticate with namespace '${account.chainId.namespace}' not supported`) 59 | return handler.authenticate(message, account, provider) 60 | } 61 | 62 | export { 63 | LinkProof, 64 | createLink, 65 | validateLink, 66 | authenticate 67 | } 68 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | interface LinkProof { 2 | version: number; 3 | message: string; 4 | signature: string; 5 | account: string; 6 | did?: string; 7 | timestamp?: number; 8 | address?: string; 9 | type?: string; 10 | chainId?: number; 11 | } 12 | 13 | interface RpcMessage { 14 | jsonrpc: string; 15 | id: number; 16 | method: string; 17 | params: any; 18 | } 19 | 20 | export interface ConsentMessage { 21 | message: string; 22 | timestamp?: number; 23 | } 24 | 25 | function getConsentMessage (did: string, addTimestamp: boolean): ConsentMessage { 26 | const res: any = { 27 | message: 'Create a new account link to your identity.' + '\n\n' + did 28 | } 29 | if (addTimestamp) { 30 | res.timestamp = Math.floor(new Date().getTime() / 1000) 31 | res.message += ' \n' + 'Timestamp: ' + res.timestamp 32 | } 33 | return res 34 | } 35 | 36 | function encodeRpcMessage (method: string, params: any): any { 37 | return { 38 | jsonrpc: '2.0', 39 | id: 1, 40 | method, 41 | params 42 | } 43 | } 44 | 45 | export { LinkProof, RpcMessage, getConsentMessage, encodeRpcMessage } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["dom", "es2017"], 5 | "target": "es6", 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "strict": true, 9 | "removeComments": true, 10 | "esModuleInterop": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true, 14 | "baseUrl": "./packages", 15 | "outDir": "lib", 16 | "rootDir": "src", 17 | "typeRoots": [ 18 | "./@types", "./node_modules/@types" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "**/fixtures.ts", 24 | "**/*.spec.ts", 25 | "**/*.test.ts" 26 | ], 27 | "include": ["./src/**/*"] 28 | } 29 | --------------------------------------------------------------------------------