├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation_bug_report.md │ ├── feature_request.md │ └── questions.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── Technical_Design.md ├── examples ├── dapp │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── AccountsSection │ │ │ ├── AccountsListItem.tsx │ │ │ ├── AccountsSection.tsx │ │ │ ├── index.css │ │ │ └── index.ts │ │ ├── App.css │ │ ├── App.tsx │ │ ├── AppStateContext.ts │ │ ├── DappClientContext.ts │ │ ├── NewRequestSection │ │ │ ├── NewRequestSection.tsx │ │ │ ├── index.css │ │ │ └── index.ts │ │ ├── index.css │ │ ├── main.tsx │ │ ├── utils │ │ │ ├── makeContext.tsx │ │ │ ├── makeLocalStorageAppStateContext.ts │ │ │ └── useAsyncAction.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── wallet │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── AccountsSection │ │ ├── AccountsList.tsx │ │ ├── AccountsListItem.tsx │ │ ├── AccountsSection.tsx │ │ ├── index.css │ │ └── index.ts │ ├── App.css │ ├── App.tsx │ ├── AppStateContext.ts │ ├── ConnectionSection │ │ ├── AccountsList.tsx │ │ ├── AccountsListItem.tsx │ │ ├── ConnectionSection.tsx │ │ ├── NewConnection.tsx │ │ ├── UpdateConnection.tsx │ │ ├── index.css │ │ └── index.ts │ ├── PromptSection │ │ ├── PromptSection.tsx │ │ ├── SignAndSubmitTransactionRequestListItem.tsx │ │ ├── SignMessageRequestListItem.tsx │ │ ├── SignTransactionRequestListItem.tsx │ │ ├── index.css │ │ └── index.ts │ ├── WalletClientContext.ts │ ├── index.css │ ├── main.tsx │ ├── useAccounts.ts │ ├── utils │ │ ├── makeContext.tsx │ │ ├── makeLocalStorageAppStateContext.ts │ │ └── useAsyncAction.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── wallet.html ├── package.json ├── packages ├── api │ ├── .eslintrc.cjs │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── network.ts │ │ ├── responseBodies.ts │ │ ├── serialization.ts │ │ └── types │ │ │ ├── account.ts │ │ │ ├── dapp.ts │ │ │ ├── dappSpecificWallet.ts │ │ │ ├── index.ts │ │ │ ├── pairing.ts │ │ │ ├── response.ts │ │ │ ├── serialize.ts │ │ │ ├── signingRequest.ts │ │ │ ├── user.ts │ │ │ └── wallet.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── crypto │ ├── .eslintrc.cjs │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── crypto.test.ts │ │ ├── encrDecr.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── securedEnvelope.ts │ │ ├── utils.ts │ │ └── walletAccounts.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── dapp-sdk │ ├── .eslintrc.cjs │ ├── DOCS.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── MockWindowAPI.ts │ │ │ ├── client.test.ts │ │ │ ├── prompt.test.ts │ │ │ └── testUtils.ts │ │ ├── client.ts │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── prompt.ts │ │ ├── state.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── eslint-config │ ├── index.js │ └── package.json ├── eslint-plugin │ ├── index.js │ ├── package.json │ └── rules │ │ ├── no-client-date.js │ │ └── no-client-date.test.js ├── prettier-config │ ├── index.json │ └── package.json ├── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ └── package.json ├── wallet-api │ ├── .eslintrc.cjs │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── serialization │ │ │ ├── bcsSerialization.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── jsonPayload.ts │ │ │ ├── rawTxn.ts │ │ │ ├── signAndSubmitTransactionRequestArgs.ts │ │ │ ├── signTransactionRequestArgs.ts │ │ │ └── signTransactionResponseArgs.ts │ │ ├── types │ │ │ ├── account.ts │ │ │ ├── index.ts │ │ │ ├── jsonPayload.ts │ │ │ ├── message.ts │ │ │ ├── network.ts │ │ │ ├── requests │ │ │ │ ├── index.ts │ │ │ │ ├── signAndSubmitTransaction.ts │ │ │ │ ├── signMessage.ts │ │ │ │ └── signTransaction.ts │ │ │ ├── responses │ │ │ │ ├── index.ts │ │ │ │ ├── signAndSubmitTransaction.ts │ │ │ │ ├── signMessage.ts │ │ │ │ └── signTransaction.ts │ │ │ └── transactionOptions.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── makeFullMessage.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── wallet-sdk │ ├── .eslintrc.cjs │ ├── DOCS.md │ ├── README.md │ ├── package.json │ ├── src │ ├── __tests__ │ │ └── testUtils.ts │ ├── client.ts │ ├── index.ts │ ├── state.ts │ ├── types.ts │ └── utils.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── turbo.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a bug report to help improve Identity Connect 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🐛 Bug 11 | 12 | 14 | 15 | ## To reproduce 16 | 17 | **Code snippet to reproduce** 18 | ```Typescript 19 | // Your code goes here 20 | // Please make sure it does not require any external dependencies 21 | ``` 22 | 23 | **Stack trace/error message** 24 | ``` 25 | // Paste the output here 26 | ``` 27 | 28 | ## Expected Behavior 29 | 30 | 31 | 32 | ## System information 33 | 34 | **Please complete the following information:** 35 | - 36 | - 37 | 38 | 39 | ## Additional context 40 | 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Documentation/aptos.dev Bug report" 3 | about: Create a bug report to help improve the Aptos Developers' Website 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🐛 Bug 11 | 12 | 14 | 15 | ## Steps to reproduce 16 | 17 | 18 | 19 | ** Error message or problem ** 20 | ``` 21 | // Paste the output here 22 | ``` 23 | 24 | ## Expected Behavior 25 | 26 | 27 | 28 | ## System information 29 | 30 | **Please complete the following information:** 31 | - 32 | 33 | ## Additional context 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest a new feature for Identity Connect 4 | title: "[Feature Request]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 🚀 Feature Request 11 | 12 | 13 | 14 | ## Motivation 15 | 16 | **Is your feature request related to a problem? Please describe.** 17 | 18 | 19 | 20 | ## Pitch 21 | 22 | **Describe the solution you'd like** 23 | 24 | 25 | **Describe alternatives you've considered** 26 | 27 | 28 | **Are you willing to open a pull request?** (See [CONTRIBUTING](../../CONTRIBUTING.md)) 29 | 30 | ## Additional context 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Questions/Help 3 | about: If you have questions, please check Discord 4 | --- 5 | 6 | ## ❓ Questions and Help 7 | 8 | ### Please note that this issue tracker is not a help form and this issue will be closed. 9 | 10 | Please contact the development team on [Discord](https://discord.com/invite/aptoslabs) 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Test Plan 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | node_modules 4 | **/node_modules/ 5 | coverage 6 | **/coverage 7 | build 8 | .DS_Store 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | .yarn-integrity 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | *.zip 19 | 20 | # Xcode 21 | # 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | apps/mobile/xcuserdata 31 | *.xccheckout 32 | *.moved-aside 33 | DerivedData 34 | *.hmap 35 | *.ipa 36 | *.xcuserstate 37 | ios/.xcode.env.local 38 | 39 | # Android/IntelliJ 40 | # 41 | apps/mobile/.idea 42 | apps/mobile/.gradle 43 | apps/mobile/local.properties 44 | *.iml 45 | *.hprof 46 | 47 | # BUCK 48 | apps/mobile/buck-out/ 49 | apps/mobile/.buckd/ 50 | *.keystore 51 | apps/mobile/!debug.keystore 52 | 53 | # fastlane 54 | # 55 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 56 | # screenshots whenever they are needed. 57 | # For more information about the recommended setup visit: 58 | # https://docs.fastlane.tools/best-practices/source-control/ 59 | 60 | **/fastlane/report.xml 61 | **/fastlane/Preview.html 62 | **/fastlane/screenshots 63 | **/fastlane/test_output 64 | 65 | # Bundle artifact 66 | apps/mobile/*.jsbundle 67 | 68 | # Ruby / CocoaPods 69 | apps/mobile/ios/Pods/ 70 | apps/mobile/vendor/bundle/ 71 | 72 | # Turborepo 73 | .turbo 74 | **/.turbo 75 | .next/** 76 | **/*.log 77 | **/out/ 78 | 79 | # Webpack profiler 80 | stats.json 81 | apps/extension/dist 82 | 83 | # JetBrains IDE 84 | .idea 85 | 86 | # Playwright 87 | test-results 88 | playwright-report 89 | e2e/.cache 90 | 91 | # eslint 92 | .eslintcache 93 | 94 | # typescript 95 | *.tsbuildinfo 96 | 97 | dist/ 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect Docs 2 | 3 | This is a public repository for Identity Connect, that will contain docs, examples and source code for the SDKs. 4 | 5 | For SDK specific docs, please refer to the following links: 6 | 7 | - [Wallet SDK Docs](packages/wallet-sdk/DOCS.md) 8 | - [Dapp SDK Docs](packages/dapp-sdk/DOCS.md) 9 | 10 | Separately, see dashboard staging [here](https://identity-connect.staging.gcp.aptosdev.com/) 11 | 12 | This monorepo uses `turborepo`. 13 | 14 | ## Getting started 15 | 16 | 1. Install `node.js` 17 | 2. Clone the repo 18 | 3. run `yarn install` from the root directory 19 | 20 | ## Examples 21 | 22 | Under the `/examples` folder you can find an example wallet and an example dapp, to use as reference 23 | for adopting the IC SDK in your application. 24 | 25 | ## Packages 26 | 27 | - `eslint-config`: `eslint` configurations 28 | - `tsconfig`: `tsconfig.json`s used throughout the monorepo 29 | 30 | ## Why Identity Connect? 31 | 32 | In Web2, platforms like Google and Steam help connect users and their identities to their applications. Web3 traditionally has avoided identity management services and empowers users to own their identity via decentralized identity platforms such as self-custodial wallets and other identity management platforms. As a user interacts with an application on one device, their identity may be managed on another. This inherently creates a challenge for enabling seamless identity integration within an application. To address this, we created ************************************Identity Connect*******************,***************** to be used as a platform, and protocol for coordinating across identity services and applications. 33 | 34 | **Identity Connect has the following properties**: 35 | 36 | - A core protocol for routing requests from applications to identity providers that allow for proof of identity and signing transactions. 37 | - A platform for managing persistent connections across applications and identity services, e.g., wallets and custodial solutions. 38 | - Leverage existing identity solutions, such as OAuth, for coordination across user interfaces. This allows users to login from the application, identity services, or a dedicated front-end to establish and manage connections. 39 | 40 | The main difference between Identity Connect and [Aptos Wallet Adapter](https://aptos.dev/integration/wallet-adapter-concept/) is that IC allows cross-platform transaction signing (i.e. play a game on desktop app, but sign on mobile app), while wallet adapter requires same environment to enable signing. However, IC is now also integrated with Wallet Adapter, which helps to circumvent this issue. In other words, you can access IC (and it's associated advantages through Wallet Adapter. 41 | 42 | ## How does Identity Connect work? 43 | 44 | Identity Connect is a stateful service allowing asynchronous communication between dApps (desktop, mobile, browser) and wallets (custody, mobile, chrome extensions). Having a permanent stateful server as the relay between dApp and wallet allows Identity Connect to avoid any pitfalls of dropping requests and allows us to extend this to not only self-custody wallets but even identity brokering (’login with IC’), MPC, and custody services in the future. 45 | 46 | **Identity Connect V1** allows two ways to create a pairing between a dApp and a Wallet: 47 | 48 | 1. **Anonymous pairing** (via QR Code) 49 | 1. Enables users to connect an application with their wallet (most likely a mobile wallet) without registering their identity via OAuth. All transaction requests and payloads will be end-to-end encrypted to ensure privacy. 50 | 2. **Connected pairing** (via OAuth) 51 | 1. Users who opt-in can use their OAuth identity (like Google Sign-in) to store the identities and applications they’ve connected with, across all devices. This effectively avoids users needing to re-register their wallets with their applications after the initial connection. 52 | 2. The advantages of this are as follows: 53 | 1. The first time a user connects an application with their wallet, they will need a QR code or link to register the wallet on their mobile device with that application. 54 | 2. When a user revisits the application in the future, transaction signing notifications can be automatically sent to their device without having to scan a QR code again to register a connection. 55 | 56 | **The user flow** for connecting and sending transactions from a dApp will look like this: 57 | 58 | 1. Dapp presents an [OAuth](https://en.wikipedia.org/wiki/OAuth) prompt (”Sign-in with Gmail” or another OAuth provider) to sign into Identity Connect. 59 | 2. User selects an existing account attached to their Identity Connect. They must connect a wallet if this is their first time using Identity Connect. 60 | 3. The selected wallet receives a notification and prompts the user to “Connect” to the dApp. 61 | 4. The dApp can now send transactions to Identity Connect, which will be routed to the wallet to be signed by the user. Users can get signing notifications (”notifications to sign a transaction”) for the same account in multiple wallets thanks to the Identity-Connect (IC) abstraction around accounts. It is sufficient for a wallet to prove it has control over a private key to start getting notifications for that account. 62 | 63 | ```mermaid 64 | %% Flowchart 65 | graph TD 66 | Dapp1[Dapp] <-->|Encrypted| IC(IC) 67 | Dapp2[Dapp] <-->|Encrypted| IC(IC) 68 | IC <--> |Encrypted|User{User} 69 | User{User} <--> Wallet1[Wallet] 70 | User{User} <--> Wallet2[Wallet] 71 | Wallet1[Wallet] <--> Account1[Account] 72 | Wallet1[Wallet] <--> Account2[Account] 73 | Wallet2[Wallet] <--> Account1[Account] 74 | ``` 75 | ## Other Technical Details 76 | To read more about technical design, go to [Technical Design](Technical_Design.md) 77 | 78 | -------------------------------------------------------------------------------- /examples/dapp/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const devTools = ['./vite.config.ts']; 5 | 6 | module.exports = { 7 | env: { browser: true, es2020: true }, 8 | extends: ['@identity-connect/eslint-config'], 9 | parserOptions: { 10 | project: ['tsconfig.json', 'tsconfig.node.json'], 11 | }, 12 | plugins: ['react-refresh'], 13 | root: true, 14 | rules: { 15 | 'import/extensions': 'off', 16 | 'import/no-extraneous-dependencies': ['error', { devDependencies: devTools }], 17 | 'react-refresh/only-export-components': 'warn', 18 | 'react/react-in-jsx-scope': 'off', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/dapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/dapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example Dapp 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/dapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-dapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --force", 8 | "build": "tsc && vite build", 9 | "lint": "prettier --check src", 10 | "lint:fix": "prettier --write src && eslint --cache ./src --fix", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@identity-connect/api": "*", 15 | "@identity-connect/dapp-sdk": "*", 16 | "@identity-connect/wallet-api": "*", 17 | "aptos": "^1.20.0", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@identity-connect/eslint-config": "*", 23 | "@types/react": "18.0.28", 24 | "@types/react-dom": "^18.0.11", 25 | "@vitejs/plugin-react-swc": "^3.0.0", 26 | "eslint": "8.36.0", 27 | "eslint-plugin-react-refresh": "^0.3.4", 28 | "typescript": "5.0.4", 29 | "vite": "^4.3.9" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/dapp/src/AccountsSection/AccountsListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DappPairingData } from '@identity-connect/dapp-sdk'; 5 | import { useAppState } from '../AppStateContext.ts'; 6 | import { useDappClient } from '../DappClientContext.ts'; 7 | import useAsyncAction from '../utils/useAsyncAction.ts'; 8 | import './index.css'; 9 | 10 | export interface AccountsListItemProps { 11 | pairing: DappPairingData; 12 | } 13 | 14 | export default function AccountsListItem({ pairing }: AccountsListItemProps) { 15 | const dappClient = useDappClient(); 16 | const appState = useAppState(); 17 | 18 | const disconnect = useAsyncAction(async () => { 19 | await dappClient.disconnect(pairing.accountAddress); 20 | const activeAccountAddress = appState.get('activeAccountAddress'); 21 | if (activeAccountAddress === pairing.accountAddress) { 22 | appState.set('activeAccountAddress', undefined); 23 | } 24 | }); 25 | 26 | const offboard = async () => { 27 | await dappClient.offboard(pairing.accountAddress); 28 | const activeAccountAddress = appState.get('activeAccountAddress'); 29 | if (activeAccountAddress === pairing.accountAddress) { 30 | appState.set('activeAccountAddress', undefined); 31 | } 32 | }; 33 | 34 | const onSelect = () => { 35 | appState.set('activeAccountAddress', pairing.accountAddress); 36 | }; 37 | 38 | // Determine if this is the active account on the first render. 39 | // No need to handle re-renders as HTML will take care of updating all radio buttons 40 | // on change 41 | const initialActiveAccountAddress = appState.get('activeAccountAddress'); 42 | 43 | const collapsedAddress = pairing.accountAddress.slice(0, 18); 44 | 45 | return ( 46 |
  • 47 | {collapsedAddress} 48 | 54 | 57 | {pairing.dappWalletId ? ( 58 | 61 | ) : null} 62 |
  • 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /examples/dapp/src/AccountsSection/AccountsSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useAppState } from '../AppStateContext.ts'; 5 | import { useDappClient } from '../DappClientContext.ts'; 6 | import useAsyncAction from '../utils/useAsyncAction.ts'; 7 | import './index.css'; 8 | import AccountsListItem from './AccountsListItem.tsx'; 9 | 10 | export default function AccountsSection() { 11 | const dappClient = useDappClient(); 12 | const appState = useAppState(); 13 | const pairings = appState.watch('icPairings'); 14 | 15 | const connect = useAsyncAction(async () => { 16 | const accountAddress = await dappClient.connect(); 17 | if (accountAddress !== undefined) { 18 | appState.set('activeAccountAddress', accountAddress); 19 | } 20 | }); 21 | 22 | return ( 23 |
    24 |

    Accounts

    25 | 30 | 33 |
    34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/dapp/src/AccountsSection/index.css: -------------------------------------------------------------------------------- 1 | .accounts-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1em; 5 | } 6 | 7 | .accounts-list-item { 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | gap: 1em; 15 | } 16 | -------------------------------------------------------------------------------- /examples/dapp/src/AccountsSection/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import AccountsSection from './AccountsSection.tsx'; 5 | 6 | export default AccountsSection; 7 | -------------------------------------------------------------------------------- /examples/dapp/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | flex-direction: row; 7 | } 8 | 9 | .layout { 10 | display: flex; 11 | flex-direction: row; 12 | gap: 2em; 13 | } 14 | 15 | .card { 16 | flex-grow: 1; 17 | flex-shrink: 0; 18 | padding: 2em; 19 | box-shadow: rgba(149, 157, 165, 0.2) 0 8px 24px; 20 | } 21 | -------------------------------------------------------------------------------- /examples/dapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import AccountsSection from './AccountsSection'; 5 | import NewRequestSection from './NewRequestSection'; 6 | import './App.css'; 7 | 8 | export default function App() { 9 | return ( 10 | <> 11 |

    Example Dapp

    12 |
    13 |
    14 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/dapp/src/AppStateContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DappPairingDataMap } from '@identity-connect/dapp-sdk'; 5 | import { NetworkName } from '@identity-connect/api'; 6 | import makeLocalStorageAppStateContext from './utils/makeLocalStorageAppStateContext.ts'; 7 | 8 | export interface AppState { 9 | activeAccountAddress?: string; 10 | icPairings: DappPairingDataMap; 11 | selectedNetwork: NetworkName; 12 | } 13 | 14 | const STORAGE_KEY_PREFIX = 'appState'; 15 | export const [AppStateContextProvider, useAppState] = makeLocalStorageAppStateContext(STORAGE_KEY_PREFIX, { 16 | icPairings: {}, 17 | selectedNetwork: NetworkName.TESTNET, 18 | }); 19 | -------------------------------------------------------------------------------- /examples/dapp/src/DappClientContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DappPairingData, ICDappClient } from '@identity-connect/dapp-sdk'; 5 | import { useMemo } from 'react'; 6 | import { useAppState } from './AppStateContext.ts'; 7 | import makeContext from './utils/makeContext.tsx'; 8 | 9 | const { VITE_DAPP_ID, VITE_IC_BACKEND_URL, VITE_IC_FRONTEND_URL } = import.meta.env; 10 | 11 | export const [DappClientContextProvider, useDappClient] = makeContext('DappClientContext', () => { 12 | const appState = useAppState(); 13 | 14 | const selectedNetwork = appState.watch('selectedNetwork'); 15 | return useMemo(() => { 16 | const accessors = { 17 | get: async (address: string) => { 18 | const pairings = appState.get('icPairings'); 19 | return pairings[address]; 20 | }, 21 | getAll: async () => appState.get('icPairings'), 22 | update: async (address: string, newValue?: DappPairingData) => { 23 | const pairings = appState.get('icPairings'); 24 | if (newValue === undefined) { 25 | delete pairings[address]; 26 | } else { 27 | pairings[address] = newValue; 28 | } 29 | appState.set('icPairings', pairings); 30 | }, 31 | }; 32 | 33 | if (VITE_DAPP_ID === undefined) { 34 | throw new Error('VITE_DAPP_ID env variable not provided'); 35 | } 36 | 37 | return new ICDappClient(VITE_DAPP_ID, { 38 | accessors, 39 | axiosConfig: VITE_IC_BACKEND_URL ? { baseURL: VITE_IC_BACKEND_URL } : undefined, 40 | defaultNetworkName: appState.get('selectedNetwork'), 41 | frontendBaseURL: VITE_IC_FRONTEND_URL, 42 | }); 43 | }, [appState, selectedNetwork]); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/dapp/src/NewRequestSection/NewRequestSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable no-console */ 5 | 6 | import { AptosAccount, BCS, TransactionBuilder, TxnBuilderTypes } from 'aptos'; 7 | import { NetworkName } from '@identity-connect/api'; 8 | import { useAppState } from '../AppStateContext.ts'; 9 | import { useDappClient } from '../DappClientContext.ts'; 10 | import useAsyncAction from '../utils/useAsyncAction.ts'; 11 | import './index.css'; 12 | 13 | function makeJsonPayload() { 14 | return { 15 | arguments: ['0xb0b', 717], 16 | function: '0x1::coin::transfer', 17 | type: 'entry_function_payload' as const, 18 | type_arguments: ['0x1::aptos_coin::AptosCoin'], 19 | }; 20 | } 21 | 22 | function makeBcsPayload() { 23 | const typeArgs = [ 24 | new TxnBuilderTypes.TypeTagStruct(TxnBuilderTypes.StructTag.fromString('0x1::aptos_coin::AptosCoin')), 25 | ]; 26 | const encodedArgs = [BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex('0xb0b')), BCS.bcsSerializeUint64(717)]; 27 | const entryFunction = TxnBuilderTypes.EntryFunction.natural('0x1::coin', 'transfer', typeArgs, encodedArgs); 28 | return new TxnBuilderTypes.TransactionPayloadEntryFunction(entryFunction); 29 | } 30 | 31 | function makeRawTxn(signerAddress: string) { 32 | const payload = makeBcsPayload(); 33 | const expirationTimestamp = Math.ceil(Date.now() / 1000) + 60; 34 | return new TxnBuilderTypes.RawTransaction( 35 | TxnBuilderTypes.AccountAddress.fromHex(signerAddress), 36 | 0n, 37 | payload, 38 | 0n, 39 | 0n, 40 | BigInt(expirationTimestamp), 41 | new TxnBuilderTypes.ChainId(2), 42 | ); 43 | } 44 | 45 | export default function NewRequestSection() { 46 | const dappClient = useDappClient(); 47 | const appState = useAppState(); 48 | 49 | const activeAccountAddress = appState.watch('activeAccountAddress'); 50 | 51 | const signMessage = useAsyncAction(async () => { 52 | if (activeAccountAddress === undefined) { 53 | return; 54 | } 55 | 56 | const response = await dappClient.signMessage(activeAccountAddress, { 57 | message: 'testMessage', 58 | nonce: Date.now().toString(), 59 | }); 60 | console.log(response); 61 | }); 62 | 63 | const signAndSubmitJsonTransaction = useAsyncAction(async () => { 64 | if (activeAccountAddress === undefined) { 65 | return; 66 | } 67 | const payload = makeJsonPayload(); 68 | const response = await dappClient.signAndSubmitTransaction(activeAccountAddress, { payload }); 69 | console.log(response); 70 | }); 71 | 72 | const signAndSubmitBcsTransaction = useAsyncAction(async () => { 73 | if (activeAccountAddress === undefined) { 74 | return; 75 | } 76 | const payload = makeBcsPayload(); 77 | const response = await dappClient.signAndSubmitTransaction(activeAccountAddress, { payload }); 78 | console.log(response); 79 | }); 80 | 81 | const signAndSubmitFeePayerTransaction = useAsyncAction(async () => { 82 | if (activeAccountAddress === undefined) { 83 | return; 84 | } 85 | const rawTxn = makeRawTxn(activeAccountAddress); 86 | const feePayer = new AptosAccount(); 87 | const feePayerRawTxn = new TxnBuilderTypes.FeePayerRawTransaction( 88 | rawTxn, 89 | [], 90 | TxnBuilderTypes.AccountAddress.fromHex(feePayer.address()), 91 | ); 92 | 93 | const txnSigningMessage = TransactionBuilder.getSigningMessage(feePayerRawTxn); 94 | const feePayerSignature = feePayer.signBuffer(txnSigningMessage); 95 | const feePayerSignatureBytes = feePayerSignature.toUint8Array(); 96 | const feePayerAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( 97 | new TxnBuilderTypes.Ed25519PublicKey(feePayer.signingKey.publicKey), 98 | new TxnBuilderTypes.Ed25519Signature(feePayerSignatureBytes), 99 | ); 100 | 101 | const response = await dappClient.signAndSubmitTransaction(activeAccountAddress, { 102 | feePayerAuthenticator, 103 | rawTxn: feePayerRawTxn, 104 | }); 105 | console.log(response); 106 | }); 107 | 108 | const signJsonTransaction = useAsyncAction(async () => { 109 | if (activeAccountAddress === undefined) { 110 | return; 111 | } 112 | const payload = makeJsonPayload(); 113 | const response = await dappClient.signTransaction(activeAccountAddress, { payload }); 114 | console.log(response); 115 | }); 116 | 117 | const signBcsTransaction = useAsyncAction(async () => { 118 | if (activeAccountAddress === undefined) { 119 | return; 120 | } 121 | const payload = makeBcsPayload(); 122 | const response = await dappClient.signTransaction(activeAccountAddress, { payload }); 123 | console.log(response); 124 | }); 125 | 126 | const signRawTxn = useAsyncAction(async () => { 127 | if (activeAccountAddress === undefined) { 128 | return; 129 | } 130 | const rawTxn = makeRawTxn(activeAccountAddress); 131 | const response = await dappClient.signTransaction(activeAccountAddress, { rawTxn }); 132 | console.log(response); 133 | }); 134 | 135 | const signFeePayerTxn = useAsyncAction(async () => { 136 | if (activeAccountAddress === undefined) { 137 | return; 138 | } 139 | const rawTxn = makeRawTxn(activeAccountAddress); 140 | const feePayer = new AptosAccount(); 141 | const feePayerRawTxn = new TxnBuilderTypes.FeePayerRawTransaction( 142 | rawTxn, 143 | [], 144 | TxnBuilderTypes.AccountAddress.fromHex(feePayer.address()), 145 | ); 146 | 147 | const response = await dappClient.signTransaction(activeAccountAddress, { rawTxn: feePayerRawTxn }); 148 | console.log(response); 149 | }); 150 | 151 | return ( 152 |
    153 |

    New request

    154 |
    155 |
    156 | Network: 157 |
    158 | 161 | 168 | 175 | 182 | 189 | 196 | 199 | 206 |
    207 |
    208 | ); 209 | } 210 | 211 | function NetworkSelect() { 212 | const appState = useAppState(); 213 | const selectedNetwork = appState.watch('selectedNetwork'); 214 | 215 | function onChange(event: React.ChangeEvent) { 216 | appState.set('selectedNetwork', event.target.value as NetworkName); 217 | } 218 | 219 | return ( 220 | 224 | ); 225 | } 226 | -------------------------------------------------------------------------------- /examples/dapp/src/NewRequestSection/index.css: -------------------------------------------------------------------------------- 1 | .new-request-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1em; 5 | } 6 | -------------------------------------------------------------------------------- /examples/dapp/src/NewRequestSection/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import NewRequestSection from './NewRequestSection.tsx'; 5 | 6 | export default NewRequestSection; 7 | -------------------------------------------------------------------------------- /examples/dapp/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button.btn-small { 51 | padding: 0.3em 0.6em; 52 | } 53 | button:hover { 54 | border-color: #646cff; 55 | } 56 | button:focus, 57 | button:focus-visible { 58 | outline: 4px auto -webkit-focus-ring-color; 59 | } 60 | 61 | @media (prefers-color-scheme: light) { 62 | :root { 63 | color: #213547; 64 | background-color: #ffffff; 65 | } 66 | a:hover { 67 | color: #747bff; 68 | } 69 | button { 70 | background-color: #f9f9f9; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/dapp/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/client'; 6 | import App from './App'; 7 | import { AppStateContextProvider } from './AppStateContext.ts'; 8 | import { DappClientContextProvider } from './DappClientContext.ts'; 9 | import './index.css'; 10 | 11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /examples/dapp/src/utils/makeContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable react/function-component-definition */ 5 | 6 | import React, { PropsWithChildren, createContext, useContext } from 'react'; 7 | 8 | function makeContext(name: string): [React.Context, () => TContext]; 9 | 10 | function makeContext( 11 | name: string, 12 | valueProvider: (props: TProps) => TContext, 13 | ): [(props: PropsWithChildren) => JSX.Element, () => TContext, React.Context]; 14 | 15 | function makeContext(name: string, valueProvider?: (props: TProps) => TContext) { 16 | const Context = createContext(undefined); 17 | Context.displayName = name; 18 | 19 | function useContextHook() { 20 | const context = useContext(Context); 21 | if (context === undefined) { 22 | throw new Error(`No provider for ${name}`); 23 | } 24 | return context; 25 | } 26 | 27 | if (!valueProvider) { 28 | return [Context, useContextHook] as const; 29 | } 30 | 31 | const ContextProvider = ({ children, ...props }: PropsWithChildren) => { 32 | const value = valueProvider(props as TProps); 33 | return {children}; 34 | }; 35 | 36 | return [ContextProvider, useContextHook, Context] as const; 37 | } 38 | 39 | export default makeContext; 40 | -------------------------------------------------------------------------------- /examples/dapp/src/utils/makeLocalStorageAppStateContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useEffect, useRef, useState } from 'react'; 5 | import makeContext from './makeContext.tsx'; 6 | 7 | /** 8 | * Utility function to define the app state and implement accessors with `window.localStorage`. 9 | * It allows atomically getting and setting the individual properties of the state, as well as 10 | * watching for changes and trigger re-renders. 11 | */ 12 | export default function makeLocalStorageAppStateContext(storageKeyPrefix: string, initialValue: TState) { 13 | return makeContext('AppStateContext', () => { 14 | type StateChangeCallback = (key: keyof TState, newValue: TState[keyof TState]) => void; 15 | const changeCallbacks = useRef(new Set()); 16 | 17 | function get(key: TKey): TState[TKey] { 18 | const strKey = key.toString(); 19 | const serialized = window.localStorage.getItem(`${storageKeyPrefix}.${strKey}`) ?? undefined; 20 | return serialized !== undefined ? JSON.parse(serialized) : initialValue[key]; 21 | } 22 | 23 | function set(key: TKey, value: TState[TKey]) { 24 | const strKey = key.toString(); 25 | if (value !== undefined) { 26 | const serialized = JSON.stringify(value); 27 | window.localStorage.setItem(`${storageKeyPrefix}.${strKey}`, serialized); 28 | } else { 29 | window.localStorage.removeItem(`${storageKeyPrefix}.${strKey}`); 30 | } 31 | for (const callback of changeCallbacks.current) { 32 | callback(key, value); 33 | } 34 | } 35 | 36 | /** 37 | * Watch for changes to a specific key in the state. 38 | * When the value is updated, a re-render is triggered for the watching component 39 | */ 40 | function watch(key: TKey) { 41 | /* eslint-disable react-hooks/rules-of-hooks */ 42 | const [value, setValue] = useState(get(key)); 43 | 44 | useEffect(() => { 45 | const onStateChange = (changedKey: keyof TState, newValue: TState[keyof TState]) => { 46 | if (changedKey === key) { 47 | setValue(newValue as TState[TKey]); 48 | } 49 | }; 50 | 51 | changeCallbacks.current.add(onStateChange); 52 | return () => { 53 | changeCallbacks.current.delete(onStateChange); 54 | }; 55 | }); 56 | 57 | return value; 58 | /* eslint-enable react-hooks/rules-of-hooks */ 59 | } 60 | 61 | return { 62 | get, 63 | set, 64 | watch, 65 | }; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /examples/dapp/src/utils/useAsyncAction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useState } from 'react'; 5 | 6 | export default function useAsyncAction( 7 | callback: (...params: TParams) => Promise, 8 | ) { 9 | const [isLoading, setIsLoading] = useState(false); 10 | 11 | async function trigger(...params: TParams) { 12 | setIsLoading(true); 13 | try { 14 | await callback(...params); 15 | } finally { 16 | setIsLoading(false); 17 | } 18 | } 19 | 20 | return { 21 | isLoading, 22 | trigger, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/dapp/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// 5 | 6 | interface ImportMetaEnv { 7 | readonly VITE_DAPP_ID?: string; 8 | readonly VITE_IC_BACKEND_URL?: string; 9 | readonly VITE_IC_FRONTEND_URL?: string; 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv; 14 | } 15 | -------------------------------------------------------------------------------- /examples/dapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "allowImportingTsExtensions": true, 16 | "allowSyntheticDefaultImports": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src", "./vite.config.ts"], 25 | "exclude": ["node_modules"], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/dapp/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | ".eslintrc.cjs", 11 | "vite.config.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/dapp/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig } from 'vite'; 5 | import react from '@vitejs/plugin-react-swc'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react()], 10 | }); 11 | -------------------------------------------------------------------------------- /examples/wallet/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const devTools = ['./vite.config.ts']; 5 | 6 | module.exports = { 7 | env: { browser: true, es2020: true }, 8 | extends: ['@identity-connect/eslint-config'], 9 | parserOptions: { 10 | project: ['tsconfig.json', 'tsconfig.node.json'], 11 | }, 12 | plugins: ['react-refresh'], 13 | root: true, 14 | rules: { 15 | 'import/extensions': 'off', 16 | 'import/no-extraneous-dependencies': ['error', { devDependencies: devTools }], 17 | 'react-refresh/only-export-components': 'warn', 18 | 'react/react-in-jsx-scope': 'off', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/wallet/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/wallet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example Wallet 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/wallet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-wallet", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --force", 8 | "build": "tsc && vite build", 9 | "lint": "prettier --check src", 10 | "lint:fix": "prettier --write src && eslint --cache ./src --fix", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@identity-connect/api": "*", 15 | "@identity-connect/crypto": "*", 16 | "@identity-connect/wallet-sdk": "*", 17 | "@identity-connect/wallet-api": "*", 18 | "aptos": "^1.20.0", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0" 21 | }, 22 | "devDependencies": { 23 | "@identity-connect/eslint-config": "*", 24 | "@types/react": "18.0.28", 25 | "@types/react-dom": "^18.0.11", 26 | "@vitejs/plugin-react-swc": "^3.0.0", 27 | "eslint": "8.36.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "5.0.4", 30 | "vite": "^4.3.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/wallet/src/AccountsSection/AccountsList.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import './index.css'; 5 | import useAccounts from '../useAccounts.ts'; 6 | import AccountsListItem from './AccountsListItem.tsx'; 7 | 8 | export default function AccountsList() { 9 | const accounts = useAccounts().watch(); 10 | return ( 11 |
      12 | {Object.values(accounts).map((account) => ( 13 | 14 | ))} 15 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/wallet/src/AccountsSection/AccountsListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import './index.css'; 5 | import { WalletAccount } from '../AppStateContext.ts'; 6 | import useAccounts from '../useAccounts.ts'; 7 | 8 | export interface AccountsListItemProps { 9 | account: WalletAccount; 10 | } 11 | 12 | export default function AccountsListItem({ account }: AccountsListItemProps) { 13 | const accounts = useAccounts(); 14 | 15 | const collapsedAddress = account.address.slice(0, 18); 16 | 17 | const onRemove = () => { 18 | accounts.remove(account.address); 19 | }; 20 | 21 | return ( 22 |
  • 23 | {collapsedAddress} 24 | 27 |
  • 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /examples/wallet/src/AccountsSection/AccountsSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import './index.css'; 5 | import useAccounts from '../useAccounts.ts'; 6 | import AccountsList from './AccountsList.tsx'; 7 | 8 | export default function AccountsSection() { 9 | const accounts = useAccounts(); 10 | 11 | const onGenerate = async () => { 12 | const newAccount = accounts.generate(); 13 | accounts.add(newAccount); 14 | }; 15 | 16 | return ( 17 |
    18 |

    Accounts

    19 | 20 | 23 |
    24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/wallet/src/AccountsSection/index.css: -------------------------------------------------------------------------------- 1 | .accounts-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1em; 5 | } 6 | 7 | .accounts-list-item { 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | gap: 1em; 15 | } 16 | -------------------------------------------------------------------------------- /examples/wallet/src/AccountsSection/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import AccountsSection from './AccountsSection.tsx'; 5 | 6 | export default AccountsSection; 7 | -------------------------------------------------------------------------------- /examples/wallet/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | flex-direction: row; 7 | } 8 | 9 | .layout { 10 | display: flex; 11 | flex-direction: row; 12 | gap: 2em; 13 | } 14 | 15 | .card { 16 | flex-grow: 1; 17 | flex-shrink: 0; 18 | padding: 2em; 19 | box-shadow: rgba(149, 157, 165, 0.2) 0 8px 24px; 20 | } 21 | -------------------------------------------------------------------------------- /examples/wallet/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import './App.css'; 5 | import AccountsSection from './AccountsSection'; 6 | import ConnectionSection from './ConnectionSection'; 7 | import PromptSection from './PromptSection'; 8 | 9 | function App() { 10 | return ( 11 | <> 12 |

    Example Wallet

    13 |
    14 |
    15 | 16 |
    17 |
    18 | 19 |
    20 |
    21 | 22 |
    23 |
    24 | 25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /examples/wallet/src/AppStateContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { WalletConnectionDataMap } from '@identity-connect/wallet-sdk'; 5 | import makeLocalStorageAppStateContext from './utils/makeLocalStorageAppStateContext.ts'; 6 | 7 | export interface WalletAccount { 8 | address: string; 9 | publicKeyB64: string; 10 | secretKeyB64: string; 11 | } 12 | 13 | export interface AppState { 14 | accounts: { [address: string]: WalletAccount }; 15 | activeAccountAddress?: string; 16 | icWalletConnections: WalletConnectionDataMap; 17 | } 18 | 19 | const STORAGE_KEY_PREFIX = 'appState'; 20 | export const [AppStateContextProvider, useAppState] = makeLocalStorageAppStateContext(STORAGE_KEY_PREFIX, { 21 | accounts: {}, 22 | icWalletConnections: {}, 23 | }); 24 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/AccountsList.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable jsx-a11y/label-has-associated-control */ 5 | 6 | import { 7 | AccountConnectionAction, 8 | decodeBase64, 9 | KeyTypes, 10 | makeEd25519SecretKeySignCallbackNoDomainSeparation, 11 | toKey, 12 | } from '@identity-connect/crypto'; 13 | import { createWalletAccountConnectInfo, WalletAccountConnectInfo } from '@identity-connect/wallet-sdk'; 14 | import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; 15 | import useAccounts from '../useAccounts.ts'; 16 | import AccountsListItem from './AccountsListItem.tsx'; 17 | import './index.css'; 18 | 19 | const defaultInitialConnectedAddresses = new Set(); 20 | 21 | export interface AccountsListHandle { 22 | getActionRequests: () => WalletAccountConnectInfo[]; 23 | } 24 | 25 | export interface AccountsListProps { 26 | initialConnectedAddresses?: Set; 27 | walletId: string; 28 | } 29 | 30 | const AccountsList = forwardRef( 31 | ({ initialConnectedAddresses = defaultInitialConnectedAddresses, walletId }, ref) => { 32 | const accounts = useAccounts().watch(); 33 | const actionRequestCache = useRef<{ [address: string]: WalletAccountConnectInfo }>({}); 34 | const [connectedAddresses, setConnectedAddresses] = useState(initialConnectedAddresses); 35 | 36 | useImperativeHandle(ref, () => ({ 37 | getActionRequests: () => 38 | Object.entries(actionRequestCache.current) 39 | .filter(([address]) => initialConnectedAddresses.has(address) !== connectedAddresses.has(address)) 40 | .map(([_, value]) => value), 41 | })); 42 | 43 | useEffect(() => { 44 | actionRequestCache.current = {}; 45 | }, [initialConnectedAddresses, walletId]); 46 | 47 | const onAccountToggle = useCallback( 48 | async (address: string, shouldBeConnected: boolean) => { 49 | if (!(address in actionRequestCache.current)) { 50 | const wasConnected = initialConnectedAddresses.has(address); 51 | const action = wasConnected ? AccountConnectionAction.REMOVE : AccountConnectionAction.ADD; 52 | const account = accounts[address]; 53 | const secretKeyBytes = decodeBase64(account.secretKeyB64); 54 | const publicKeyBytes = decodeBase64(account.publicKeyB64); 55 | 56 | const signCallback = makeEd25519SecretKeySignCallbackNoDomainSeparation( 57 | toKey(secretKeyBytes, KeyTypes.Ed25519SecretKey), 58 | ); 59 | actionRequestCache.current[address] = await createWalletAccountConnectInfo( 60 | signCallback, 61 | toKey(publicKeyBytes, KeyTypes.Ed25519PublicKey), 62 | action, 63 | walletId, 64 | ); 65 | } 66 | setConnectedAddresses((prevValue) => { 67 | const newValue = new Set(prevValue); 68 | if (shouldBeConnected) { 69 | newValue.add(address); 70 | } else { 71 | newValue.delete(address); 72 | } 73 | return newValue; 74 | }); 75 | }, 76 | [accounts, initialConnectedAddresses, walletId], 77 | ); 78 | 79 | return ( 80 |
      81 | {Object.values(accounts).map((account) => ( 82 | 88 | ))} 89 |
    90 | ); 91 | }, 92 | ); 93 | 94 | export default AccountsList; 95 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/AccountsListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { memo, useRef } from 'react'; 5 | import './index.css'; 6 | import { WalletAccount } from '../AppStateContext.ts'; 7 | 8 | export interface AccountsListItemProps { 9 | account: WalletAccount; 10 | isConnected: boolean; 11 | onChange: (address: string, shouldBeConnected: boolean) => void; 12 | } 13 | 14 | const AccountsListItem = memo(({ account, isConnected, onChange }: AccountsListItemProps) => { 15 | const collapsedAddress = account.address.slice(0, 18); 16 | 17 | const inputRef = useRef(null); 18 | const onCheckboxChange = () => { 19 | if (inputRef.current) { 20 | onChange(account.address, inputRef.current.checked); 21 | } 22 | }; 23 | 24 | return ( 25 |
  • 26 | {collapsedAddress} 27 | 28 |
  • 29 | ); 30 | }); 31 | 32 | export default AccountsListItem; 33 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/ConnectionSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable jsx-a11y/label-has-associated-control */ 5 | 6 | import { useEffect, useRef, useState } from 'react'; 7 | import { useAppState } from '../AppStateContext.ts'; 8 | import './index.css'; 9 | import { useWalletClient } from '../WalletClientContext.ts'; 10 | import NewConnection from './NewConnection.tsx'; 11 | import UpdateConnection from './UpdateConnection.tsx'; 12 | 13 | export default function ConnectionSection() { 14 | const appState = useAppState(); 15 | const walletClient = useWalletClient(); 16 | 17 | const walletIdInputRef = useRef(null); 18 | 19 | const connections = appState.watch('icWalletConnections'); 20 | const connection = Object.values(connections)[0]; 21 | const connectedWalletId = connection?.walletId; 22 | 23 | // Make sure to reset state on connection change 24 | useEffect(() => { 25 | if (walletIdInputRef.current) { 26 | walletIdInputRef.current.value = connectedWalletId ?? ''; 27 | } 28 | setNewWalletId(undefined); 29 | }, [connectedWalletId]); 30 | 31 | const [newWalletId, setNewWalletId] = useState(); 32 | 33 | const onScan = () => { 34 | const walletId = walletIdInputRef.current?.value || undefined; 35 | setNewWalletId(walletId); 36 | }; 37 | 38 | const onCancel = async () => { 39 | setNewWalletId(undefined); 40 | }; 41 | 42 | const onDisconnect = async () => { 43 | if (connectedWalletId !== undefined) { 44 | await walletClient.removeConnection(connectedWalletId); 45 | } 46 | }; 47 | 48 | const isConnected = connectedWalletId !== undefined; 49 | const isConnecting = !isConnected && newWalletId !== undefined; 50 | 51 | function renderButton() { 52 | if (isConnected) { 53 | return ( 54 | 57 | ); 58 | } 59 | if (isConnecting) { 60 | return ( 61 | 64 | ); 65 | } 66 | return ( 67 | 70 | ); 71 | } 72 | 73 | function renderContent() { 74 | if (isConnected) { 75 | return ; 76 | } 77 | if (isConnecting) { 78 | return ; 79 | } 80 | return null; 81 | } 82 | 83 | return ( 84 |
    85 |

    Connection

    86 |
    87 | 88 | 89 | {renderButton()} 90 |
    91 | {renderContent()} 92 |
    93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/NewConnection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useRef } from 'react'; 5 | import useAsyncAction from '../utils/useAsyncAction.ts'; 6 | import { useWalletClient } from '../WalletClientContext.ts'; 7 | import AccountsList, { AccountsListHandle } from './AccountsList.tsx'; 8 | import './index.css'; 9 | 10 | export interface NewConnectionProps { 11 | walletId: string; 12 | } 13 | 14 | export default function NewConnection({ walletId }: NewConnectionProps) { 15 | const accountsListRef = useRef(null); 16 | const walletClient = useWalletClient(); 17 | 18 | const connect = useAsyncAction(async () => { 19 | if (!accountsListRef.current) { 20 | return; 21 | } 22 | const actionRequests = accountsListRef.current.getActionRequests(); 23 | await walletClient.finalizeConnection(walletId, actionRequests); 24 | }); 25 | 26 | return ( 27 |
    28 | 29 | 32 |
    33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/UpdateConnection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { WalletConnectionData } from '@identity-connect/wallet-sdk'; 5 | import { useRef } from 'react'; 6 | import useAsyncAction from '../utils/useAsyncAction.ts'; 7 | import { useWalletClient } from '../WalletClientContext.ts'; 8 | import AccountsList, { AccountsListHandle } from './AccountsList.tsx'; 9 | import './index.css'; 10 | 11 | export interface UpdateConnectionProps { 12 | connection: WalletConnectionData; 13 | } 14 | 15 | export default function UpdateConnection({ connection }: UpdateConnectionProps) { 16 | const accountsListRef = useRef(null); 17 | const walletClient = useWalletClient(); 18 | 19 | const update = useAsyncAction(async () => { 20 | if (!accountsListRef.current) { 21 | return; 22 | } 23 | const actionRequests = accountsListRef.current.getActionRequests(); 24 | await walletClient.updateAccounts(connection.walletId, actionRequests); 25 | }); 26 | 27 | const initialConnectedAddresses = new Set(Object.keys(connection.accounts)); 28 | 29 | return ( 30 |
    31 | 36 | 39 |
    40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/index.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 1em; 6 | } 7 | 8 | .accounts-list-item { 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | white-space: nowrap; 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | gap: 1em; 16 | } 17 | -------------------------------------------------------------------------------- /examples/wallet/src/ConnectionSection/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import ConnectionSection from './ConnectionSection.tsx'; 5 | 6 | export default ConnectionSection; 7 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/PromptSection.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SigningRequestTypes } from '@identity-connect/api'; 5 | import { SignatureRequest } from '@identity-connect/wallet-sdk'; 6 | import { useState } from 'react'; 7 | import useAsyncAction from '../utils/useAsyncAction.ts'; 8 | import { useWalletClient } from '../WalletClientContext.ts'; 9 | import './index.css'; 10 | import SignAndSubmitTransactionRequestListItem from './SignAndSubmitTransactionRequestListItem.tsx'; 11 | import SignMessageRequestListItem from './SignMessageRequestListItem.tsx'; 12 | import SignTransactionRequestListItem from './SignTransactionRequestListItem.tsx'; 13 | 14 | export default function PromptSection() { 15 | const walletClient = useWalletClient(); 16 | 17 | const [signingRequests, setSigningRequests] = useState(); 18 | const fetchSigningRequests = useAsyncAction(async () => { 19 | const newSigningRequests = await walletClient.getAllSigningRequests(); 20 | setSigningRequests(newSigningRequests); 21 | }); 22 | 23 | return ( 24 |
    25 |

    Signing requests

    26 |
    27 |
      28 | {signingRequests?.map((request) => { 29 | switch (request.type) { 30 | case SigningRequestTypes.SIGN_MESSAGE: 31 | return ( 32 | 37 | ); 38 | case SigningRequestTypes.SIGN_TRANSACTION: 39 | return ( 40 | 45 | ); 46 | case SigningRequestTypes.SIGN_AND_SUBMIT_TRANSACTION: 47 | return ( 48 | 53 | ); 54 | default: 55 | return null; 56 | } 57 | })} 58 |
    59 | 62 |
    63 |
    64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/SignAndSubmitTransactionRequestListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DeserializedTransactionPayload } from '@identity-connect/api'; 5 | import { 6 | decodeBase64, 7 | KeyTypes, 8 | makeEd25519SecretKeySignCallbackNoDomainSeparation, 9 | toKey, 10 | } from '@identity-connect/crypto'; 11 | import { deserializeSignAndSubmitTransactionRequestArgs, JsonTransactionPayload } from '@identity-connect/wallet-api'; 12 | import { SignAndSubmitTransactionRequest } from '@identity-connect/wallet-sdk'; 13 | import { HexString, TxnBuilderTypes } from 'aptos'; 14 | import { useMemo } from 'react'; 15 | import { useAppState } from '../AppStateContext.ts'; 16 | import useAsyncAction from '../utils/useAsyncAction.ts'; 17 | import { useWalletClient } from '../WalletClientContext.ts'; 18 | import './index.css'; 19 | 20 | function getEntryFunctionInfo(payload: DeserializedTransactionPayload) { 21 | if (payload instanceof TxnBuilderTypes.TransactionPayloadEntryFunction) { 22 | const moduleAddress = HexString.fromUint8Array(payload.value.module_name.address.address).toShortString(); 23 | const moduleName = payload.value.module_name.name.value; 24 | const functionName = payload.value.function_name.value; 25 | const encodedArgs = payload.value.args.map((arg) => HexString.fromUint8Array(arg).toShortString()); 26 | return { 27 | args: encodedArgs, 28 | functionId: `${moduleAddress}::${moduleName}::${functionName}`, 29 | }; 30 | } 31 | 32 | if (payload instanceof TxnBuilderTypes.TransactionPayload) { 33 | throw new Error('Not supported'); 34 | } 35 | if (payload.type === 'multisig_payload') { 36 | throw new Error('Not supported'); 37 | } 38 | 39 | return { 40 | args: payload.arguments, 41 | functionId: payload.function, 42 | }; 43 | } 44 | 45 | interface RequestListItemProps { 46 | onRespond: () => void; 47 | request: SignAndSubmitTransactionRequest; 48 | } 49 | 50 | export default function SignAndSubmitTransactionRequestListItem({ onRespond, request }: RequestListItemProps) { 51 | const appState = useAppState(); 52 | const walletClient = useWalletClient(); 53 | 54 | const deserializedArgs = useMemo(() => deserializeSignAndSubmitTransactionRequestArgs(request.args), [request.args]); 55 | 56 | const respond = useAsyncAction(async (action: string) => { 57 | const accounts = appState.get('accounts'); 58 | const account = accounts[request.accountAddress]; 59 | if (account === undefined) { 60 | throw new Error('Account not available in wallet'); 61 | } 62 | 63 | const accountEd25519SecretKey = toKey(decodeBase64(account.secretKeyB64), KeyTypes.Ed25519SecretKey); 64 | const signCallback = makeEd25519SecretKeySignCallbackNoDomainSeparation(accountEd25519SecretKey); 65 | 66 | if (action === 'approve') { 67 | const version = Date.now(); 68 | const hashBytes = await signCallback(new TextEncoder().encode(version.toString())); 69 | const mockHash = HexString.fromUint8Array(hashBytes).toString(); 70 | await walletClient.approveSigningRequest(request.id, request.pairingId, { hash: mockHash }); 71 | } else { 72 | await walletClient.rejectSigningRequest(request.id, request.pairingId); 73 | } 74 | onRespond(); 75 | }); 76 | 77 | const collapsedId = `${request.id.slice(0, 4)}...${request.id.slice(-4)}`; 78 | 79 | let payload: JsonTransactionPayload | TxnBuilderTypes.TransactionPayload | undefined; 80 | let feePayerAddress: string | undefined; 81 | if ('payload' in deserializedArgs) { 82 | payload = deserializedArgs.payload; 83 | } else if (deserializedArgs.rawTxn instanceof TxnBuilderTypes.RawTransaction) { 84 | payload = deserializedArgs.rawTxn.payload; 85 | } else { 86 | payload = deserializedArgs.rawTxn.raw_txn.payload; 87 | if (deserializedArgs.rawTxn instanceof TxnBuilderTypes.FeePayerRawTransaction) { 88 | feePayerAddress = deserializedArgs.rawTxn.fee_payer_address.toHexString().toString(); 89 | } 90 | } 91 | const payloadInfo = payload ? getEntryFunctionInfo(payload) : undefined; 92 | 93 | return ( 94 |
    95 |
    96 |
    97 |
    98 | ({collapsedId}) Sign and submit transaction 99 |
    100 |
    {payloadInfo?.functionId}
    101 | {feePayerAddress !== undefined ?
    Fee payer: {feePayerAddress.slice(0, 4)}...
    : null} 102 |
    {JSON.stringify(payloadInfo?.args)}
    103 |
    104 |
    105 |
    106 | 114 | 122 |
    123 |
    124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/SignMessageRequestListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | decodeBase64, 6 | KeyTypes, 7 | makeEd25519SecretKeySignCallbackNoDomainSeparation, 8 | toKey, 9 | } from '@identity-connect/crypto'; 10 | import { FullMessageParams, makeFullMessage } from '@identity-connect/wallet-api'; 11 | import { SignMessageRequest } from '@identity-connect/wallet-sdk'; 12 | import { HexString } from 'aptos'; 13 | import { useAppState } from '../AppStateContext.ts'; 14 | import useAsyncAction from '../utils/useAsyncAction.ts'; 15 | import { useWalletClient } from '../WalletClientContext.ts'; 16 | import './index.css'; 17 | 18 | interface RequestListItemProps { 19 | onRespond: () => void; 20 | request: SignMessageRequest; 21 | } 22 | 23 | export default function SignMessageRequestListItem({ onRespond, request }: RequestListItemProps) { 24 | const appState = useAppState(); 25 | const walletClient = useWalletClient(); 26 | 27 | const respond = useAsyncAction(async (action: string) => { 28 | const accounts = appState.get('accounts'); 29 | const account = accounts[request.accountAddress]; 30 | if (account === undefined) { 31 | throw new Error('Account not available in wallet'); 32 | } 33 | 34 | const accountEd25519SecretKey = toKey(decodeBase64(account.secretKeyB64), KeyTypes.Ed25519SecretKey); 35 | const signCallback = makeEd25519SecretKeySignCallbackNoDomainSeparation(accountEd25519SecretKey); 36 | 37 | if (action === 'approve') { 38 | const { message, nonce, ...flags } = request.args; 39 | 40 | const address = request.accountAddress; 41 | const application = request.registeredDapp.hostname; 42 | const chainId = request.networkName === 'mainnet' ? 1 : 2; 43 | const params: FullMessageParams = { address, application, chainId, message, nonce }; 44 | 45 | const { fullMessage, prefix } = makeFullMessage(params, flags); 46 | 47 | const fullMessageBytes = new TextEncoder().encode(fullMessage); 48 | const signatureBytes = await signCallback(fullMessageBytes); 49 | const signature = HexString.fromUint8Array(signatureBytes).toString(); 50 | 51 | await walletClient.approveSigningRequest(request.id, request.pairingId, { 52 | ...params, 53 | fullMessage, 54 | prefix, 55 | signature, 56 | }); 57 | } else { 58 | await walletClient.rejectSigningRequest(request.id, request.pairingId); 59 | } 60 | onRespond(); 61 | }); 62 | 63 | const collapsedId = `${request.id.slice(0, 4)}...${request.id.slice(-4)}`; 64 | 65 | return ( 66 |
    67 |
    68 |
    69 | ({collapsedId}) Sign Message 70 |
    71 |
    {request.args.message}
    72 |
    73 |
    74 | 82 | 90 |
    91 |
    92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/SignTransactionRequestListItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DeserializedTransactionPayload } from '@identity-connect/api'; 5 | import { 6 | decodeBase64, 7 | KeyTypes, 8 | makeEd25519SecretKeySignCallbackNoDomainSeparation, 9 | toKey, 10 | } from '@identity-connect/crypto'; 11 | import { 12 | bcsSerialize, 13 | deserializeSignTransactionRequestArgs, 14 | JsonTransactionPayload, 15 | } from '@identity-connect/wallet-api'; 16 | import { SignTransactionRequest } from '@identity-connect/wallet-sdk'; 17 | import { HexString, TransactionBuilder, TxnBuilderTypes } from 'aptos'; 18 | import { useMemo } from 'react'; 19 | import { useAppState } from '../AppStateContext.ts'; 20 | import useAsyncAction from '../utils/useAsyncAction.ts'; 21 | import { useWalletClient } from '../WalletClientContext.ts'; 22 | import './index.css'; 23 | 24 | function getEntryFunctionInfo(payload: DeserializedTransactionPayload) { 25 | if (payload instanceof TxnBuilderTypes.TransactionPayloadEntryFunction) { 26 | const moduleAddress = HexString.fromUint8Array(payload.value.module_name.address.address).toShortString(); 27 | const moduleName = payload.value.module_name.name.value; 28 | const functionName = payload.value.function_name.value; 29 | const encodedArgs = payload.value.args.map((arg) => HexString.fromUint8Array(arg).toShortString()); 30 | return { 31 | args: encodedArgs, 32 | functionId: `${moduleAddress}::${moduleName}::${functionName}`, 33 | }; 34 | } 35 | 36 | if (payload instanceof TxnBuilderTypes.TransactionPayload) { 37 | throw new Error('Not supported'); 38 | } 39 | if ((payload as any).type === 'multisig_payload') { 40 | throw new Error('Not supported'); 41 | } 42 | 43 | return { 44 | args: (payload as any).arguments, 45 | functionId: (payload as any).function, 46 | }; 47 | } 48 | 49 | function makeMockPayload() { 50 | const entryFunction = new TxnBuilderTypes.EntryFunction( 51 | TxnBuilderTypes.ModuleId.fromStr('0x1::mock'), 52 | new TxnBuilderTypes.Identifier('doSomething'), 53 | [], 54 | [], 55 | ); 56 | return new TxnBuilderTypes.TransactionPayloadEntryFunction(entryFunction); 57 | } 58 | 59 | interface RequestListItemProps { 60 | onRespond: () => void; 61 | request: SignTransactionRequest; 62 | } 63 | 64 | export default function SignTransactionRequestListItem({ onRespond, request }: RequestListItemProps) { 65 | const appState = useAppState(); 66 | const walletClient = useWalletClient(); 67 | 68 | const deserializedArgs = useMemo(() => deserializeSignTransactionRequestArgs(request.args), [request.args]); 69 | 70 | const respond = useAsyncAction(async (action: string) => { 71 | const accounts = appState.get('accounts'); 72 | const account = accounts[request.accountAddress]; 73 | if (account === undefined) { 74 | throw new Error('Account not available in wallet'); 75 | } 76 | 77 | const accountEd25519SecretKey = toKey(decodeBase64(account.secretKeyB64), KeyTypes.Ed25519SecretKey); 78 | const signCallback = makeEd25519SecretKeySignCallbackNoDomainSeparation(accountEd25519SecretKey); 79 | 80 | if (action === 'approve') { 81 | let rawTxn: TxnBuilderTypes.RawTransaction | undefined; 82 | let signingMessageBytes: Uint8Array; 83 | if ('payload' in deserializedArgs) { 84 | const expirationTimestamp = Math.ceil(Date.now() / 1000) + 60; 85 | const sender = deserializedArgs.options?.sender ?? request.accountAddress; 86 | rawTxn = new TxnBuilderTypes.RawTransaction( 87 | TxnBuilderTypes.AccountAddress.fromHex(sender), 88 | 0n, 89 | makeMockPayload(), 90 | 0n, 91 | 0n, 92 | BigInt(expirationTimestamp), 93 | new TxnBuilderTypes.ChainId(request.networkName === 'mainnet' ? 1 : 2), 94 | ); 95 | signingMessageBytes = TransactionBuilder.getSigningMessage(rawTxn); 96 | } else { 97 | signingMessageBytes = TransactionBuilder.getSigningMessage(deserializedArgs.rawTxn); 98 | } 99 | 100 | const signatureBytes = await signCallback(signingMessageBytes); 101 | const accountAuthenticator = new TxnBuilderTypes.AccountAuthenticatorEd25519( 102 | new TxnBuilderTypes.Ed25519PublicKey(decodeBase64(account.publicKeyB64)), 103 | new TxnBuilderTypes.Ed25519Signature(signatureBytes), 104 | ); 105 | 106 | await walletClient.approveSigningRequest(request.id, request.pairingId, { 107 | accountAuthenticator: bcsSerialize(accountAuthenticator), 108 | rawTxn: rawTxn ? bcsSerialize(rawTxn) : undefined, 109 | }); 110 | } else { 111 | await walletClient.rejectSigningRequest(request.id, request.pairingId); 112 | } 113 | onRespond(); 114 | }); 115 | 116 | const collapsedId = `${request.id.slice(0, 4)}...${request.id.slice(-4)}`; 117 | 118 | let payload: JsonTransactionPayload | TxnBuilderTypes.TransactionPayload | undefined; 119 | let feePayerAddress: string | undefined; 120 | if ('payload' in deserializedArgs) { 121 | payload = deserializedArgs.payload; 122 | } else if (deserializedArgs.rawTxn instanceof TxnBuilderTypes.RawTransaction) { 123 | payload = deserializedArgs.rawTxn.payload; 124 | } else { 125 | payload = deserializedArgs.rawTxn.raw_txn.payload; 126 | if (deserializedArgs.rawTxn instanceof TxnBuilderTypes.FeePayerRawTransaction) { 127 | feePayerAddress = deserializedArgs.rawTxn.fee_payer_address.toHexString().toString(); 128 | } 129 | } 130 | const payloadInfo = payload ? getEntryFunctionInfo(payload) : undefined; 131 | 132 | return ( 133 |
    134 |
    135 |
    136 |
    137 | ({collapsedId}) Sign transaction 138 |
    139 |
    {payloadInfo?.functionId}
    140 | {feePayerAddress !== undefined ?
    Fee payer: {feePayerAddress.slice(0, 4)}...
    : null} 141 |
    {JSON.stringify(payloadInfo?.args)}
    142 |
    143 |
    144 |
    145 | 153 | 161 |
    162 |
    163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/index.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 1em; 6 | } 7 | 8 | .signing-requests-container { 9 | min-width: 300px; 10 | max-width: 500px; 11 | height: 100%; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .signing-requests-body { 17 | display: flex; 18 | flex-grow: 1; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | align-items: center; 22 | } 23 | 24 | .signing-requests { 25 | display: flex; 26 | flex-grow: 1; 27 | flex-direction: column; 28 | align-items: stretch; 29 | gap: 1em; 30 | width: 100%; 31 | padding: 0; 32 | } 33 | 34 | .signing-request { 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: space-between; 38 | align-items: center; 39 | gap: 1em; 40 | background-color: #fafafa20; 41 | } 42 | -------------------------------------------------------------------------------- /examples/wallet/src/PromptSection/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import PromptSection from './PromptSection.tsx'; 5 | 6 | export default PromptSection; 7 | -------------------------------------------------------------------------------- /examples/wallet/src/WalletClientContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { NetworkName } from '@identity-connect/api'; 5 | import { ICWalletClient, WalletConnectionData, WalletInfo } from '@identity-connect/wallet-sdk'; 6 | import { useMemo } from 'react'; 7 | import { useAppState } from './AppStateContext.ts'; 8 | import makeContext from './utils/makeContext.tsx'; 9 | 10 | const { VITE_IC_BACKEND_URL } = import.meta.env; 11 | const walletInfo: WalletInfo = { 12 | deviceIdentifier: 'example-wallet', 13 | platform: 'chrome-extension', 14 | platformOS: 'osx', 15 | walletName: 'petra', 16 | }; 17 | 18 | export const [WalletClientContextProvider, useWalletClient] = makeContext('WalletClientContext', () => { 19 | const appState = useAppState(); 20 | 21 | return useMemo(() => { 22 | const accessors = { 23 | get: async (walletId: string) => { 24 | const connections = appState.get('icWalletConnections'); 25 | return connections[walletId]; 26 | }, 27 | getAll: async () => appState.get('icWalletConnections'), 28 | update: async (walletId: string, newValue?: WalletConnectionData) => { 29 | const connections = appState.get('icWalletConnections'); 30 | if (newValue === undefined) { 31 | delete connections[walletId]; 32 | } else { 33 | connections[walletId] = newValue; 34 | } 35 | appState.set('icWalletConnections', connections); 36 | }, 37 | }; 38 | 39 | return new ICWalletClient(walletInfo, accessors, { 40 | axiosConfig: VITE_IC_BACKEND_URL ? { baseURL: VITE_IC_BACKEND_URL } : undefined, 41 | defaultNetworkName: NetworkName.TESTNET, 42 | }); 43 | }, [appState]); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/wallet/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | button.btn-small { 59 | padding: 0.3em 0.6em; 60 | } 61 | 62 | @media (prefers-color-scheme: light) { 63 | :root { 64 | color: #213547; 65 | background-color: #ffffff; 66 | } 67 | a:hover { 68 | color: #747bff; 69 | } 70 | button { 71 | background-color: #f9f9f9; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/wallet/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/client'; 6 | import App from './App'; 7 | import { AppStateContextProvider } from './AppStateContext.ts'; 8 | import './index.css'; 9 | import { WalletClientContextProvider } from './WalletClientContext.ts'; 10 | 11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /examples/wallet/src/useAccounts.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { encodeBase64 } from '@identity-connect/crypto'; 5 | import { AptosAccount } from 'aptos'; 6 | import { useAppState, WalletAccount } from './AppStateContext.ts'; 7 | 8 | export default function useAccounts() { 9 | const appState = useAppState(); 10 | 11 | function get() { 12 | return appState.get('accounts'); 13 | } 14 | 15 | function watch() { 16 | return appState.watch('accounts'); 17 | } 18 | 19 | function generate(): WalletAccount { 20 | const account = new AptosAccount(); 21 | const address = account.address().hex(); 22 | const publicKeyB64 = encodeBase64(account.signingKey.publicKey); 23 | const secretKeyB64 = encodeBase64(account.signingKey.secretKey); 24 | return { 25 | address, 26 | publicKeyB64, 27 | secretKeyB64, 28 | }; 29 | } 30 | 31 | function add(account: WalletAccount) { 32 | const accounts = appState.get('accounts'); 33 | const newAccounts = { 34 | ...accounts, 35 | [account.address]: account, 36 | }; 37 | appState.set('accounts', newAccounts); 38 | } 39 | 40 | function remove(address: string) { 41 | const accounts = appState.get('accounts'); 42 | const { [address]: _, ...newAccounts } = accounts; 43 | appState.set('accounts', newAccounts); 44 | } 45 | 46 | return { 47 | add, 48 | generate, 49 | get, 50 | remove, 51 | watch, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /examples/wallet/src/utils/makeContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* eslint-disable react/function-component-definition */ 5 | 6 | import React, { PropsWithChildren, createContext, useContext } from 'react'; 7 | 8 | function makeContext(name: string): [React.Context, () => TContext]; 9 | 10 | function makeContext( 11 | name: string, 12 | valueProvider: (props: TProps) => TContext, 13 | ): [(props: PropsWithChildren) => JSX.Element, () => TContext, React.Context]; 14 | 15 | function makeContext(name: string, valueProvider?: (props: TProps) => TContext) { 16 | const Context = createContext(undefined); 17 | Context.displayName = name; 18 | 19 | function useContextHook() { 20 | const context = useContext(Context); 21 | if (context === undefined) { 22 | throw new Error(`No provider for ${name}`); 23 | } 24 | return context; 25 | } 26 | 27 | if (!valueProvider) { 28 | return [Context, useContextHook] as const; 29 | } 30 | 31 | const ContextProvider = ({ children, ...props }: PropsWithChildren) => { 32 | const value = valueProvider(props as TProps); 33 | return {children}; 34 | }; 35 | 36 | return [ContextProvider, useContextHook, Context] as const; 37 | } 38 | 39 | export default makeContext; 40 | -------------------------------------------------------------------------------- /examples/wallet/src/utils/makeLocalStorageAppStateContext.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useEffect, useRef, useState } from 'react'; 5 | import makeContext from './makeContext.tsx'; 6 | 7 | /** 8 | * Utility function to define the app state and implement accessors with `window.localStorage`. 9 | * It allows atomically getting and setting the individual properties of the state, as well as 10 | * watching for changes and trigger re-renders. 11 | */ 12 | export default function makeLocalStorageAppStateContext(storageKeyPrefix: string, initialValue: TState) { 13 | return makeContext('AppStateContext', () => { 14 | type StateChangeCallback = (key: keyof TState, newValue: TState[keyof TState]) => void; 15 | const changeCallbacks = useRef(new Set()); 16 | 17 | function get(key: TKey): TState[TKey] { 18 | const strKey = key.toString(); 19 | const serialized = window.localStorage.getItem(`${storageKeyPrefix}.${strKey}`) ?? undefined; 20 | return serialized !== undefined ? JSON.parse(serialized) : initialValue[key]; 21 | } 22 | 23 | function set(key: TKey, value: TState[TKey]) { 24 | const strKey = key.toString(); 25 | if (value !== undefined) { 26 | const serialized = JSON.stringify(value); 27 | window.localStorage.setItem(`${storageKeyPrefix}.${strKey}`, serialized); 28 | } else { 29 | window.localStorage.removeItem(`${storageKeyPrefix}.${strKey}`); 30 | } 31 | for (const callback of changeCallbacks.current) { 32 | callback(key, value); 33 | } 34 | } 35 | 36 | /** 37 | * Watch for changes to a specific key in the state. 38 | * When the value is updated, a re-render is triggered for the watching component 39 | */ 40 | function watch(key: TKey) { 41 | /* eslint-disable react-hooks/rules-of-hooks */ 42 | const [value, setValue] = useState(get(key)); 43 | 44 | useEffect(() => { 45 | const onStateChange = (changedKey: keyof TState, newValue: TState[keyof TState]) => { 46 | if (changedKey === key) { 47 | setValue(newValue as TState[TKey]); 48 | } 49 | }; 50 | 51 | changeCallbacks.current.add(onStateChange); 52 | return () => { 53 | changeCallbacks.current.delete(onStateChange); 54 | }; 55 | }); 56 | 57 | return value; 58 | /* eslint-enable react-hooks/rules-of-hooks */ 59 | } 60 | 61 | return { 62 | get, 63 | set, 64 | watch, 65 | }; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /examples/wallet/src/utils/useAsyncAction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useState } from 'react'; 5 | 6 | export default function useAsyncAction( 7 | callback: (...params: TParams) => Promise, 8 | ) { 9 | const [isLoading, setIsLoading] = useState(false); 10 | 11 | async function trigger(...params: TParams) { 12 | setIsLoading(true); 13 | try { 14 | await callback(...params); 15 | } finally { 16 | setIsLoading(false); 17 | } 18 | } 19 | 20 | return { 21 | isLoading, 22 | trigger, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/wallet/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// 5 | 6 | interface ImportMetaEnv { 7 | readonly VITE_IC_BACKEND_URL?: string; 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv; 12 | } 13 | -------------------------------------------------------------------------------- /examples/wallet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": [ 27 | ".eslintrc.cjs", 28 | "src" 29 | ], 30 | "references": [ 31 | { 32 | "path": "./tsconfig.node.json" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /examples/wallet/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | ".eslintrc.cjs", 11 | "vite.config.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/wallet/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig } from 'vite'; 5 | import react from '@vitejs/plugin-react-swc'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react()], 10 | }); 11 | -------------------------------------------------------------------------------- /examples/wallet/wallet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wallet 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "identity-connect-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": { 6 | "packages": [ 7 | "examples/*", 8 | "packages/*" 9 | ] 10 | }, 11 | "scripts": { 12 | "build": "turbo run build", 13 | "lint": "turbo run lint", 14 | "lint:fix": "turbo run lint:fix", 15 | "test": "turbo run test", 16 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 17 | }, 18 | "devDependencies": { 19 | "@identity-connect/eslint-config": "*", 20 | "@identity-connect/eslint-plugin": "*", 21 | "dotenv": "^16.0.3", 22 | "eslint": "^8.21.0", 23 | "prettier": "latest", 24 | "syncpack": "^8.4.11", 25 | "turbo": "latest", 26 | "typescript": "5.0.4" 27 | }, 28 | "engines": { 29 | "npm": ">=7.0.0", 30 | "node": ">=14.0.0" 31 | }, 32 | "packageManager": "yarn@1.22.18", 33 | "resolutions": { 34 | "typescript": "5.0.4" 35 | }, 36 | "prettier": "@identity-connect/prettier-config" 37 | } 38 | -------------------------------------------------------------------------------- /packages/api/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | '@identity-connect/eslint-config', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect API 2 | 3 | This packages contains the API definition. 4 | For now this includes a manually-defined schema, but in the near future this will be ported over to OpenApi. 5 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/api", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | "require": "./dist/index.js", 10 | "import": "./dist/index.mjs" 11 | }, 12 | "scripts": { 13 | "build": "tsup src/index.ts --tsconfig tsconfig.build.json --format cjs,esm --sourcemap --dts", 14 | "lint": "prettier --check src", 15 | "lint:fix": "prettier --write src && eslint --cache ./src --fix" 16 | }, 17 | "dependencies": { 18 | "@identity-connect/crypto": "^0.1.3", 19 | "aptos": "^1.20.0" 20 | }, 21 | "devDependencies": { 22 | "@identity-connect/eslint-config": "*", 23 | "@identity-connect/tsconfig": "*", 24 | "axios-mock-adapter": "^1.21.4", 25 | "ts-node": "10.9.1", 26 | "tsup": "^7.1.0", 27 | "typescript": "5.0.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/api/src/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const DEFAULT_BACKEND_URL = 'https://identityconnect.com'; 5 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './constants'; 5 | export * from './network'; 6 | export * from './responseBodies'; 7 | export * from './types'; 8 | export * from './serialization'; 9 | -------------------------------------------------------------------------------- /packages/api/src/network.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export enum NetworkName { 5 | DEVNET = 'devnet', 6 | MAINNET = 'mainnet', 7 | TESTNET = 'testnet', 8 | } 9 | -------------------------------------------------------------------------------- /packages/api/src/responseBodies.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | AccountData, 6 | AnonymousPairingData, 7 | AuthenticatedWalletData, 8 | BackendErrorResponse, 9 | BackendResponse, 10 | BaseFinalizedPairingData, 11 | GetDappData, 12 | NewPairingData, 13 | NewWalletData, 14 | PairingData, 15 | ProjectData, 16 | SerializedDate, 17 | SigningRequestData, 18 | UserData, 19 | WalletData, 20 | } from './types'; 21 | 22 | // region Dapp 23 | 24 | export type GetDappResponse = BackendResponse<{ dapp: GetDappData }>; 25 | export type GetDappSerializedResponse = SerializedDate; 26 | 27 | // endregion 28 | 29 | // region Wallet 30 | 31 | export type CreateWalletConnectionResponse = BackendResponse<{ 32 | wallet: NewWalletData; 33 | }>; 34 | export type CreateWalletConnectionSerializedResponse = SerializedDate; 35 | 36 | export type GetWalletResponse = BackendResponse<{ 37 | wallet: WalletData; 38 | }>; 39 | export type GetWalletSerializedResponse = SerializedDate; 40 | 41 | export type FinalizeConnectionResponse = BackendResponse<{ 42 | wallet: AuthenticatedWalletData; 43 | }>; 44 | export type FinalizeConnectionSerializedResponse = SerializedDate; 45 | 46 | // endregion 47 | 48 | // region Pairing 49 | 50 | export type CreatePairingResponse = BackendResponse<{ pairing: NewPairingData }>; 51 | export type CreatePairingSerializedResponse = SerializedDate; 52 | 53 | export type GetPairingResponse = BackendResponse<{ pairing: PairingData }>; 54 | export type GetPairingSerializedResponse = SerializedDate; 55 | 56 | export type FinalizePairingResponse = BackendResponse<{ pairing: BaseFinalizedPairingData }> | BackendErrorResponse; 57 | export type FinalizePairingSerializedResponse = SerializedDate; 58 | 59 | export type FinalizeAnonymousPairingResponse = BackendResponse<{ pairing: AnonymousPairingData }>; 60 | export type FinalizeAnonymousPairingSerializedResponse = SerializedDate; 61 | 62 | // endregion 63 | 64 | // region Signing request 65 | 66 | export type CreateSigningRequestResponse = BackendResponse<{ 67 | signingRequest: SigningRequestData; 68 | }>; 69 | export type CreateSigningRequestSerializedResponse = SerializedDate; 70 | 71 | export type GetSigningRequestResponse = BackendResponse<{ 72 | signingRequest: SigningRequestData; 73 | }>; 74 | export type GetSigningRequestSerializedResponse = SerializedDate; 75 | 76 | export type GetSigningRequestsResponse = BackendResponse<{ 77 | signingRequests: SigningRequestData[]; 78 | }>; 79 | export type GetSigningRequestsSerializedResponse = SerializedDate; 80 | 81 | export type RespondToSignRequestResponse = BackendResponse<{ 82 | signingRequest: SigningRequestData; 83 | }>; 84 | export type RespondToSignRequestSerializedResponse = SerializedDate; 85 | 86 | export type CancelSigningRequestResponse = BackendResponse<{ 87 | signingRequest: SigningRequestData; 88 | }>; 89 | export type CancelSigningRequestSerializedResponse = SerializedDate; 90 | 91 | // endregion 92 | 93 | // region Other 94 | 95 | export type GetUserAccountsResponse = BackendResponse<{ accounts: AccountData[] }>; 96 | export type GetUserAccountsSerializedResponse = SerializedDate; 97 | 98 | export type GetUserProjectsResponse = BackendResponse<{ projects: ProjectData[] }>; 99 | export type GetUserProjectsSerializedResponse = SerializedDate; 100 | 101 | export type GetUserDataResponse = BackendResponse<{ user: UserData }>; 102 | export type GetUserDataSerializedResponse = SerializedDate; 103 | 104 | export type GetUserPairingsResponse = BackendResponse<{ pairings: PairingData[] }>; 105 | export type GetUserPairingsSerializedResponse = SerializedDate; 106 | 107 | // endregion 108 | -------------------------------------------------------------------------------- /packages/api/src/serialization.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { BCS, HexString, TxnBuilderTypes, Types } from 'aptos'; 5 | 6 | export type JsonEntryFunctionPayload = Types.EntryFunctionPayload & { 7 | // Note: when the type is not specified, we default to entry function 8 | type?: 'entry_function_payload'; 9 | }; 10 | export type JsonMultisigPayload = Types.MultisigPayload & { 11 | type: 'multisig_payload'; 12 | }; 13 | export type JsonPayload = JsonEntryFunctionPayload | JsonMultisigPayload; 14 | 15 | export type SerializedPayload = JsonPayload | string; 16 | export type DeserializedTransactionPayload = JsonPayload | TxnBuilderTypes.TransactionPayload; 17 | 18 | // region Payload arg 19 | 20 | interface SerializedUint8ArrayArg { 21 | stringValue: string; 22 | type: 'Uint8Array'; 23 | } 24 | 25 | type SerializedArg = SerializedUint8ArrayArg; 26 | 27 | function isSerializedArg(argument: any): argument is SerializedArg { 28 | return ( 29 | argument !== undefined && 30 | argument?.stringValue !== undefined && 31 | typeof argument?.stringValue === 'string' && 32 | argument?.type !== undefined 33 | ); 34 | } 35 | 36 | function serializePayloadArg(argument: TArg): SerializedArg | TArg { 37 | if (argument instanceof Uint8Array) { 38 | return { 39 | stringValue: HexString.fromUint8Array(argument).hex(), 40 | type: 'Uint8Array', 41 | }; 42 | } 43 | 44 | // Everything else is already serializable 45 | return argument; 46 | } 47 | 48 | export function deserializePayloadArg(argument: any) { 49 | if (!isSerializedArg(argument)) { 50 | return argument; 51 | } 52 | 53 | if (argument.type === 'Uint8Array') { 54 | return new HexString(argument.stringValue).toUint8Array(); 55 | } 56 | 57 | // Everything else is already deserializable 58 | return argument; 59 | } 60 | 61 | // endregion 62 | 63 | // region Payload 64 | 65 | function isBcsSerializable(payload: any): payload is TxnBuilderTypes.TransactionPayload { 66 | // Note: Just using `instanceof` won't work, since the dapp has its own Aptos bundle 67 | // which is distinct from the bundle used by Petra, and `instanceof` fails 68 | return ( 69 | payload instanceof TxnBuilderTypes.TransactionPayload || 70 | (payload as TxnBuilderTypes.TransactionPayload)?.serialize !== undefined 71 | ); 72 | } 73 | 74 | export function serializeEntryFunctionPayload(payload: Types.EntryFunctionPayload): Types.EntryFunctionPayload { 75 | // Replace arguments with serialized ones 76 | const serializedArgs = payload.arguments.map((arg) => serializePayloadArg(arg)); 77 | return { ...payload, arguments: serializedArgs }; 78 | } 79 | 80 | export function ensurePayloadSerialized( 81 | payload: SerializedPayload | DeserializedTransactionPayload, 82 | ): SerializedPayload { 83 | if (typeof payload === 'string') { 84 | return payload; 85 | } 86 | 87 | // If the payload is serializable to BCS, serialize it to bytes string 88 | if (isBcsSerializable(payload)) { 89 | const payloadBytes = BCS.bcsToBytes(payload); 90 | return HexString.fromUint8Array(payloadBytes).hex(); 91 | } 92 | 93 | if (payload.type === 'multisig_payload') { 94 | const txnPayload = 95 | payload.transaction_payload !== undefined 96 | ? serializeEntryFunctionPayload(payload.transaction_payload) 97 | : undefined; 98 | return { ...payload, transaction_payload: txnPayload }; 99 | } 100 | 101 | return serializeEntryFunctionPayload(payload); 102 | } 103 | 104 | export function deserializeEntryFunctionPayload(payload: Types.EntryFunctionPayload): Types.EntryFunctionPayload { 105 | // Replace arguments with deserialized ones 106 | const deserializedArgs = payload.arguments.map((arg) => deserializePayloadArg(arg)); 107 | return { ...payload, arguments: deserializedArgs }; 108 | } 109 | 110 | export function ensurePayloadDeserialized(payload: SerializedPayload): DeserializedTransactionPayload { 111 | // If the payload is a BCS bytes string, deserialize it into a payload object 112 | if (typeof payload === 'string') { 113 | const encodedPayload = new HexString(payload).toUint8Array(); 114 | const deserializer = new BCS.Deserializer(encodedPayload); 115 | return TxnBuilderTypes.TransactionPayload.deserialize(deserializer); 116 | } 117 | 118 | if (payload.type === 'multisig_payload') { 119 | const txnPayload = 120 | payload.transaction_payload !== undefined 121 | ? deserializeEntryFunctionPayload(payload.transaction_payload) 122 | : undefined; 123 | return { ...payload, transaction_payload: txnPayload }; 124 | } 125 | 126 | return deserializeEntryFunctionPayload(payload); 127 | } 128 | 129 | // endregion 130 | -------------------------------------------------------------------------------- /packages/api/src/types/account.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DappSpecificWallet } from './dappSpecificWallet'; 5 | 6 | export interface AccountData { 7 | accountAddress: string; 8 | createdAt: Date; 9 | ed25519PublicKeyB64: string; 10 | id: string; 11 | transportEd25519PublicKeyB64: string; 12 | updatedAt: Date; 13 | userSubmittedAlias: string | null; 14 | // TODO: figure out why this looks like this 15 | walletAccounts: { 16 | dappSpecificWallet?: DappSpecificWallet | null; 17 | dappSpecificWalletId?: string | null; 18 | walletAccountId: string; 19 | walletName: string | null; 20 | }[]; 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/src/types/dapp.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface RegisteredDappDataBase { 5 | dappSpecificWalletAllowed: boolean; 6 | description: string | null; 7 | feePayerAllowed: boolean; 8 | hostname: string; 9 | iconUrl: string | null; 10 | id: string; 11 | name: string; 12 | } 13 | 14 | export interface GetDappData extends RegisteredDappDataBase { 15 | adminUserId: string; 16 | allowPairingsWithoutHostname: boolean; 17 | createdAt: Date; 18 | id: string; 19 | isDappHostnameVerified: boolean; 20 | updatedAt: Date; 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/src/types/dappSpecificWallet.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface DappSpecificWallet { 5 | addressHex: string; 6 | createdAt: Date; 7 | id: string; 8 | publicKeyHex: string; 9 | registeredDappId: string; 10 | transportEd25519PublicKeyB64: string; 11 | updatedAt: Date; 12 | userId: string; 13 | } 14 | -------------------------------------------------------------------------------- /packages/api/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './account'; 5 | export * from './dapp'; 6 | export * from './pairing'; 7 | export * from './response'; 8 | export * from './serialize'; 9 | export * from './signingRequest'; 10 | export * from './user'; 11 | export * from './wallet'; 12 | export * from './dappSpecificWallet'; 13 | -------------------------------------------------------------------------------- /packages/api/src/types/pairing.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { AccountData } from './account'; 5 | import { RegisteredDappDataBase } from './dapp'; 6 | import { WalletData } from './wallet'; 7 | import { DappSpecificWallet } from './dappSpecificWallet'; 8 | 9 | export enum PairingStatus { 10 | Finalized = 'FINALIZED', 11 | Pending = 'PENDING', 12 | } 13 | 14 | export interface BasePairingData { 15 | createdAt: Date; 16 | dappEd25519PublicKeyB64: string; 17 | dappSpecificWallet?: DappSpecificWallet; 18 | dappSpecificWalletId?: string; 19 | expiresAt: Date; 20 | id: string; 21 | maxDappSequenceNumber: number; 22 | maxWalletSequenceNumber: number; 23 | registeredDapp: RegisteredDappDataBase; 24 | registeredDappId: string; 25 | status: PairingStatus; 26 | updatedAt: Date; 27 | } 28 | 29 | export interface NewPairingData extends BasePairingData { 30 | maxDappSequenceNumber: -1; 31 | maxWalletSequenceNumber: -1; 32 | status: PairingStatus.Pending; 33 | } 34 | 35 | export interface BaseFinalizedPairingData extends BasePairingData { 36 | account: AccountData; 37 | accountId: string; 38 | status: PairingStatus.Finalized; 39 | walletName: string; 40 | } 41 | 42 | export interface AnonymousPairingData extends BaseFinalizedPairingData { 43 | anonymousWallet: WalletData; 44 | anonymousWalletId: string; 45 | } 46 | 47 | export type FinalizedPairingData = BaseFinalizedPairingData | AnonymousPairingData; 48 | export type PairingData = NewPairingData | FinalizedPairingData; 49 | -------------------------------------------------------------------------------- /packages/api/src/types/response.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface BackendSuccessResponse { 5 | data: T; 6 | status: 200; 7 | success: true; 8 | } 9 | 10 | export interface BackendErrorResponse { 11 | message: string; 12 | status: 400 | 401 | 500; 13 | success: false; 14 | } 15 | 16 | export type BackendResponse = BackendSuccessResponse; 17 | -------------------------------------------------------------------------------- /packages/api/src/types/serialize.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export type SerializedDate = T extends (infer S)[] 5 | ? SerializedDate[] 6 | : { 7 | [P in keyof T]: T[P] extends Date ? string : SerializedDate; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/api/src/types/signingRequest.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SecuredEnvelopeTransport } from '@identity-connect/crypto'; 5 | import { RegisteredDappDataBase } from './dapp'; 6 | 7 | export enum SigningRequestTypes { 8 | SIGN_AND_SUBMIT_TRANSACTION = 'SIGN_AND_SUBMIT_TRANSACTION', 9 | SIGN_MESSAGE = 'SIGN_MESSAGE', 10 | SIGN_TRANSACTION = 'SIGN_TRANSACTION', 11 | } 12 | 13 | export enum SigningRequestStatus { 14 | APPROVED = 'APPROVED', 15 | CANCELLED = 'CANCELLED', 16 | INVALID = 'INVALID', 17 | PENDING = 'PENDING', 18 | REJECTED = 'REJECTED', 19 | } 20 | 21 | export interface SigningRequestData { 22 | apiVersion: string; 23 | createdAt: Date; 24 | id: string; 25 | networkName: string | null; 26 | pairing: { 27 | registeredDapp: RegisteredDappDataBase; 28 | }; 29 | pairingId: string; 30 | requestEnvelope: SecuredEnvelopeTransport; 31 | requestType: SigningRequestTypes; 32 | responseEnvelope?: SecuredEnvelopeTransport; 33 | status: SigningRequestStatus; 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/src/types/user.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface UserData { 5 | confirmedPrivacyPolicy: boolean; 6 | confirmedTOS?: boolean; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | username: string; 10 | } 11 | 12 | export interface ProjectData { 13 | adminUserId: string; 14 | allowPairingsWithoutHostname: boolean; 15 | createdAt: Date; 16 | description: string; 17 | hostname: string; 18 | iconUrl: string | null; 19 | id: string; 20 | isDappHostnameVerified: boolean; 21 | name: string; 22 | updatedAt: Date; 23 | } 24 | -------------------------------------------------------------------------------- /packages/api/src/types/wallet.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import type { AccountData } from './account'; 5 | import type { AnonymousPairingData } from './pairing'; 6 | import { DappSpecificWallet } from './dappSpecificWallet'; 7 | 8 | export type WalletName = 'petra' | 'martian' | 'ic' | 'pontem'; 9 | 10 | export type WalletOS = 'linux' | 'osx' | 'win' | 'ios' | 'android' | 'ic'; 11 | 12 | export type WalletPlatform = 13 | /// Desktop 14 | | 'firefox-extension' 15 | | 'chrome-extension' 16 | | 'safari-extension' 17 | | 'brave-extension' 18 | | 'opera-extension' 19 | /// Mobile 20 | | 'kiwi-extension' 21 | | 'native-app' 22 | /// Reserved for IC full custody 23 | | 'ic-dapp-wallet'; 24 | 25 | export interface BaseWalletData { 26 | createdAt: Date; 27 | dappSpecificWallet?: DappSpecificWallet; 28 | dappSpecificWalletId?: string; 29 | icEd25519PublicKeyB64: string; 30 | id: string; 31 | updatedAt: Date; 32 | } 33 | 34 | export interface NewWalletData extends BaseWalletData { 35 | walletEd25519PublicKeyB64: null; 36 | } 37 | 38 | export interface BaseConnectedWalletData extends BaseWalletData { 39 | accounts: AccountData[]; 40 | deviceIdentifier: string; 41 | platform: WalletPlatform; 42 | platformOS: WalletOS; 43 | walletEd25519PublicKeyB64: string; 44 | walletName: WalletName; 45 | } 46 | 47 | export interface AuthenticatedWalletData extends BaseConnectedWalletData { 48 | anonymousPairing: null; 49 | userId: string; 50 | } 51 | 52 | export interface AnonymousWalletData extends BaseConnectedWalletData { 53 | anonymousPairing: AnonymousPairingData; 54 | userId: null; 55 | } 56 | 57 | export type ConnectedWalletData = AuthenticatedWalletData | AnonymousWalletData; 58 | 59 | export type WalletData = NewWalletData | ConnectedWalletData; 60 | -------------------------------------------------------------------------------- /packages/api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | { 8 | "path": "./tsconfig.build.json" 9 | } 10 | ], 11 | "include": [ 12 | ".eslintrc.cjs" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/crypto/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | '@identity-connect/eslint-config', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/crypto/README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect Crypto 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /packages/crypto/jest.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import type { JestConfigWithTsJest } from 'ts-jest'; 5 | 6 | const config: JestConfigWithTsJest = { 7 | preset: 'ts-jest', 8 | testRegex: '/.*\\.(test|spec)?\\.(ts|tsx)$', 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /packages/crypto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/crypto", 3 | "version": "0.1.4", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | "require": "./dist/index.js", 10 | "import": "./dist/index.mjs" 11 | }, 12 | "scripts": { 13 | "build": "tsup src/index.ts --tsconfig tsconfig.build.json --format cjs,esm --sourcemap --dts", 14 | "lint": "prettier --check src", 15 | "lint:fix": "prettier --write src && eslint --cache ./src --fix", 16 | "test": "jest" 17 | }, 18 | "dependencies": { 19 | "@noble/hashes": "^1.3.1", 20 | "aptos": "^1.20.0", 21 | "ed2curve": "^0.3.0", 22 | "tweetnacl": "^1.0.3" 23 | }, 24 | "devDependencies": { 25 | "@identity-connect/eslint-config": "*", 26 | "@identity-connect/tsconfig": "*", 27 | "@types/ed2curve": "^0.2.2", 28 | "@types/jest": "^29.5.1", 29 | "@types/node": "^18.15.11", 30 | "jest": "^29.1.0", 31 | "ts-jest": "^29.1.0", 32 | "ts-node": "10.9.1", 33 | "tsup": "^7.1.0", 34 | "typescript": "5.0.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/crypto/src/__tests__/crypto.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { HexString } from 'aptos'; 5 | import nacl from 'tweetnacl'; 6 | import { encryptAndSignEnvelope, decryptEnvelope, deserializeTransportEnvelope } from '../securedEnvelope'; 7 | import { EnvelopeMessageMismatchError } from '../errors'; 8 | import { decryptObject, encryptObject } from '../encrDecr'; 9 | import { 10 | createEd25519KeyPair, 11 | decodeBase64, 12 | ed25519KeypairFromSecret, 13 | keypairToEd25519, 14 | keypairToX25519, 15 | KeyTypes, 16 | toKey, 17 | } from '../utils'; 18 | 19 | const TEST_MESSAGE_PUBLIC = { 20 | content: 'Hello, world!', 21 | }; 22 | 23 | const TEST_MESSAGE_PRIVATE = { 24 | private: true, 25 | }; 26 | 27 | describe('Encrypts and decrypts', () => { 28 | it('should encrypt and then decrypt with Ed25519 (ephemeral sender x25519)', () => { 29 | const k1 = createEd25519KeyPair(); 30 | const k2 = ed25519KeypairFromSecret(k1.secretKey.key.slice(0, 32)); 31 | 32 | expect(k1.secretKey.key).toEqual(k2.secretKey.key); 33 | }); 34 | 35 | it('should encrypt and then decrypt with Ed25519 (ephemeral sender x25519)', () => { 36 | const receiverEd25519KeyPair = keypairToEd25519( 37 | nacl.sign.keyPair.fromSeed( 38 | HexString.ensure('0x9db6d578a3cd65c35873408d578ac7c5e5e8ae19d0441870f19f383fbbe063ff').toUint8Array(), 39 | ), 40 | ); 41 | const senderEphemeralX25519KeyPair = keypairToX25519( 42 | nacl.box.keyPair.fromSecretKey( 43 | HexString.ensure('0x3bdb40d535c4f578055b7d7be4c45213dab6161aa239224cb6fcd7b4a8af243e').toUint8Array(), 44 | ), 45 | ); 46 | 47 | const encryptedObject = encryptObject( 48 | senderEphemeralX25519KeyPair.secretKey, 49 | receiverEd25519KeyPair.publicKey, 50 | TEST_MESSAGE_PUBLIC, 51 | ); 52 | 53 | const decryptedObject = decryptObject( 54 | senderEphemeralX25519KeyPair.publicKey, 55 | receiverEd25519KeyPair.secretKey, 56 | encryptedObject.secured, 57 | encryptedObject.nonce, 58 | ); 59 | 60 | expect(decryptedObject).toEqual(TEST_MESSAGE_PUBLIC); 61 | }); 62 | 63 | it('should encrypt envelope with Ed25519 (ephemeral sender x25519)', () => { 64 | const senderEd25519KeyPair = createEd25519KeyPair(); 65 | const receiverEd25519KeyPair = createEd25519KeyPair(); 66 | 67 | const secured = encryptAndSignEnvelope( 68 | senderEd25519KeyPair.secretKey, 69 | senderEd25519KeyPair.publicKey, 70 | receiverEd25519KeyPair.publicKey, 71 | 0, 72 | TEST_MESSAGE_PUBLIC, 73 | TEST_MESSAGE_PRIVATE, 74 | ); 75 | 76 | const { publicMessage } = deserializeTransportEnvelope(secured); 77 | const senderEphemeralX25519PublicKeyBytes = decodeBase64(publicMessage._metadata.senderX25519PublicKeyB64); 78 | const senderEphemeralX25519PublicKey = toKey(senderEphemeralX25519PublicKeyBytes, KeyTypes.X25519PublicKey); 79 | 80 | const decrypted = decryptObject( 81 | senderEphemeralX25519PublicKey, 82 | receiverEd25519KeyPair.secretKey, 83 | decodeBase64(secured.encryptedPrivateMessage.securedB64), 84 | decodeBase64(secured.encryptedPrivateMessage.nonceB64), 85 | ); 86 | expect(decrypted).toStrictEqual(TEST_MESSAGE_PRIVATE); 87 | }); 88 | 89 | it('should encrypt and then decrypt envelope with Ed25519 (ephemeral sender x25519)', async () => { 90 | const senderEd25519KeyPair = createEd25519KeyPair(); 91 | const receiverEd25519KeyPair = createEd25519KeyPair(); 92 | 93 | const secured = await encryptAndSignEnvelope( 94 | senderEd25519KeyPair.secretKey, 95 | senderEd25519KeyPair.publicKey, 96 | receiverEd25519KeyPair.publicKey, 97 | 0, 98 | TEST_MESSAGE_PUBLIC, 99 | TEST_MESSAGE_PRIVATE, 100 | ); 101 | 102 | const decrypted = decryptEnvelope( 103 | senderEd25519KeyPair.publicKey, 104 | receiverEd25519KeyPair.secretKey, 105 | secured, 106 | ); 107 | 108 | expect(decrypted.publicMessage.content).toEqual(TEST_MESSAGE_PUBLIC.content); 109 | expect(decrypted.privateMessage.private).toEqual(TEST_MESSAGE_PRIVATE.private); 110 | }); 111 | 112 | it('should not decrypt with the wrong keyPair', () => { 113 | const senderEd25519KeyPair = createEd25519KeyPair(); 114 | const receiverEd25519KeyPair = createEd25519KeyPair(); 115 | const wrongKeyPair = createEd25519KeyPair(); 116 | 117 | const secured = encryptAndSignEnvelope( 118 | senderEd25519KeyPair.secretKey, 119 | senderEd25519KeyPair.publicKey, 120 | receiverEd25519KeyPair.publicKey, 121 | 0, 122 | TEST_MESSAGE_PUBLIC, 123 | TEST_MESSAGE_PRIVATE, 124 | ); 125 | 126 | expect(() => { 127 | decryptEnvelope(senderEd25519KeyPair.publicKey, wrongKeyPair.secretKey, secured); 128 | }).toThrow('Could not decrypt message'); 129 | }); 130 | 131 | it('should explode if the public metadata is tampered with', () => { 132 | const senderEd25519KeyPair = createEd25519KeyPair(); 133 | const receiverEd25519KeyPair = createEd25519KeyPair(); 134 | 135 | const secured = encryptAndSignEnvelope( 136 | senderEd25519KeyPair.secretKey, 137 | senderEd25519KeyPair.publicKey, 138 | receiverEd25519KeyPair.publicKey, 139 | 0, 140 | TEST_MESSAGE_PUBLIC, 141 | TEST_MESSAGE_PRIVATE, 142 | ); 143 | const publicMessage = JSON.parse(secured.serializedPublicMessage); 144 | publicMessage._metadata.sequence = 1337; 145 | secured.serializedPublicMessage = JSON.stringify(publicMessage); 146 | expect(() => { 147 | decryptEnvelope(senderEd25519KeyPair.publicKey, receiverEd25519KeyPair.secretKey, secured); 148 | }).toThrow(EnvelopeMessageMismatchError); 149 | }); 150 | 151 | it('should explode if the private/public message contains same keys', () => { 152 | const senderEd25519KeyPair = createEd25519KeyPair(); 153 | const receiverEd25519KeyPair = createEd25519KeyPair(); 154 | 155 | expect(() => 156 | encryptAndSignEnvelope( 157 | senderEd25519KeyPair.secretKey, 158 | senderEd25519KeyPair.publicKey, 159 | receiverEd25519KeyPair.publicKey, 160 | 0, 161 | // @ts-ignore - required for the test 162 | TEST_MESSAGE_PUBLIC, 163 | { ...TEST_MESSAGE_PRIVATE, content: 'Oh No!' }, 164 | ), 165 | ).toThrow(EnvelopeMessageMismatchError); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /packages/crypto/src/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export class EncryptionEnvelopeError extends Error {} 5 | 6 | export class EnvelopeMessageMismatchError extends EncryptionEnvelopeError { 7 | constructor(message: string, public field: string) { 8 | super(message); 9 | this.name = 'EnvelopeMessageMismatchError'; 10 | Object.setPrototypeOf(this, EnvelopeMessageMismatchError.prototype); 11 | } 12 | } 13 | 14 | export class DecryptionError extends EncryptionEnvelopeError { 15 | constructor(message: string) { 16 | super(message); 17 | this.name = 'DecryptionError'; 18 | Object.setPrototypeOf(this, DecryptionError.prototype); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/crypto/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './encrDecr'; 5 | export * from './errors'; 6 | export * from './securedEnvelope'; 7 | export * from './utils'; 8 | export * from './walletAccounts'; 9 | -------------------------------------------------------------------------------- /packages/crypto/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import nacl from 'tweetnacl'; 5 | 6 | export enum KeyTypes { 7 | 'Ed25519PublicKey' = 'Ed25519PublicKey', 8 | 'Ed25519SecretKey' = 'Ed25519SecretKey', 9 | 'X25519PublicKey' = 'X25519PublicKey', 10 | 'X25519SecretKey' = 'X25519SecretKey', 11 | } 12 | 13 | export interface IKey { 14 | key: Uint8Array; 15 | type: Type; 16 | } 17 | 18 | export type X25519PublicKey = IKey; 19 | export type X25519SecretKey = IKey; 20 | export type X25519KeyPair = { 21 | publicKey: X25519PublicKey; 22 | secretKey: X25519SecretKey; 23 | }; 24 | 25 | export type Ed25519PublicKey = IKey; 26 | export type Ed25519SecretKey = IKey; 27 | export type Ed25519KeyPair = { 28 | publicKey: Ed25519PublicKey; 29 | secretKey: Ed25519SecretKey; 30 | }; 31 | 32 | export type RawKeyPair = { 33 | publicKey: Uint8Array; 34 | secretKey: Uint8Array; 35 | }; 36 | 37 | export function createX25519KeyPair(): X25519KeyPair { 38 | return keypairToX25519(nacl.box.keyPair()); 39 | } 40 | 41 | export function createEd25519KeyPair(): Ed25519KeyPair { 42 | return keypairToEd25519(nacl.sign.keyPair()); 43 | } 44 | 45 | export function toKey( 46 | rawKey: Uint8Array, 47 | type: Type, 48 | ): Type extends KeyTypes.Ed25519PublicKey 49 | ? Ed25519PublicKey 50 | : Type extends KeyTypes.Ed25519SecretKey 51 | ? Ed25519SecretKey 52 | : Type extends KeyTypes.X25519PublicKey 53 | ? X25519PublicKey 54 | : Type extends KeyTypes.X25519SecretKey 55 | ? X25519SecretKey 56 | : never { 57 | return { 58 | key: rawKey, 59 | type, 60 | } as any; 61 | } 62 | 63 | export function keypairToEd25519(keyPair: RawKeyPair): Ed25519KeyPair { 64 | return { 65 | publicKey: toKey(keyPair.publicKey, KeyTypes.Ed25519PublicKey), 66 | secretKey: toKey(keyPair.secretKey, KeyTypes.Ed25519SecretKey), 67 | }; 68 | } 69 | 70 | export function keypairToX25519(keyPair: RawKeyPair): X25519KeyPair { 71 | return { 72 | publicKey: toKey(keyPair.publicKey, KeyTypes.X25519PublicKey), 73 | secretKey: toKey(keyPair.secretKey, KeyTypes.X25519SecretKey), 74 | }; 75 | } 76 | 77 | export function aptosAccountToEd25519Keypair(account: { signingKey: nacl.SignKeyPair }) { 78 | return ed25519KeypairFromSecret(account.signingKey.secretKey); 79 | } 80 | 81 | export function ed25519KeypairFromSecret(ed25519SecretKeyBytes: Uint8Array): Ed25519KeyPair { 82 | return keypairToEd25519(nacl.sign.keyPair.fromSeed(ed25519SecretKeyBytes.slice(0, 32))); 83 | } 84 | 85 | export function decodeBase64(base64Str: string): Uint8Array { 86 | if (globalThis.Buffer) { 87 | return new Uint8Array(Buffer.from(base64Str, 'base64')); 88 | } 89 | return Uint8Array.from(atob(base64Str), (m) => m.codePointAt(0)!); 90 | } 91 | 92 | export function encodeBase64(bytes: Uint8Array): string { 93 | if (globalThis.Buffer) { 94 | return Buffer.from(bytes).toString('base64'); 95 | } 96 | return btoa(Array.from(bytes, (x) => String.fromCodePoint(x)).join('')); 97 | } 98 | 99 | export function concatUint8array(arrayOne: Uint8Array, arrayTwo: Uint8Array): Uint8Array { 100 | const mergedArray = new Uint8Array(arrayOne.length + arrayTwo.length); 101 | mergedArray.set(arrayOne); 102 | mergedArray.set(arrayTwo, arrayOne.length); 103 | return mergedArray; 104 | } 105 | -------------------------------------------------------------------------------- /packages/crypto/src/walletAccounts.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Adding or removing an account? This will be idempotent, but racy 5 | import { sha3_256 } from '@noble/hashes/sha3'; 6 | import { AptosAccount, HexString, TxnBuilderTypes } from 'aptos'; 7 | import { 8 | aptosAccountToEd25519Keypair, 9 | ed25519KeypairFromSecret, 10 | encodeBase64, 11 | Ed25519KeyPair, 12 | Ed25519PublicKey, 13 | Ed25519SecretKey, 14 | } from './utils'; 15 | import { 16 | makeEd25519SecretKeySignCallbackNoDomainSeparation, 17 | messageHash, 18 | SignCallback, 19 | signWithEd25519SecretKey, 20 | } from './encrDecr'; 21 | 22 | const { AuthenticationKey, Ed25519PublicKey: AptosEd25519PublicKey } = TxnBuilderTypes; 23 | 24 | // ADD/REMOVE is used for account connections 25 | export enum AccountConnectionAction { 26 | ADD = 'add', 27 | REMOVE = 'remove', 28 | } 29 | 30 | /** 31 | * When a wallet wants to create a pairing, or add/remove an account from a wallet connection, it must prove that it 32 | * has the secret key for a given account. To do so it uses an `AccountConnectInfo` object. 33 | * 1. Once the `AccountConnectInfo` is assembled, it’s JSON serialized to get a `accountInfoSerialized` string. 34 | * 2. We then domain separate and hash the `accountInfoSerialized` to get the `accountInfoHash`: 35 | * `SHA3-256(SHA3-256('APTOS::IDENTITY_CONNECT::') | SHA3-256(accountInfoSerialized))` 36 | * 3. To obtain the `signature`, we sign the `accountInfoHash` with the Ed25519 private key of the sender, and hex 37 | * encode it. 38 | * 4. These are assembled into an `AccountConnectInfoSerialized`, ready to be sent in an HTTP request. 39 | */ 40 | 41 | export type AccountConnectInfo = { 42 | // The account address 43 | accountAddress: string; 44 | // either 'add' or 'remove' 45 | action: AccountConnectionAction; 46 | // The account public key, base64 47 | ed25519PublicKeyB64: string; 48 | // A unique identifier for this connection: it is either the walletId or the pairingId 49 | // Prevents replay attacks across wallets 50 | intentId: string; 51 | // Prevents replay attacks across time- these are only valid for 5 minutes 52 | timestampMillis: number; 53 | // The public key for the encrypted e2e channel, base64 54 | transportEd25519PublicKeyB64: string; 55 | }; 56 | 57 | export type AccountConnectInfoSerialized = { 58 | accountInfoSerialized: string; 59 | signature: string; 60 | }; 61 | 62 | export function deriveAccountTransportEd25519Keypair( 63 | ed25519SecretKey: Ed25519SecretKey, 64 | ed25519PublicKey: Ed25519PublicKey, 65 | ): Ed25519KeyPair; 66 | 67 | export async function deriveAccountTransportEd25519Keypair( 68 | signCallback: SignCallback, 69 | ed25519PublicKey: Ed25519PublicKey, 70 | ): Promise; 71 | 72 | export function deriveAccountTransportEd25519Keypair( 73 | ed25519SecretKeyOrSignCallback: Ed25519SecretKey | SignCallback, 74 | ed25519PublicKey: Ed25519PublicKey, 75 | ) { 76 | if (ed25519SecretKeyOrSignCallback instanceof Function) { 77 | const seedGeneratorBytes = messageHash(ed25519PublicKey.key, 'TRANSPORT_KEYPAIR'); 78 | return ed25519SecretKeyOrSignCallback(seedGeneratorBytes).then(ed25519KeypairFromSecret); 79 | } 80 | 81 | const seedBytes = signWithEd25519SecretKey(ed25519PublicKey.key, ed25519SecretKeyOrSignCallback, 'TRANSPORT_KEYPAIR'); 82 | return ed25519KeypairFromSecret(seedBytes); 83 | } 84 | 85 | export async function createSerializedAccountInfo( 86 | signCallback: SignCallback, 87 | ed25519PublicKey: Ed25519PublicKey, 88 | transportEd25519PublicKey: Ed25519PublicKey, 89 | action: AccountConnectionAction, 90 | intentId: string, 91 | accountAddress?: string, 92 | ): Promise { 93 | // TODO: WRITE TESTS FOR THIS! 94 | 95 | // Either the passed in Pk, or the Pk derived from the Sk 96 | const authKey = AuthenticationKey.fromEd25519PublicKey(new AptosEd25519PublicKey(ed25519PublicKey.key)); 97 | 98 | // Either the passed in account address, or the one derived from the authKey: (either Pk, or derived from Sk) 99 | const finalAccountAddress = accountAddress || authKey?.derivedAddress().hex(); 100 | 101 | const accountInfo: AccountConnectInfo = { 102 | accountAddress: finalAccountAddress, 103 | action, 104 | ed25519PublicKeyB64: encodeBase64(ed25519PublicKey.key), 105 | intentId, 106 | timestampMillis: Date.now(), 107 | transportEd25519PublicKeyB64: encodeBase64(transportEd25519PublicKey.key), 108 | }; 109 | const accountInfoSerialized = JSON.stringify(accountInfo); 110 | const accountInfoBytes = new TextEncoder().encode(accountInfoSerialized); 111 | const accountInfoHash = sha3_256(accountInfoBytes); 112 | 113 | const signatureBytes = await signCallback(messageHash(accountInfoHash, 'ACCOUNT_INFO')); 114 | const signature = HexString.fromUint8Array(signatureBytes).hex(); 115 | return { 116 | accountInfoSerialized, 117 | signature, 118 | }; 119 | } 120 | 121 | export async function aptosAccountToSerializedInfo( 122 | account: AptosAccount, 123 | intentId: string, 124 | ): Promise { 125 | const key = aptosAccountToEd25519Keypair(account); 126 | const signCallback = makeEd25519SecretKeySignCallbackNoDomainSeparation(key.secretKey); 127 | const transportKey = await deriveAccountTransportEd25519Keypair(signCallback, key.publicKey); 128 | return createSerializedAccountInfo( 129 | signCallback, 130 | key.publicKey, 131 | transportKey.publicKey, 132 | AccountConnectionAction.ADD, 133 | intentId, 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /packages/crypto/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "exclude": [ 11 | "src/__tests__" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/crypto/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "jest", 6 | "node" 7 | ], 8 | "noEmit": true 9 | }, 10 | "references": [ 11 | { 12 | "path": "./tsconfig.build.json" 13 | } 14 | ], 15 | "include": [ 16 | "src/__tests__", 17 | ".eslintrc.cjs", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/dapp-sdk/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | '@identity-connect/eslint-config', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/dapp-sdk/DOCS.md: -------------------------------------------------------------------------------- 1 | # Dapp SDK 2 | 3 | The Dapp SDK is intented for Dapp developers that want to allow the user to connect to the Dapp through IC. 4 | 5 | For now, the Dapp SDK is only available for Typescript, but it’ll very soon be available for Rust and to many more languages thanks to native bindings. 6 | 7 | 💡 **Note**: for web Dapps, we’re planning to include the Dapp SDK into the Wallet Adapter as a built-in provider. This means that using the Wallet Adapter will give your dapp access to IC for free. 8 | 9 | 💡 **Note**: In order to enable your Dapp to work with IC, it first need to be registered through the IC dashboard. 10 | TODO expand on this and add reference to external doc 11 | 12 | ## Client initialization 13 | 14 | The main entrypoint of the Dapp SDK is the `ICDappClient` class. It allows the Dapp to connect to accounts using IC and and send them signature requests. 15 | 16 | In order to initialize the client, the `dappId` associated to your registered Dapp needs to be provided as first argument. 17 | 18 | ```tsx 19 | import { DAPP_ID } from '@src/constants'; 20 | import { ICDappClient } from '@identity-connect/dapp-sdk'; 21 | 22 | const icDappClient = new ICDappClient(DAPP_ID); 23 | ``` 24 | 25 | The client needs to persist a state in order to keep pairings alive across dapp sessions. By default, we are using the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), which should work out of the box for most web dapps. For more complex dapps, the developer should provide a custom implementation of state accessors in the constructor options. 26 | 27 | ```tsx 28 | import { DAPP_ID } from '@src/constants'; 29 | import { ICDappClient, DappPairingData, DappStateAccessors } from '@identity-connect/dapp-sdk'; 30 | 31 | const inMemoryStorage: { [address: string]: DappPairingData } = {}; 32 | const accessors: DappStateAccessors = { 33 | get: (address: string) => inMemoryStorage[address], 34 | update: (address: string, pairing?: DappPairingData) => { 35 | inMemoryStorage[address] = pairing; 36 | }, 37 | }; 38 | 39 | const icDappClient = new ICDappClient(DAPP_ID, { accessors }); 40 | ``` 41 | 42 | **Note**: in the future we might move the default web storage accessors in a separate package “web” package. 43 | 44 | ## Connect an account to your Dapp 45 | 46 | Once the Dapp client is initialized, we can request an account connection by calling the `connect` method. 47 | 48 | ```tsx 49 | // This will open up a connection prompt, in which the user 50 | // can choose to connect one of their accounts 51 | const accountAddress = await icDappClient.connect(); 52 | 53 | if (accountAddress !== undefined) { 54 | // The user approved the connection to an account, and its address is returned 55 | console.log(`Account with address ${accountAddress} is now connected.`) 56 | } 57 | ``` 58 | 59 | If the user is not authenticated yet, they will be prompted to sign in. When signed in, they will be able to either: 60 | 61 | - Pair one of the accounts they previously connected to IC 62 | - Start an anonymous pairing (TODO not available yet in the prompt) 63 | 64 | If the user closes the prompt without selecting an account, the call will return `undefined`, otherwise the paired account’s address is returned. 65 | 66 | ## Send signature requests 67 | 68 | Once an account is connected to the Dapp, we can send signature requests using either the `signMessage` or the `signAndSubmitTransaction` methods. 69 | 70 | The signature API adheres to the [Aptos wallet standard Dapp API](https://aptos.dev/standards/wallets/#dapp-api), so it should feel familiar to developers. 71 | 72 | Since `ICDappClient` has no concept of “active account”, the signature API expects the signer’s address as first argument. 73 | 74 | Below are examples on how to sign a message and a transaction respectively. 75 | 76 | ```tsx 77 | const nonce = Datetime.now(); 78 | const { signature } = await icDappClient.signMessage(signerAddress, { 79 | message: 'Message to be signed', 80 | nonce, 81 | }); 82 | ``` 83 | 84 | ```tsx 85 | const payload = { 86 | arguments: ['0xb0b', 100], 87 | function: '0x1::coin::transfer', 88 | type: 'entry_function_payload' as const, 89 | type_arguments: ['0x1::aptos_coin::AptosCoin'], 90 | }; 91 | const userTxn = await icDappClient.signAndSubmitTransaction( 92 | signerAddress, 93 | payload, 94 | ); 95 | ``` 96 | 97 | ## Disconnect account 98 | 99 | Disconnecting an account is done very simply by calling the `disconnect` method on the DappClient. 100 | 101 | ```tsx 102 | await icDappClient.disconnect(address); 103 | ``` 104 | 105 | The call will instruct the server to expire the pairing, as well as remove it from the client’s state. 106 | -------------------------------------------------------------------------------- /packages/dapp-sdk/README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect Dapp SDK 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /packages/dapp-sdk/jest.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import type { JestConfigWithTsJest } from 'ts-jest'; 5 | 6 | const config: JestConfigWithTsJest = { 7 | preset: 'ts-jest', 8 | testRegex: '/.*\\.(test|spec)?\\.(ts|tsx)$', 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /packages/dapp-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/dapp-sdk", 3 | "version": "0.4.1", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "require": "./dist/index.js", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./src/": "./src/" 14 | }, 15 | "scripts": { 16 | "build": "tsup src/index.ts --tsconfig tsconfig.build.json --format cjs,esm --sourcemap --dts", 17 | "lint": "prettier --check src", 18 | "lint:fix": "prettier --write src && eslint --cache ./src --fix", 19 | "test": "jest" 20 | }, 21 | "dependencies": { 22 | "@identity-connect/api": "^0.4.0", 23 | "@identity-connect/crypto": "^0.1.3", 24 | "@identity-connect/wallet-api": "^0.0.3", 25 | "aptos": "^1.20.0", 26 | "axios": "^1.4.0" 27 | }, 28 | "devDependencies": { 29 | "@identity-connect/eslint-config": "*", 30 | "@identity-connect/tsconfig": "*", 31 | "@types/jest": "^29.5.1", 32 | "axios-mock-adapter": "^1.21.4", 33 | "jest": "^29.1.0", 34 | "ts-jest": "^29.1.0", 35 | "ts-node": "10.9.1", 36 | "tsup": "^7.1.0", 37 | "typescript": "5.0.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/__tests__/MockWindowAPI.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Quick polyfill for window, so we don't have to use jsdom 5 | if (global.window === undefined) { 6 | (global as any).window = { 7 | addEventListener: jest.fn(), 8 | open: jest.fn(), 9 | postMessage: jest.fn(), 10 | removeEventListener: jest.fn(), 11 | ...{ 12 | outerHeight: 768, 13 | outerWidth: 1024, 14 | screenLeft: 0, 15 | screenTop: 0, 16 | }, 17 | }; 18 | } 19 | 20 | class Signal { 21 | private isSet: boolean = false; 22 | private promise?: Promise; 23 | private resolve?: () => void; 24 | 25 | async wait() { 26 | if (this.isSet) { 27 | return; 28 | } 29 | if (this.promise === undefined) { 30 | this.promise = new Promise((resolve) => { 31 | this.resolve = resolve; 32 | }); 33 | } 34 | await this.promise; 35 | } 36 | 37 | set() { 38 | this.isSet = true; 39 | if (this.resolve) { 40 | this.resolve(); 41 | this.promise = undefined; 42 | this.resolve = undefined; 43 | } 44 | } 45 | 46 | clear() { 47 | this.isSet = false; 48 | expect(this.promise).toBeUndefined(); 49 | } 50 | } 51 | 52 | class Pipe { 53 | private buffer: T[] = []; 54 | private nonEmptySignal = new Signal(); 55 | 56 | push(value: T) { 57 | this.buffer.push(value); 58 | this.nonEmptySignal.set(); 59 | } 60 | 61 | async pop() { 62 | await this.nonEmptySignal.wait(); 63 | const first = this.buffer[0]; 64 | this.buffer.shift(); 65 | if (this.buffer.length === 0) { 66 | this.nonEmptySignal.clear(); 67 | } 68 | return first; 69 | } 70 | } 71 | 72 | interface MockWindow { 73 | closed: boolean; 74 | location?: URL; 75 | } 76 | 77 | type MessageListener = (this: Window, event: { data: any }) => void; 78 | 79 | export default class MockWindowAPI { 80 | open = jest.spyOn(window, 'open'); 81 | postMessage = jest.spyOn(window, 'postMessage'); 82 | addEventListener = jest.spyOn(window, 'addEventListener'); 83 | removeEventListener = jest.spyOn(window, 'removeEventListener'); 84 | 85 | private messagePipe = new Pipe(); 86 | private hasListenersSignal = new Signal(); 87 | readonly listeners: Set = new Set(); 88 | 89 | constructor() { 90 | this.postMessage.mockImplementation((message) => { 91 | this.messagePipe.push(message); 92 | }); 93 | 94 | this.addEventListener.mockImplementation((type, listener: any) => { 95 | expect(type).toEqual('message'); 96 | this.listeners.add(listener); 97 | if (this.listeners.size === 1) { 98 | this.hasListenersSignal.set(); 99 | } 100 | }); 101 | 102 | this.removeEventListener.mockImplementation((type, listener: any) => { 103 | expect(type).toEqual('message'); 104 | expect(this.listeners.has(listener)).toBeTruthy(); 105 | this.listeners.delete(listener); 106 | if (this.listeners.size === 0) { 107 | this.hasListenersSignal.clear(); 108 | } 109 | }); 110 | } 111 | 112 | mockClear() { 113 | expect(this.listeners.size).toEqual(0); 114 | this.open.mockClear(); 115 | this.postMessage.mockClear(); 116 | this.addEventListener.mockClear(); 117 | this.removeEventListener.mockClear(); 118 | } 119 | 120 | interceptWindowOpen() { 121 | const mockWindow: MockWindow = { closed: false, location: undefined }; 122 | this.open.mockImplementationOnce((url) => { 123 | mockWindow.location = url !== undefined ? new URL(url) : undefined; 124 | return mockWindow as any as Window; 125 | }); 126 | return mockWindow; 127 | } 128 | 129 | async waitForMessage() { 130 | const message = await this.messagePipe.pop(); 131 | expect(this.postMessage).toHaveBeenCalled(); 132 | return message; 133 | } 134 | 135 | async waitForListeners() { 136 | await this.hasListenersSignal.wait(); 137 | } 138 | 139 | async postMessageAs(response: any, source: any = window) { 140 | expect(this.addEventListener).toHaveBeenCalled(); 141 | for (const listener of this.listeners) { 142 | listener.call(window, { data: response, source }); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/__tests__/prompt.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { openPrompt, waitForPromptResponse } from '../prompt'; 5 | import MockWindowAPI from './MockWindowAPI'; 6 | 7 | const mockWindowApi = new MockWindowAPI(); 8 | afterEach(() => { 9 | mockWindowApi.mockClear(); 10 | }); 11 | 12 | describe(openPrompt, () => { 13 | const mockPromptUrl = 'https://website.com/prompt'; 14 | 15 | it("throws an error if the prompt doesn't open", async () => { 16 | mockWindowApi.open.mockReturnValueOnce(null); 17 | expect(openPrompt(mockPromptUrl)).rejects.toThrowError("Couldn't open prompt"); 18 | }); 19 | 20 | it('opens a prompt and returns its reference', async () => { 21 | const mockPromptWindow = mockWindowApi.interceptWindowOpen(); 22 | const promptWindow = await openPrompt(mockPromptUrl); 23 | expect(promptWindow).toBe(mockPromptWindow); 24 | expect(promptWindow.closed).toBeFalsy(); 25 | expect(promptWindow.location.href).toEqual(mockPromptUrl); 26 | }); 27 | }); 28 | 29 | describe(waitForPromptResponse, () => { 30 | it('returns undefined when the prompt is closed', async () => { 31 | const mockPromptWindow = { closed: false }; 32 | const poller = waitForPromptResponse(mockPromptWindow as Window); 33 | expect(mockWindowApi.listeners.size === 1); 34 | 35 | mockPromptWindow.closed = true; 36 | const response = await poller; 37 | expect(mockWindowApi.listeners.size === 0); 38 | expect(response).toBeUndefined(); 39 | }); 40 | 41 | it('ignores messages that are not from the prompt', async () => { 42 | const mockPromptWindow = { closed: false }; 43 | const poller = waitForPromptResponse(mockPromptWindow as Window); 44 | expect(mockWindowApi.listeners.size === 1); 45 | 46 | const mockMessage = { data: "I'm merely jesting" }; 47 | await mockWindowApi.postMessageAs(mockMessage, {}); 48 | expect(mockWindowApi.listeners.size === 1); 49 | 50 | mockPromptWindow.closed = true; 51 | const response = await poller; 52 | expect(mockWindowApi.listeners.size === 0); 53 | expect(response).toBeUndefined(); 54 | }); 55 | 56 | it('returns messages received from the prompt', async () => { 57 | const mockPromptWindow = { closed: false }; 58 | const poller = waitForPromptResponse(mockPromptWindow as Window); 59 | expect(mockWindowApi.listeners.size === 1); 60 | 61 | const mockMessage = { data: "I'm merely jesting" }; 62 | await mockWindowApi.postMessageAs(mockMessage, mockPromptWindow); 63 | expect(mockWindowApi.listeners.size === 0); 64 | 65 | const response = await poller; 66 | expect(response).toBe(mockMessage); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/__tests__/testUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | ed25519KeypairFromSecret, 6 | Ed25519PublicKey, 7 | Ed25519SecretKey, 8 | encryptAndSignEnvelope, 9 | } from '@identity-connect/crypto'; 10 | import { DappPairingDataMap, DappStateAccessors } from '../state'; 11 | 12 | export function makeMockDappState() { 13 | const mockDappState = { 14 | pairings: {} as DappPairingDataMap, 15 | }; 16 | const mockDappStateAccessors: DappStateAccessors = { 17 | get: async (address) => mockDappState.pairings[address], 18 | getAll: async () => mockDappState.pairings, 19 | update: async (address, pairing) => { 20 | if (pairing === undefined) { 21 | delete mockDappState.pairings[address]; 22 | } else { 23 | mockDappState.pairings[address] = pairing; 24 | } 25 | }, 26 | }; 27 | return { mockDappState, mockDappStateAccessors }; 28 | } 29 | 30 | export async function makeResponseEnvelope( 31 | accountSecretKey: Ed25519SecretKey, 32 | dappPublicKey: Ed25519PublicKey, 33 | body: any, 34 | ) { 35 | const accountPublicKey = ed25519KeypairFromSecret(accountSecretKey.key).publicKey; 36 | return encryptAndSignEnvelope(accountSecretKey, accountPublicKey, dappPublicKey, 0, {}, body); 37 | } 38 | 39 | export interface WrappedPromise extends Promise { 40 | pending: boolean; 41 | rejected: boolean; 42 | resolved: boolean; 43 | } 44 | 45 | /** 46 | * Utility to detect the state of a promise before awaiting it 47 | * @param promise 48 | */ 49 | export function wrapPromise(promise: Promise) { 50 | const wrapper = promise 51 | .then((result) => { 52 | wrapper.pending = false; 53 | wrapper.resolved = true; 54 | return result; 55 | }) 56 | .catch((err) => { 57 | wrapper.pending = false; 58 | wrapper.rejected = true; 59 | throw err; 60 | }) as WrappedPromise; 61 | wrapper.pending = true; 62 | wrapper.resolved = false; 63 | wrapper.rejected = false; 64 | return wrapper; 65 | } 66 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { DEFAULT_BACKEND_URL } from '@identity-connect/api'; 5 | 6 | export const DEFAULT_FRONTEND_URL = DEFAULT_BACKEND_URL; 7 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/errors.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SigningRequestStatus } from '@identity-connect/api'; 5 | 6 | export class SignatureRequestError extends Error { 7 | constructor(status: SigningRequestStatus) { 8 | super(status); 9 | this.name = 'SignatureRequestError'; 10 | Object.setPrototypeOf(this, SignatureRequestError.prototype); 11 | } 12 | } 13 | 14 | export class UnexpectedSignatureResponseError extends Error { 15 | constructor(missingFields: string[]) { 16 | const message = `Missing the following fields: ${missingFields.join(', ')}`; 17 | super(message); 18 | this.name = 'UnexpectedSignatureResponseError'; 19 | Object.setPrototypeOf(this, UnexpectedSignatureResponseError.prototype); 20 | } 21 | } 22 | 23 | export class PairingExpiredError extends Error { 24 | constructor() { 25 | super(); 26 | this.name = 'PairingExpiredError'; 27 | Object.setPrototypeOf(this, PairingExpiredError.prototype); 28 | } 29 | } 30 | 31 | export class UnregisteredDappError extends Error { 32 | constructor() { 33 | super('Dapp ID is invalid or not associated with a registered Dapp.'); 34 | this.name = 'UnregisteredDappError'; 35 | Object.setPrototypeOf(this, UnregisteredDappError.prototype); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './client'; 5 | export * from './state'; 6 | export * from './types'; 7 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/prompt.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const PROMPT_POLLER_INTERVAL = 500; 5 | const DEFAULT_PROMPT_SIZE = { height: 695, width: 465 }; 6 | 7 | export async function openPrompt(url: string, size = DEFAULT_PROMPT_SIZE) { 8 | const { height, width } = size; 9 | const params = { 10 | height, 11 | left: window.screenLeft + window.outerWidth - width, 12 | popup: true, 13 | top: window.screenTop, 14 | width, 15 | }; 16 | 17 | const strParams = Object.entries(params) 18 | .map(([key, value]) => `${key}=${value}`) 19 | .reduce((acc, entry) => `${acc}, ${entry}`); 20 | 21 | const promptWindow = window.open(url, undefined, strParams); 22 | if (promptWindow === null) { 23 | throw new Error("Couldn't open prompt"); 24 | } 25 | 26 | return promptWindow; 27 | } 28 | 29 | export async function waitForPromptResponse(promptWindow: Window) { 30 | return new Promise((resolve) => { 31 | const listeners = { 32 | onMessage: (message: MessageEvent) => { 33 | if (message.source === promptWindow) { 34 | window.removeEventListener('message', listeners.onMessage); 35 | clearTimeout(listeners.promptPollerId); 36 | resolve(message.data); 37 | } 38 | }, 39 | promptPollerId: setInterval(() => { 40 | if (promptWindow.closed) { 41 | window.removeEventListener('message', listeners.onMessage); 42 | clearTimeout(listeners.promptPollerId); 43 | resolve(undefined); 44 | } 45 | }, PROMPT_POLLER_INTERVAL), 46 | }; 47 | 48 | window.addEventListener('message', listeners.onMessage); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/state.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface DappPairingData { 5 | accountAddress: string; 6 | accountAlias?: string; 7 | accountEd25519PublicKeyB64: string; 8 | accountTransportEd25519PublicKeyB64: string; 9 | currSequenceNumber: number; 10 | dappEd25519PublicKeyB64: string; 11 | dappEd25519SecretKeyB64: string; 12 | dappWalletId?: string; 13 | pairingId: string; 14 | } 15 | 16 | export type DappPairingDataMap = { [address: string]: DappPairingData }; 17 | 18 | export interface DappStateAccessors { 19 | get: (address: string) => Promise; 20 | getAll: () => Promise; 21 | update: (address: string, pairing?: DappPairingData) => Promise; 22 | } 23 | 24 | export const DAPP_PAIRINGS_WINDOW_STORAGE_KEY = 'icDappPairings'; 25 | 26 | /** 27 | * Default implementation of DappStateAccessors that uses the Window localStorage API. 28 | * This should work for most dapps. 29 | */ 30 | export const windowStateAccessors: DappStateAccessors = { 31 | async get(address: string) { 32 | const pairings = await this.getAll(); 33 | return pairings[address]; 34 | }, 35 | async getAll() { 36 | const serialized = window.localStorage.getItem(DAPP_PAIRINGS_WINDOW_STORAGE_KEY); 37 | return serialized ? (JSON.parse(serialized) as DappPairingDataMap) : {}; 38 | }, 39 | async update(address: string, pairing?: DappPairingData) { 40 | const pairings = await this.getAll(); 41 | if (pairing === undefined) { 42 | delete pairings[address]; 43 | } else { 44 | pairings[address] = pairing; 45 | } 46 | const newSerialized = JSON.stringify(pairings); 47 | window.localStorage.setItem(DAPP_PAIRINGS_WINDOW_STORAGE_KEY, newSerialized); 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface CancelToken { 5 | cancelled: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /packages/dapp-sdk/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SignAndSubmitTransactionResponseArgs, SignMessageResponseArgs } from '@identity-connect/wallet-api'; 5 | import { UnexpectedSignatureResponseError } from './errors'; 6 | 7 | const SIGN_MESSAGE_RESPONSE_REQUIRED_FIELDS: (keyof SignMessageResponseArgs)[] = [ 8 | 'address', 9 | 'application', 10 | 'chainId', 11 | 'fullMessage', 12 | 'message', 13 | 'nonce', 14 | 'prefix', 15 | 'signature', 16 | ]; 17 | 18 | export function validateSignMessageResponse(response: SignMessageResponseArgs) { 19 | const providedFields = new Set(Object.keys(response)); 20 | const missingFields = SIGN_MESSAGE_RESPONSE_REQUIRED_FIELDS.filter((field) => !providedFields.has(field)); 21 | if (missingFields.length > 0) { 22 | throw new UnexpectedSignatureResponseError(missingFields); 23 | } 24 | } 25 | 26 | const SIGN_AND_SUBMIT_TRANSACTION_RESPONSE_REQUIRED_FIELDS: (keyof SignAndSubmitTransactionResponseArgs)[] = ['hash']; 27 | 28 | export function validateSignAndSubmitTransactionResponse(response: SignAndSubmitTransactionResponseArgs) { 29 | const providedFields = new Set(Object.keys(response)); 30 | const missingFields = SIGN_AND_SUBMIT_TRANSACTION_RESPONSE_REQUIRED_FIELDS.filter( 31 | (field) => !providedFields.has(field), 32 | ); 33 | if (missingFields.length > 0) { 34 | throw new UnexpectedSignatureResponseError(missingFields); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/dapp-sdk/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2021", 6 | "dom" 7 | ], 8 | "types": [ 9 | "node" 10 | ], 11 | "rootDir": "src", 12 | "outDir": "dist" 13 | }, 14 | "include": [ 15 | "src" 16 | ], 17 | "exclude": [ 18 | "src/__tests__" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/dapp-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2021", 6 | "dom" 7 | ], 8 | "types": [ 9 | "jest", 10 | "node" 11 | ], 12 | "noEmit": true 13 | }, 14 | "references": [ 15 | { 16 | "path": "./tsconfig.build.json" 17 | } 18 | ], 19 | "include": [ 20 | "src/__tests__", 21 | ".eslintrc.cjs", 22 | "jest.config.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | env: { 6 | es2021: true, 7 | jest: true, 8 | }, 9 | extends: ['airbnb', 'airbnb-typescript', 'plugin:typescript-sort-keys/recommended'], 10 | ignorePatterns: ['dist/**/*', '*.css', '*.jsx'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | project: ['tsconfig.json', 'tsconfig.build.json'], 17 | ecmaVersion: 'latest', 18 | sourceType: 'module', 19 | }, 20 | plugins: [ 21 | '@identity-connect', 22 | '@typescript-eslint', 23 | 'react', 24 | 'react-hooks', 25 | 'sort-class-members', 26 | 'sort-destructure-keys', 27 | 'sort-keys-fix', 28 | 'header', 29 | ], 30 | rules: { 31 | '@typescript-eslint/brace-style': 'off', 32 | '@typescript-eslint/indent': 'off', 33 | '@typescript-eslint/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 34 | '@typescript-eslint/naming-convention': [ 35 | 'error', 36 | { 37 | selector: 'variableLike', 38 | modifiers: ['unused'], 39 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'], 40 | leadingUnderscore: 'allow', 41 | }, 42 | ], 43 | '@typescript-eslint/no-use-before-define': 'off', 44 | // allow unused variables that start with an underscore 45 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 46 | 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 47 | 'function-paren-newline': 'off', 48 | 'header/header': [2, 'line', [' Copyright © Aptos', ' SPDX-License-Identifier: Apache-2.0'], 2], 49 | 'implicit-arrow-linebreak': 'off', 50 | 'import/prefer-default-export': 'off', 51 | 'max-classes-per-file': 'off', 52 | 'max-len': ['error', { code: 120 }], 53 | 'no-confusing-arrow': 'off', 54 | 'no-continue': 'off', 55 | // Replacing airbnb rule with following, to re-enable 'ForOfStatement' 56 | 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 57 | 'no-underscore-dangle': 'off', 58 | // Allow prepending statements with void to explicitly ignore the return value 59 | 'no-void': ['error', { allowAsStatement: true }], 60 | 'object-curly-newline': 'off', 61 | 'operator-linebreak': 'off', 62 | 'react/require-default-props': 0, 63 | 'react-hooks/exhaustive-deps': 'warn', 64 | 'react-hooks/rules-of-hooks': 'error', 65 | 'react/jsx-closing-tag-location': 'off', 66 | 'react/jsx-curly-newline': 'off', 67 | 'react/jsx-indent': 'off', 68 | 'react/jsx-one-expression-per-line': 'off', 69 | 'react/jsx-props-no-spreading': 'off', 70 | 'react/jsx-wrap-multilines': 'off', 71 | 'sort-destructure-keys/sort-destructure-keys': 2, 72 | 'sort-keys-fix/sort-keys-fix': 'warn', 73 | 'sort-keys': ['error', 'asc', { caseSensitive: true, minKeys: 2, natural: false }], 74 | 'newline-per-chained-call': ['off'], 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "dependencies": { 7 | "@identity-connect/eslint-plugin": "*", 8 | "@typescript-eslint/eslint-plugin": "^5.51.0", 9 | "@typescript-eslint/parser": "^5.51.0", 10 | "eslint": "^8.33.0", 11 | "eslint-config-airbnb": "^19.0.4", 12 | "eslint-config-airbnb-typescript": "^17.0.0", 13 | "eslint-plugin-header": "^3.1.1", 14 | "eslint-plugin-import": "^2.27.5", 15 | "eslint-plugin-jsx-a11y": "^6.6.1", 16 | "eslint-plugin-promise": "^6.0.0", 17 | "eslint-plugin-react": "^7.30.1", 18 | "eslint-plugin-react-hooks": "^4.6.0", 19 | "eslint-plugin-sort-class-members": "^1.14.1", 20 | "eslint-plugin-sort-destructure-keys": "^1.4.0", 21 | "eslint-plugin-sort-keys-fix": "^1.1.2", 22 | "eslint-plugin-typescript-sort-keys": "^2.3.0", 23 | "typescript": "5.0.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint-plugin/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | rules: { 6 | 'no-client-date': require('./rules/no-client-date'), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/eslint-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/eslint-plugin", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "peerDependencies": { 10 | "eslint": "^8.33.0" 11 | }, 12 | "devDependencies": { 13 | "jest": "^29.5.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/eslint-plugin/rules/no-client-date.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | meta: { 6 | type: 'problem', 7 | docs: { 8 | description: 'Use server time instead of client time', 9 | }, 10 | fixable: 'code', 11 | messages: { 12 | dateNow: 'Use getServerTime() instead of Date.now().', 13 | newDate: 'Use getServerDate() instead of new Date().', 14 | }, 15 | }, 16 | create(context) { 17 | return { 18 | CallExpression(node) { 19 | const { object, property } = node.callee; 20 | if (object?.name === 'Date' && property?.name === 'now') { 21 | context.report({ 22 | node, 23 | messageId: 'dateNow', 24 | fix: (fixer) => { 25 | return fixer.replaceText(node, 'getServerTime()'); 26 | }, 27 | }); 28 | } 29 | }, 30 | NewExpression(node) { 31 | if (node.callee.name === 'Date' && node.arguments.length === 0) { 32 | context.report({ 33 | node, 34 | messageId: 'newDate', 35 | fix: (fixer) => { 36 | return fixer.replaceText(node, 'getServerDate()'); 37 | }, 38 | }); 39 | } 40 | }, 41 | }; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/eslint-plugin/rules/no-client-date.test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { RuleTester } = require('eslint'); 5 | const noClientDateRule = require('./no-client-date'); 6 | 7 | const ruleTester = new RuleTester(); 8 | ruleTester.run('no-client-date', noClientDateRule, { 9 | valid: [ 10 | { 11 | code: 'getServerDate()', 12 | }, 13 | { 14 | code: 'getServerTime()', 15 | }, 16 | { 17 | code: 'new Date(timestamp)', 18 | }, 19 | ], 20 | invalid: [ 21 | { 22 | code: 'Date.now()', 23 | errors: [{ messageId: 'dateNow' }], 24 | output: 'getServerTime()', 25 | }, 26 | { 27 | code: 'new Date()', 28 | errors: [{ messageId: 'newDate' }], 29 | output: 'getServerDate()', 30 | }, 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /packages/prettier-config/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /packages/prettier-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/prettier-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.json" 6 | } 7 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "es2021", 6 | "lib": [ 7 | "es2021" 8 | ], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "skipLibCheck": true, 16 | "noEmitOnError": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/wallet-api/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | '@identity-connect/eslint-config', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/wallet-api/README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect Wallet Standard 2 | 3 | This packages contains the definitions for the Wallet API Standard. 4 | Will be moved under an `@aptos-labs` owned package later. 5 | -------------------------------------------------------------------------------- /packages/wallet-api/jest.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import type { JestConfigWithTsJest } from 'ts-jest'; 5 | 6 | const config: JestConfigWithTsJest = { 7 | preset: 'ts-jest', 8 | testRegex: '/.*\\.(test|spec)?\\.(ts|tsx)$', 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /packages/wallet-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/wallet-api", 3 | "version": "0.0.3", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | "require": "./dist/index.js", 10 | "import": "./dist/index.mjs" 11 | }, 12 | "scripts": { 13 | "build": "tsup src/index.ts --tsconfig tsconfig.build.json --format cjs,esm --sourcemap --dts", 14 | "lint": "prettier --check src", 15 | "lint:fix": "prettier --write src && eslint --cache ./src --fix" 16 | }, 17 | "dependencies": { 18 | "aptos": "^1.20.0" 19 | }, 20 | "devDependencies": { 21 | "@identity-connect/eslint-config": "*", 22 | "@identity-connect/tsconfig": "*", 23 | "ts-node": "10.9.1", 24 | "tsup": "^7.1.0", 25 | "typescript": "5.0.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/wallet-api/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './serialization'; 5 | export * from './types'; 6 | export * from './utils'; 7 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/bcsSerialization.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { BCS, HexString } from 'aptos'; 5 | 6 | export interface BcsSerializable { 7 | serialize: (serializer: BCS.Serializer) => void; 8 | } 9 | 10 | export interface BcsDeserializable { 11 | deserialize: (deserializer: BCS.Deserializer) => T; 12 | } 13 | 14 | /** 15 | * Check if a value is BCS serializable 16 | */ 17 | export function isBcsSerializable(value: any): value is BcsSerializable { 18 | return (value as BcsSerializable)?.serialize !== undefined; 19 | } 20 | 21 | export function bcsSerialize(serializable: BcsSerializable) { 22 | const serializedValueBytes = BCS.bcsToBytes(serializable); 23 | return HexString.fromUint8Array(serializedValueBytes).toString(); 24 | } 25 | 26 | export function bcsDeserialize(deserializable: BcsDeserializable, serializedValue: string) { 27 | const serializedValueBytes = new HexString(serializedValue).toUint8Array(); 28 | const deserializer = new BCS.Deserializer(serializedValueBytes); 29 | return deserializable.deserialize(deserializer); 30 | } 31 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/error.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export class UnexpectedValueError extends Error { 5 | constructor(message?: string) { 6 | super(message); 7 | this.name = 'UnexpectedValueError'; 8 | Object.setPrototypeOf(this, UnexpectedValueError.prototype); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './bcsSerialization'; 5 | export * from './jsonPayload'; 6 | export * from './rawTxn'; 7 | export * from './signAndSubmitTransactionRequestArgs'; 8 | export * from './signTransactionRequestArgs'; 9 | export * from './signTransactionResponseArgs'; 10 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/jsonPayload.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { HexString, Types } from 'aptos'; 5 | import type { EntryFunctionJsonTransactionPayload, JsonTransactionPayload } from '../types'; 6 | import { UnexpectedValueError } from './error'; 7 | 8 | function normalizeEntryFunctionArg(arg: any): any { 9 | if (arg instanceof Uint8Array) { 10 | return HexString.fromUint8Array(arg).toString(); 11 | } 12 | if (Array.isArray(arg)) { 13 | return arg.map(normalizeEntryFunctionArg); 14 | } 15 | return arg; 16 | } 17 | 18 | function ensureEntryFunctionPayloadSerializable( 19 | payload: Types.EntryFunctionPayload, 20 | ): EntryFunctionJsonTransactionPayload { 21 | const normalizedArgs = payload.arguments.map(normalizeEntryFunctionArg); 22 | return { 23 | type: 'entry_function_payload', 24 | ...payload, 25 | arguments: normalizedArgs, 26 | }; 27 | } 28 | 29 | export function ensureJsonTransactionPayloadSerializable(payload: JsonTransactionPayload): JsonTransactionPayload { 30 | if (payload.type === 'entry_function_payload') { 31 | return ensureEntryFunctionPayloadSerializable(payload); 32 | } 33 | if (payload.type === 'multisig_payload') { 34 | const innerPayload = 35 | payload.transaction_payload !== undefined 36 | ? ensureEntryFunctionPayloadSerializable(payload.transaction_payload) 37 | : undefined; 38 | return { ...payload, transaction_payload: innerPayload }; 39 | } 40 | throw new UnexpectedValueError(); 41 | } 42 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/rawTxn.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import { bcsDeserialize, bcsSerialize } from './bcsSerialization'; 6 | import { UnexpectedValueError } from './error'; 7 | 8 | export type SerializableRawTransaction = 9 | | TxnBuilderTypes.RawTransaction 10 | | TxnBuilderTypes.FeePayerRawTransaction 11 | | TxnBuilderTypes.MultiAgentRawTransaction; 12 | 13 | export interface SerializedSimpleRawTransaction { 14 | type: 'raw_txn'; 15 | value: string; 16 | } 17 | 18 | export interface SerializedFeePayerRawTransaction { 19 | type: 'fee_payer_raw_txn'; 20 | value: string; 21 | } 22 | 23 | export interface SerializedMultiAgentRawTransaction { 24 | type: 'multi_agent_raw_txn'; 25 | value: string; 26 | } 27 | 28 | export type SerializedRawTransaction = 29 | | SerializedSimpleRawTransaction 30 | | SerializedFeePayerRawTransaction 31 | | SerializedMultiAgentRawTransaction; 32 | 33 | export function serializeRawTransaction(rawTxn: TxnBuilderTypes.RawTransaction): SerializedSimpleRawTransaction; 34 | export function serializeRawTransaction( 35 | rawTxn: TxnBuilderTypes.FeePayerRawTransaction, 36 | ): SerializedFeePayerRawTransaction; 37 | export function serializeRawTransaction( 38 | rawTxn: TxnBuilderTypes.MultiAgentRawTransaction, 39 | ): SerializedMultiAgentRawTransaction; 40 | export function serializeRawTransaction(rawTxn: SerializableRawTransaction): SerializedRawTransaction; 41 | 42 | export function serializeRawTransaction(rawTxn: SerializableRawTransaction): SerializedRawTransaction { 43 | const value = bcsSerialize(rawTxn); 44 | if ('fee_payer_address' in rawTxn) { 45 | return { type: 'fee_payer_raw_txn', value }; 46 | } 47 | if ('secondary_signer_addresses' in rawTxn) { 48 | return { type: 'multi_agent_raw_txn', value }; 49 | } 50 | if ('chain_id' in rawTxn) { 51 | return { type: 'raw_txn', value }; 52 | } 53 | throw new UnexpectedValueError('Invalid raw transaction type'); 54 | } 55 | 56 | export function deserializeRawTransaction(serialized: SerializedSimpleRawTransaction): TxnBuilderTypes.RawTransaction; 57 | export function deserializeRawTransaction( 58 | serialized: SerializedFeePayerRawTransaction, 59 | ): TxnBuilderTypes.FeePayerRawTransaction; 60 | export function deserializeRawTransaction( 61 | serialized: SerializedMultiAgentRawTransaction, 62 | ): TxnBuilderTypes.MultiAgentRawTransaction; 63 | export function deserializeRawTransaction(serialized: SerializedRawTransaction): SerializableRawTransaction; 64 | 65 | export function deserializeRawTransaction(serialized: SerializedRawTransaction): SerializableRawTransaction { 66 | switch (serialized.type) { 67 | case 'raw_txn': 68 | return bcsDeserialize(TxnBuilderTypes.RawTransaction, serialized.value); 69 | case 'fee_payer_raw_txn': 70 | return bcsDeserialize( 71 | TxnBuilderTypes.RawTransactionWithData, 72 | serialized.value, 73 | ) as TxnBuilderTypes.FeePayerRawTransaction; 74 | case 'multi_agent_raw_txn': 75 | return bcsDeserialize( 76 | TxnBuilderTypes.RawTransactionWithData, 77 | serialized.value, 78 | ) as TxnBuilderTypes.MultiAgentRawTransaction; 79 | default: 80 | throw new UnexpectedValueError('Invalid raw transaction type'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/signAndSubmitTransactionRequestArgs.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import type { 6 | SignAndSubmitTransactionRequestArgs, 7 | SignAndSubmitTransactionWithFeePayerRawTxnRequestArgs, 8 | SignAndSubmitTransactionWithPayloadRequestArgs, 9 | SignAndSubmitTransactionWithRawTxnRequestArgs, 10 | } from '../types'; 11 | import { JsonTransactionPayload, TransactionOptions } from '../types'; 12 | import { bcsDeserialize, bcsSerialize, isBcsSerializable } from './bcsSerialization'; 13 | 14 | import { UnexpectedValueError } from './error'; 15 | import { ensureJsonTransactionPayloadSerializable } from './jsonPayload'; 16 | import { 17 | deserializeRawTransaction, 18 | type SerializedFeePayerRawTransaction, 19 | type SerializedSimpleRawTransaction, 20 | serializeRawTransaction, 21 | } from './rawTxn'; 22 | 23 | export interface SerializedSignAndSubmitTransactionWithPayloadRequestArgs { 24 | options?: TransactionOptions; 25 | payload: JsonTransactionPayload | string; 26 | } 27 | 28 | export interface SerializedSignAndSubmitTransactionWithRawTxnRequestArgs { 29 | rawTxn: SerializedSimpleRawTransaction; 30 | } 31 | 32 | export interface SerializedSignAndSubmitTransactionWithFeePayerRawTxnRequestArgs { 33 | feePayerAuthenticator: string; 34 | rawTxn: SerializedFeePayerRawTransaction; 35 | } 36 | 37 | export type SerializedSignAndSubmitTransactionRequestArgs = 38 | | SerializedSignAndSubmitTransactionWithPayloadRequestArgs 39 | | SerializedSignAndSubmitTransactionWithRawTxnRequestArgs 40 | | SerializedSignAndSubmitTransactionWithFeePayerRawTxnRequestArgs; 41 | 42 | export function serializeSignAndSubmitTransactionRequestArgs( 43 | args: SignAndSubmitTransactionRequestArgs, 44 | ): SerializedSignAndSubmitTransactionRequestArgs { 45 | if ('payload' in args) { 46 | const serializedPayload = isBcsSerializable(args.payload) 47 | ? bcsSerialize(args.payload) 48 | : ensureJsonTransactionPayloadSerializable(args.payload); 49 | return { options: args.options, payload: serializedPayload }; 50 | } 51 | if ('feePayerAuthenticator' in args) { 52 | return { 53 | feePayerAuthenticator: bcsSerialize(args.feePayerAuthenticator), 54 | rawTxn: serializeRawTransaction(args.rawTxn), 55 | }; 56 | } 57 | if ('rawTxn' in args) { 58 | return { rawTxn: serializeRawTransaction(args.rawTxn) }; 59 | } 60 | throw new UnexpectedValueError(); 61 | } 62 | 63 | export function deserializeSignAndSubmitTransactionRequestArgs( 64 | args: SerializedSignAndSubmitTransactionWithPayloadRequestArgs, 65 | ): SignAndSubmitTransactionWithPayloadRequestArgs; 66 | export function deserializeSignAndSubmitTransactionRequestArgs( 67 | args: SerializedSignAndSubmitTransactionWithRawTxnRequestArgs, 68 | ): SignAndSubmitTransactionWithRawTxnRequestArgs; 69 | export function deserializeSignAndSubmitTransactionRequestArgs( 70 | args: SerializedSignAndSubmitTransactionWithFeePayerRawTxnRequestArgs, 71 | ): SignAndSubmitTransactionWithFeePayerRawTxnRequestArgs; 72 | export function deserializeSignAndSubmitTransactionRequestArgs( 73 | args: SerializedSignAndSubmitTransactionRequestArgs, 74 | ): SignAndSubmitTransactionRequestArgs; 75 | 76 | export function deserializeSignAndSubmitTransactionRequestArgs( 77 | args: SerializedSignAndSubmitTransactionRequestArgs, 78 | ): SignAndSubmitTransactionRequestArgs { 79 | if ('payload' in args) { 80 | const payload = 81 | typeof args.payload === 'string' 82 | ? bcsDeserialize(TxnBuilderTypes.TransactionPayload, args.payload) 83 | : args.payload; 84 | return { options: args.options, payload }; 85 | } 86 | if ('feePayerAuthenticator' in args) { 87 | const deserializedRawTxn = deserializeRawTransaction(args.rawTxn); 88 | const feePayerAuthenticator = bcsDeserialize(TxnBuilderTypes.AccountAuthenticator, args.feePayerAuthenticator); 89 | return { feePayerAuthenticator, rawTxn: deserializedRawTxn }; 90 | } 91 | if ('rawTxn' in args) { 92 | const deserializedRawTxn = deserializeRawTransaction(args.rawTxn); 93 | return { rawTxn: deserializedRawTxn }; 94 | } 95 | throw new UnexpectedValueError(); 96 | } 97 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/signTransactionRequestArgs.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import type { 6 | JsonTransactionPayload, 7 | SignTransactionRequestArgs, 8 | SignTransactionWithPayloadRequestArgs, 9 | SignTransactionWithRawTxnRequestArgs, 10 | TransactionOptions, 11 | } from '../types'; 12 | import { bcsDeserialize, bcsSerialize, isBcsSerializable } from './bcsSerialization'; 13 | import { UnexpectedValueError } from './error'; 14 | import { ensureJsonTransactionPayloadSerializable } from './jsonPayload'; 15 | import type { SerializedRawTransaction } from './rawTxn'; 16 | import { deserializeRawTransaction, serializeRawTransaction } from './rawTxn'; 17 | 18 | export interface SerializedSignTransactionWithPayloadRequestArgs { 19 | options?: TransactionOptions; 20 | payload: JsonTransactionPayload | string; 21 | } 22 | 23 | export interface SerializedSignTransactionWithRawTxnRequestArgs { 24 | rawTxn: SerializedRawTransaction; 25 | } 26 | 27 | export type SerializedSignTransactionRequestArgs = 28 | | SerializedSignTransactionWithPayloadRequestArgs 29 | | SerializedSignTransactionWithRawTxnRequestArgs; 30 | 31 | export function serializeSignTransactionRequestArgs( 32 | args: SignTransactionRequestArgs, 33 | ): SerializedSignTransactionRequestArgs { 34 | if ('payload' in args) { 35 | const serializedPayload = isBcsSerializable(args.payload) 36 | ? bcsSerialize(args.payload) 37 | : ensureJsonTransactionPayloadSerializable(args.payload); 38 | return { options: args.options, payload: serializedPayload }; 39 | } 40 | if ('rawTxn' in args) { 41 | const serializedRawTxn = serializeRawTransaction(args.rawTxn); 42 | return { rawTxn: serializedRawTxn }; 43 | } 44 | throw new UnexpectedValueError(); 45 | } 46 | 47 | export function deserializeSignTransactionRequestArgs( 48 | args: SerializedSignTransactionWithPayloadRequestArgs, 49 | ): SignTransactionWithPayloadRequestArgs; 50 | export function deserializeSignTransactionRequestArgs( 51 | args: SerializedSignTransactionWithRawTxnRequestArgs, 52 | ): SignTransactionWithRawTxnRequestArgs; 53 | export function deserializeSignTransactionRequestArgs( 54 | args: SerializedSignTransactionRequestArgs, 55 | ): SignTransactionRequestArgs; 56 | 57 | export function deserializeSignTransactionRequestArgs( 58 | args: SerializedSignTransactionRequestArgs, 59 | ): SignTransactionRequestArgs { 60 | if ('payload' in args) { 61 | const payload = 62 | typeof args.payload === 'string' 63 | ? bcsDeserialize(TxnBuilderTypes.TransactionPayload, args.payload) 64 | : args.payload; 65 | return { options: args.options, payload }; 66 | } 67 | if ('rawTxn' in args) { 68 | const deserializedRawTxn = deserializeRawTransaction(args.rawTxn); 69 | return { rawTxn: deserializedRawTxn }; 70 | } 71 | throw new UnexpectedValueError(); 72 | } 73 | -------------------------------------------------------------------------------- /packages/wallet-api/src/serialization/signTransactionResponseArgs.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import type { SignTransactionResponseArgs } from '../types'; 6 | import { bcsDeserialize, bcsSerialize } from './bcsSerialization'; 7 | 8 | export interface SerializedSignTransactionWithPayloadResponseArgs { 9 | accountAuthenticator: string; 10 | rawTxn: string; 11 | } 12 | 13 | export interface SerializedSignTransactionWithRawTxnResponseArgs { 14 | accountAuthenticator: string; 15 | } 16 | 17 | export type SerializedSignTransactionResponseArgs = 18 | | SerializedSignTransactionWithPayloadResponseArgs 19 | | SerializedSignTransactionWithRawTxnResponseArgs; 20 | 21 | export function serializeSignTransactionResponseArgs( 22 | args: SignTransactionResponseArgs, 23 | ): SerializedSignTransactionResponseArgs { 24 | const accountAuthenticator = bcsSerialize(args.accountAuthenticator); 25 | if ('rawTxn' in args) { 26 | const rawTxn = bcsSerialize(args.rawTxn); 27 | return { accountAuthenticator, rawTxn }; 28 | } 29 | return { accountAuthenticator }; 30 | } 31 | 32 | export function deserializeSignTransactionResponseArgs( 33 | args: SerializedSignTransactionResponseArgs, 34 | ): SignTransactionResponseArgs { 35 | const accountAuthenticator = bcsDeserialize(TxnBuilderTypes.AccountAuthenticator, args.accountAuthenticator); 36 | if ('rawTxn' in args) { 37 | const rawTxn = bcsDeserialize(TxnBuilderTypes.RawTransaction, args.rawTxn); 38 | return { accountAuthenticator, rawTxn }; 39 | } 40 | return { accountAuthenticator }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/account.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface Account { 5 | address: string; 6 | name?: string; 7 | publicKey: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './responses'; 5 | export * from './requests'; 6 | export * from './account'; 7 | export * from './jsonPayload'; 8 | export * from './message'; 9 | export * from './network'; 10 | export * from './transactionOptions'; 11 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/jsonPayload.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Types } from 'aptos'; 5 | 6 | export type EntryFunctionJsonTransactionPayload = Types.EntryFunctionPayload & { 7 | type: 'entry_function_payload'; 8 | }; 9 | 10 | export type MultisigJsonTransactionPayload = Types.MultisigPayload & { 11 | type: 'multisig_payload'; 12 | }; 13 | 14 | export type JsonTransactionPayload = EntryFunctionJsonTransactionPayload | MultisigJsonTransactionPayload; 15 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/message.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface FullMessageFlags { 5 | address?: boolean; 6 | application?: boolean; 7 | chainId?: boolean; 8 | } 9 | 10 | export interface FullMessageParams { 11 | address: string; 12 | application: string; 13 | chainId: number; 14 | message: string; 15 | nonce: string; 16 | } 17 | 18 | export interface FullMessageResult { 19 | fullMessage: string; 20 | prefix: string; 21 | } 22 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/network.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface Network { 5 | chainId?: string; 6 | name?: string; 7 | url: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/requests/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './signAndSubmitTransaction'; 5 | export * from './signMessage'; 6 | export * from './signTransaction'; 7 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/requests/signAndSubmitTransaction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import { JsonTransactionPayload } from '../jsonPayload'; 6 | import { TransactionOptions } from '../transactionOptions'; 7 | 8 | export interface SignAndSubmitTransactionWithPayloadRequestArgs { 9 | options?: TransactionOptions; 10 | payload: JsonTransactionPayload | TxnBuilderTypes.TransactionPayload; 11 | } 12 | 13 | export interface SignAndSubmitTransactionWithRawTxnRequestArgs { 14 | rawTxn: TxnBuilderTypes.RawTransaction; 15 | } 16 | 17 | export interface SignAndSubmitTransactionWithFeePayerRawTxnRequestArgs { 18 | feePayerAuthenticator: TxnBuilderTypes.AccountAuthenticator; 19 | rawTxn: TxnBuilderTypes.FeePayerRawTransaction; 20 | } 21 | 22 | export type SignAndSubmitTransactionRequestArgs = 23 | | SignAndSubmitTransactionWithPayloadRequestArgs 24 | | SignAndSubmitTransactionWithRawTxnRequestArgs 25 | | SignAndSubmitTransactionWithFeePayerRawTxnRequestArgs; 26 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/requests/signMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { FullMessageFlags } from '../message'; 5 | 6 | export type SignMessageRequestArgs = FullMessageFlags & { 7 | message: string; 8 | nonce: string; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/requests/signTransaction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | import { JsonTransactionPayload } from '../jsonPayload'; 6 | import { TransactionOptions } from '../transactionOptions'; 7 | 8 | export interface SignTransactionWithPayloadRequestArgs { 9 | options?: TransactionOptions; 10 | payload: JsonTransactionPayload | TxnBuilderTypes.TransactionPayload; 11 | } 12 | 13 | export interface SignTransactionWithRawTxnRequestArgs { 14 | rawTxn: 15 | | TxnBuilderTypes.RawTransaction 16 | | TxnBuilderTypes.FeePayerRawTransaction 17 | | TxnBuilderTypes.MultiAgentRawTransaction; 18 | } 19 | 20 | export type SignTransactionRequestArgs = SignTransactionWithPayloadRequestArgs | SignTransactionWithRawTxnRequestArgs; 21 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/responses/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './signAndSubmitTransaction'; 5 | export * from './signMessage'; 6 | export * from './signTransaction'; 7 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/responses/signAndSubmitTransaction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface SignAndSubmitTransactionResponseArgs { 5 | hash: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/responses/signMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { FullMessageParams, FullMessageResult } from '../message'; 5 | 6 | export type SignMessageResponseArgs = FullMessageParams & 7 | FullMessageResult & { 8 | signature: string; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/responses/signTransaction.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { TxnBuilderTypes } from 'aptos'; 5 | 6 | export interface SignTransactionWithPayloadResponseArgs { 7 | accountAuthenticator: TxnBuilderTypes.AccountAuthenticator; 8 | rawTxn: TxnBuilderTypes.RawTransaction; 9 | } 10 | 11 | export interface SignTransactionWithRawTxnResponseArgs { 12 | accountAuthenticator: TxnBuilderTypes.AccountAuthenticator; 13 | } 14 | 15 | export type SignTransactionResponseArgs = 16 | | SignTransactionWithPayloadResponseArgs 17 | | SignTransactionWithRawTxnResponseArgs; 18 | -------------------------------------------------------------------------------- /packages/wallet-api/src/types/transactionOptions.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface TransactionOptions { 5 | expirationSecondsFromNow?: number; 6 | expirationTimestamp?: number; 7 | gasUnitPrice?: number; 8 | maxGasAmount?: number; 9 | sender?: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/wallet-api/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './makeFullMessage'; 5 | -------------------------------------------------------------------------------- /packages/wallet-api/src/utils/makeFullMessage.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import type { FullMessageFlags, FullMessageParams, FullMessageResult } from '../types'; 5 | 6 | const prefix = 'APTOS'; 7 | 8 | export function makeFullMessage(params: FullMessageParams, flags: FullMessageFlags): FullMessageResult { 9 | let fullMessage = prefix; 10 | if (flags.address) { 11 | fullMessage += `\naddress: ${params.address}`; 12 | } 13 | if (flags.application) { 14 | fullMessage += `\napplication: ${params.application}`; 15 | } 16 | if (flags.chainId) { 17 | fullMessage += `\nchainId: ${params.chainId}`; 18 | } 19 | 20 | fullMessage += `\nmessage: ${params.message}`; 21 | fullMessage += `\nnonce: ${params.nonce}`; 22 | 23 | return { 24 | fullMessage, 25 | prefix, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/wallet-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "src" 9 | ], 10 | "exclude": [ 11 | "src/__tests__" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/wallet-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | { 8 | "path": "./tsconfig.build.json" 9 | } 10 | ], 11 | "include": [ 12 | "src/__tests__", 13 | ".eslintrc.cjs", 14 | "jest.config.ts", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/wallet-sdk/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | '@identity-connect/eslint-config', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/wallet-sdk/DOCS.md: -------------------------------------------------------------------------------- 1 | # Wallet SDK 2 | 3 | The Wallet SDK is intented for Aptos wallet developers that want to allow the user to connect to accounts to IC through their wallet. 4 | 5 | For now, the Wallet SDK is only available for Typescript, but it’ll very soon be available for Rust and to many more languages thanks to native bindings. 6 | 7 | ## Initialization 8 | 9 | The main entrypoint of the Wallet SDK is the `ICWalletClient` class. It provides APIs to create and update a connection to IC, as well as fetching and responding to signature requests. 10 | 11 | Some metadata is required in order to initialize the client 12 | 13 | ```tsx 14 | import { ICWalletClient, WalletInfo } from '@identity-connect/wallet-sdk'; 15 | 16 | const walletInfo: WalletInfo = { 17 | deviceIdentifier: ..., 18 | platform: 'native-app', 19 | platformOS: 'ios', 20 | walletName: 'Petra', 21 | }; 22 | 23 | const icWalletClient = new ICWalletClient(walletInfo); 24 | ``` 25 | 26 | The client needs to persist a state in order to keep track of connections. 27 | It's required to provide a custom implementation of state accessors in the constructor options. 28 | 29 | ```tsx 30 | import { 31 | ICWalletClient, 32 | WalletInfo, 33 | WalletConnectionData, 34 | } from '@identity-connect/wallet-sdk'; 35 | 36 | const walletInfo: WalletInfo = { ... }; 37 | 38 | const inMemoryStorage: { [id: string]: WalletConnectionData } = {}; 39 | const accessors = { 40 | get: (id: string) => inMemoryStorage[id], 41 | getAll: () => ({ ...inMemoryStorage }), 42 | update: (id: string, connection?: WalletConnectionData) => { 43 | inMemoryStorage[id] = connection; 44 | }, 45 | }; 46 | 47 | const icWalletClient = new ICWalletClient(walletInfo, accessors); 48 | ``` 49 | 50 | ## Connect wallet to IC 51 | 52 | Once the Wallet client is initialized, it can be used to accept a connection request. 53 | 54 | The user can start a connection request from the IC dashboard, an the connection request id can be scanned as QR code or copied to the clipboard. 55 | 56 | The wallet is responsible for allowing the user to either scan the QR code or manually paste the request id. 57 | 58 | In order to establish a connection with an account managed by the wallet, the wallet requires it to provide: 59 | 60 | - a proof of ownership 61 | - a transport keypair used for encrypted end-to-end communication 62 | 63 | The following example shows how to obtain them using the wallet SDK 64 | 65 | ```tsx 66 | import { 67 | AccountConnectionAction, 68 | Ed25519PublicKey, 69 | KeyTypes, 70 | createSerializedAccountInfo, 71 | deriveAccountTransportEd25519Keypair, 72 | toKey, 73 | } from '@identity-connect/crypto'; 74 | 75 | // Public key and sign callback for the account we wish to connect 76 | const ed25519PublicKey = toKey(publicKeyBytes, KeyTypes.Ed25519PublicKey) 77 | const signCallback = (buffer: UInt8Array): UInt8Array => { ... }; 78 | 79 | // Derive a deterministic transport keypair from the account's public key 80 | const transportKeys = await deriveAccountTransportEd25519Keypair( 81 | signCallback, 82 | ed25519PublicKey, 83 | ); 84 | 85 | // Request a signed intent to connect the account to IC 86 | const info = await createSerializedAccountInfo( 87 | signCallback, 88 | ed25519PublicKey, 89 | transportKeys.publicKey, 90 | AccountConnectionAction.ADD, 91 | connectionRequestId, 92 | accountAddress, 93 | ); 94 | ``` 95 | 96 | The below utility function, will perform the two actions and pack them together 97 | into a single `WalletAccountConnectInfo` that is the input type of further methods 98 | 99 | ```tsx 100 | import { 101 | AccountConnectionAction, 102 | KeyTypes, 103 | WalletAccountConnectInfo, 104 | toKey, 105 | } from '@identity-connect/crypto'; 106 | import { createWalletAccountConnectInfo } from '@identity-connect/wallet-sdk'; 107 | 108 | const info: WalletAccountConnectInfo = await createWalletAccountConnectInfo( 109 | signCallback, 110 | toKey(publicKeyBytes, KeyTypes.Ed25519PublicKey), 111 | action, 112 | walletId, 113 | ); 114 | ``` 115 | 116 | The wallet should allow the user to choose which accounts to be included in the connection, and for each of them request the signed connection info. 117 | 118 | With that, the connection can be finalized by calling `finalizeConnection` as follows: 119 | 120 | ```tsx 121 | import { WalletAccountConnectInfo } from '@identity-connect/wallet-sdk'; 122 | 123 | const allConnectInfo: WalletAccountConnectInfo[] = ...; 124 | await walletClient.finalizeConnection(connectionRequestId, allConnectInfo); 125 | ``` 126 | 127 | The accounts are now successfully connected and will show up in the account connection prompt when connecting to a dapp. 128 | 129 | ## Fetch pending signature requests 130 | 131 | Once the wallet is connected, you can fetch pending signature request for all the accounts associated with the connection by calling the `getAllSigningRequests` method. 132 | 133 | By checking the `type` property of each request, you can type-infer the body of the request. 134 | 135 | ```tsx 136 | const requests = await icWalletClient.getAllSigningRequests(); 137 | for (const request of requests) { 138 | if (request.type === SigningRequestTypes.SIGN_MESSAGE) { 139 | // request.body will be a SignMessageRequestBody 140 | } else if (request.type === SigningRequestTypes.SIGN_AND_SUBMIT_TRANSACTION) { 141 | // request.body will be a SerializedPayload 142 | } 143 | } 144 | ``` 145 | 146 | ## Respond to a signature request 147 | 148 | In the previous section we saw how to fetch pending requests. Ideally, the wallet would display the requests to the user, and allow them to either approve or reject them. 149 | 150 | The wallet client provides `approveSigningRequest` and `rejectSigningRequest` to do so. 151 | 152 | Below, you can find an example on how to approve a message signature request, and return the message signature. 153 | 154 | ```tsx 155 | const { message, nonce } = request.body; 156 | const application = ... 157 | const chainId = ... 158 | const prefix = ... 159 | 160 | const fullMessage = ... 161 | const fullMessageBytes = new TextEncoder().encode(fullMessage); 162 | 163 | const signatureBytes = await signCallback(fullMessageBytes); 164 | const signature = Buffer.from(signatureBytes).toString('hex'); 165 | await walletClient.approveSigningRequest(request.id, request.pairingId, { 166 | address: request.accountAddress, 167 | application, 168 | chainId, 169 | fullMessage, 170 | message, 171 | nonce, 172 | prefix, 173 | signature, 174 | }); 175 | ``` 176 | 177 | ## Disconnect wallet 178 | 179 | Disconnecting the wallet from IC is done very simply by calling the `removeConnection` method on the wallet client. 180 | 181 | ```tsx 182 | await icWalletClient.removeConnection(id); 183 | ``` 184 | 185 | The call will instruct the server to expire the connection, as well as remove it from the client’s state. 186 | -------------------------------------------------------------------------------- /packages/wallet-sdk/README.md: -------------------------------------------------------------------------------- 1 | # Identity Connect Wallet SDK 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /packages/wallet-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@identity-connect/wallet-sdk", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "require": "./dist/index.js", 11 | "import": "./dist/index.mjs" 12 | }, 13 | "./src/": "./src/" 14 | }, 15 | "scripts": { 16 | "build": "tsup src/index.ts --tsconfig tsconfig.build.json --format cjs,esm --sourcemap --dts", 17 | "lint": "prettier --check src", 18 | "lint:fix": "prettier --write src && eslint --cache ./src --fix" 19 | }, 20 | "dependencies": { 21 | "@identity-connect/api": "^0.4.0", 22 | "@identity-connect/crypto": "^0.1.3", 23 | "@identity-connect/wallet-api": "^0.0.3", 24 | "axios": "^1.4.0" 25 | }, 26 | "devDependencies": { 27 | "@identity-connect/eslint-config": "*", 28 | "@identity-connect/tsconfig": "*", 29 | "@types/jest": "^29.5.1", 30 | "axios-mock-adapter": "^1.21.4", 31 | "jest": "^29.1.0", 32 | "ts-jest": "^29.1.0", 33 | "ts-node": "10.9.1", 34 | "tsup": "^7.1.0", 35 | "typescript": "5.0.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/wallet-sdk/src/__tests__/testUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { WalletConnectionDataMap, WalletStateAccessors } from '../state'; 5 | 6 | export function makeMockWalletState() { 7 | const mockWalletState = { 8 | pairings: {} as WalletConnectionDataMap, 9 | }; 10 | const mockWalletStateAccessors: WalletStateAccessors = { 11 | get: async (address) => mockWalletState.pairings[address], 12 | getAll: async () => mockWalletState.pairings, 13 | update: async (address, pairing) => { 14 | if (pairing === undefined) { 15 | delete mockWalletState.pairings[address]; 16 | } else { 17 | mockWalletState.pairings[address] = pairing; 18 | } 19 | }, 20 | }; 21 | return { mockWalletState, mockWalletStateAccessors }; 22 | } 23 | 24 | export interface WrappedPromise extends Promise { 25 | pending: boolean; 26 | rejected: boolean; 27 | resolved: boolean; 28 | } 29 | 30 | /** 31 | * Utility to detect the state of a promise before awaiting it 32 | * @param promise 33 | */ 34 | export function wrapPromise(promise: Promise) { 35 | const wrapper = promise 36 | .then((result) => { 37 | wrapper.pending = false; 38 | wrapper.resolved = true; 39 | return result; 40 | }) 41 | .catch((err) => { 42 | wrapper.pending = false; 43 | wrapper.rejected = true; 44 | throw err; 45 | }) as WrappedPromise; 46 | wrapper.pending = true; 47 | wrapper.resolved = false; 48 | wrapper.rejected = false; 49 | return wrapper; 50 | } 51 | -------------------------------------------------------------------------------- /packages/wallet-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export * from './client'; 5 | export * from './state'; 6 | export * from './types'; 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /packages/wallet-sdk/src/state.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export interface WalletConnectionAccount { 5 | address: string; 6 | ed25519PublicKeyB64: string; 7 | transportEd25519PublicKeyB64: string; 8 | transportEd25519SecretKeyB64: string; 9 | userSubmittedAlias?: string; 10 | } 11 | 12 | export interface WalletConnectionData { 13 | accounts: { [address: string]: WalletConnectionAccount }; 14 | icEd25519PublicKeyB64: string; 15 | walletEd25519PublicKeyB64: string; 16 | walletEd25519SecretKeyB64: string; 17 | walletId: string; 18 | } 19 | 20 | export type WalletConnectionDataMap = { [walletId: string]: WalletConnectionData }; 21 | 22 | export interface WalletStateAccessors { 23 | get: (walletId: string) => Promise; 24 | getAll: () => Promise; 25 | update: (walletId: string, pairing?: WalletConnectionData) => Promise; 26 | } 27 | -------------------------------------------------------------------------------- /packages/wallet-sdk/src/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { NetworkName, RegisteredDappDataBase, SigningRequestTypes } from '@identity-connect/api'; 5 | import { AccountConnectInfoSerialized, Ed25519SecretKey } from '@identity-connect/crypto'; 6 | import { 7 | SerializedSignAndSubmitTransactionRequestArgs, 8 | SerializedSignTransactionRequestArgs, 9 | SerializedSignTransactionResponseArgs, 10 | SignAndSubmitTransactionResponseArgs, 11 | SignMessageRequestArgs, 12 | SignMessageResponseArgs, 13 | } from '@identity-connect/wallet-api'; 14 | 15 | export interface WalletAccountConnectInfo { 16 | info: AccountConnectInfoSerialized; 17 | transportEd25519SecretKey: Ed25519SecretKey; 18 | } 19 | 20 | export interface BaseSignatureRequest { 21 | accountAddress: string; 22 | apiVersion: string; 23 | createdAt: Date; 24 | id: string; 25 | networkName: NetworkName; 26 | pairingId: string; 27 | registeredDapp: RegisteredDappDataBase; 28 | } 29 | 30 | export interface SignMessageRequest extends BaseSignatureRequest { 31 | args: SignMessageRequestArgs; 32 | type: SigningRequestTypes.SIGN_MESSAGE; 33 | } 34 | 35 | export interface SignTransactionRequest extends BaseSignatureRequest { 36 | args: SerializedSignTransactionRequestArgs; 37 | type: SigningRequestTypes.SIGN_TRANSACTION; 38 | } 39 | 40 | export interface SignAndSubmitTransactionRequest extends BaseSignatureRequest { 41 | args: SerializedSignAndSubmitTransactionRequestArgs; 42 | type: SigningRequestTypes.SIGN_AND_SUBMIT_TRANSACTION; 43 | } 44 | 45 | export type SignatureRequest = SignMessageRequest | SignTransactionRequest | SignAndSubmitTransactionRequest; 46 | export type SerializedSignatureResponseArgs = 47 | | SignMessageResponseArgs 48 | | SerializedSignTransactionResponseArgs 49 | | SignAndSubmitTransactionResponseArgs; 50 | -------------------------------------------------------------------------------- /packages/wallet-sdk/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright © Aptos 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | AccountConnectInfo, 6 | AccountConnectionAction, 7 | createSerializedAccountInfo, 8 | deriveAccountTransportEd25519Keypair, 9 | Ed25519PublicKey, 10 | encodeBase64, 11 | SignCallback, 12 | } from '@identity-connect/crypto'; 13 | import { WalletAccountConnectInfo } from './types'; 14 | 15 | export function walletAccountFromConnectInfo({ info, transportEd25519SecretKey }: WalletAccountConnectInfo) { 16 | const { accountAddress, action, ed25519PublicKeyB64, transportEd25519PublicKeyB64 } = JSON.parse( 17 | info.accountInfoSerialized, 18 | ) as AccountConnectInfo; 19 | const transportEd25519SecretKeyB64 = encodeBase64(transportEd25519SecretKey.key); 20 | return { 21 | account: { 22 | address: accountAddress, 23 | ed25519PublicKeyB64, 24 | transportEd25519PublicKeyB64, 25 | transportEd25519SecretKeyB64, 26 | }, 27 | action, 28 | }; 29 | } 30 | 31 | export async function createWalletAccountConnectInfo( 32 | signCallback: SignCallback, 33 | ed25519PublicKey: Ed25519PublicKey, 34 | action: AccountConnectionAction, 35 | intentId: string, 36 | accountAddress?: string, 37 | ): Promise { 38 | const transportKeys = await deriveAccountTransportEd25519Keypair(signCallback, ed25519PublicKey); 39 | const info = await createSerializedAccountInfo( 40 | signCallback, 41 | ed25519PublicKey, 42 | transportKeys.publicKey, 43 | action, 44 | intentId, 45 | accountAddress, 46 | ); 47 | return { info, transportEd25519SecretKey: transportKeys.secretKey }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/wallet-sdk/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | ], 7 | "rootDir": "src", 8 | "outDir": "dist" 9 | }, 10 | "include": [ 11 | "src" 12 | ], 13 | "exclude": [ 14 | "src/__tests__" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/wallet-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig/base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "jest" 6 | ], 7 | "noEmit": true 8 | }, 9 | "references": [ 10 | { 11 | "path": "./tsconfig.build.json" 12 | } 13 | ], 14 | "include": [ 15 | "src/__tests__", 16 | ".eslintrc.cjs", 17 | "jest.config.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "inputs": [ 9 | "src/**", 10 | "package.json", 11 | "tsconfig.build.json" 12 | ], 13 | "outputs": [ 14 | "dist/**" 15 | ] 16 | }, 17 | "lint": { 18 | "dependsOn": ["^build"] 19 | }, 20 | "lint:fix": { 21 | "dependsOn": ["^build"] 22 | }, 23 | "test": { 24 | "dependsOn": ["build", "^test"], 25 | "outputs": [] 26 | }, 27 | "dev": { 28 | "cache": false, 29 | "persistent": true 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------