├── .gitignore ├── README.md ├── aspect ├── README.md ├── asconfig.json ├── assembly │ ├── aspect │ │ ├── aspect.ts │ │ └── counter-storage.ts │ └── index.ts ├── package.json ├── project.config.json ├── scripts │ ├── aspect-deploy.cjs │ ├── bind.cjs │ ├── contract-call.cjs │ ├── contract-deploy.cjs │ ├── contract-send.cjs │ ├── create-account.cjs │ └── transfer.cjs ├── test_contracts │ └── Counter.sol ├── tests │ ├── integration_test.cjs │ ├── pressure_test.cjs │ ├── test_operation.cjs │ └── test_tx.cjs └── tsconfig.json ├── img ├── 2023-11-09-15.32.38.png ├── 2023-11-09-15.37.26.png ├── 2023-11-09-15.52.20.png ├── 2023-11-09-15.54.18.png └── 2023-11-09-16.16.17.png └── js_client ├── README.md └── session_key_aspect_client ├── README.md ├── index.js ├── package.json ├── src └── aspect_client.js ├── test ├── client │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js ├── demo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── context │ │ └── Web3Modal.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js └── sessionKeyAspectClient.test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Private 3 | privateKey.txt 4 | privateKey*.txt 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | dist 98 | 99 | # Gatsby files 100 | .cache/ 101 | # Comment in the public line in if your project uses Gatsby and not Next.js 102 | # https://nextjs.org/blog/next-9-1#public-directory-support 103 | # public 104 | 105 | # vuepress build output 106 | .vuepress/dist 107 | 108 | # vuepress v2.x temp and cache directory 109 | .temp 110 | .cache 111 | 112 | # Docusaurus cache and generated files 113 | .docusaurus 114 | 115 | # Serverless directories 116 | .serverless/ 117 | 118 | # FuseBox cache 119 | .fusebox/ 120 | 121 | # DynamoDB Local files 122 | .dynamodb/ 123 | 124 | # TernJS port file 125 | .tern-port 126 | 127 | # Stores VSCode versions used for testing VSCode extensions 128 | .vscode-test 129 | 130 | # yarn v2 131 | .yarn/cache 132 | .yarn/unplugged 133 | .yarn/build-state.yml 134 | .yarn/install-state.gz 135 | .pnp.* 136 | .idea 137 | 138 | build/ 139 | package-lock.json 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abstract 2 | 3 | **Sesscion-Key Aspect** allows EoA to extend several sub-keys named **Session Key**. 4 | 5 | Session keys are able to **stand in for** EoA private key to sign a **specific transaction**. These keys will automatically expire at the block height, which is set by EoA. They are also limited to sign specific transactions, calling only specific smart contract methods. 6 | 7 | **With Session-Key Aspect, you can enable following features for your dApp:** 8 | * Enable On-Click-Trading for defi protocol 9 | * Improve UX and wallet security for mini web app (like PWA, bot and TWA in Telegram) 10 | * Use your dApp like web2 products: login once, and click without interacting with the wallet 11 | 12 | # Project Intro 13 | 14 | * Folder [aspect](https://github.com/artela-network/session-key-aspect/blob/main/aspect/README.md) implements the session key Aspect; 15 | * Folder [js_client](https://github.com/artela-network/session-key-aspect/blob/main/js_client/session_key_aspect_client/README.md) implements the session key Aspect javascript client. 16 | 17 | # Integration 18 | * Bind session key Aspect to you contract by this [guide](https://github.com/artela-network/session-key-aspect/blob/main/aspect/README.md) 19 | * Integrate js client to you font-end dApp by this [guide](https://github.com/artela-network/session-key-aspect/blob/main/js_client/session_key_aspect_client/README.md) 20 | 21 | 22 | # Session-key Overview 23 | 24 | ## 1. EoA binding session key to Aspect 25 | 26 | ![截屏2023-11-09 15.37.26.png](https://github.com/artela-network/session-key-aspect/blob/main/img/2023-11-09-15.37.26.png) 27 | 28 | 29 | 30 | Additional info: 31 | 32 | - `Specific contract` is a contract address. For example, if it’s a DEX contract address, it means that the session key is only limited to calling an Artex contract. 33 | 34 | - `Specific methods` is a list of method signature of `Specific contract`. For example, if it’s `[0x0000CAFE, 0xCAFE0000,]` , it means that the session key is only limited to this two method. 35 | 36 | 37 | 38 | ## 2. Use session key to sign transaction 39 | 40 | ![截屏2023-11-09 15.32.38.png](https://github.com/artela-network/session-key-aspect/blob/main/img/2023-11-09-15.32.38.png) 41 | 42 | Additional info : 43 | 44 | - The `from` is still the address of EoA. 45 | 46 | - The signature `v,r,s` is generated by private key of sKey. 47 | 48 | 49 | 50 | ## 3. Artela verify the transaction 51 | 52 | ![截屏2023-11-09 15.52.20.png](https://github.com/artela-network/session-key-aspect/blob/main/img/2023-11-09-15.52.20.png) 53 | 54 | Additional info 55 | 56 | - The transaction may be signed by the EoA privaten point needs to be verified by the EoA public key again if the Aspect returns key, so the joi false. 57 | 58 | 59 | 60 | ## 4. Call contract 61 | 62 | ![截屏2023-11-09 15.54.18.png](https://github.com/artela-network/session-key-aspect/blob/main/img/2023-11-09-15.54.18.png) 63 | 64 | Additional info 65 | 66 | - Smart contract doesn’t know which keys sign the tx. 67 | 68 | - When the smart contract accesses `msg.sender` , the value is `from` in the transaction. 69 | 70 | 71 | 72 | # Implements 73 | 74 | Session-key Aspect project contains three components. 75 | 76 | - **Client**, `sessioin-key-aspect.js`, a client for the dApp front end to use session key. 77 | - **Aspect**, wasm bytecode deployed on Artela 78 | - **Explorer,** extend Artela explorer to show Aspect info 79 | 80 | 81 | ![截屏2023-11-09 16.16.17.png](https://github.com/artela-network/session-key-aspect/blob/main/img/2023-11-09-16.16.17.png) 82 | 83 | 84 | 85 | # Client Details 86 | 87 | `sessioin-key-aspect.js` is the client of Session-key Aspect. 88 | 89 | It contains 3 key modules: 90 | 91 | - session-key store 92 | - signer 93 | - aspect-client 94 | 95 | **session-key store** offers: 96 | 97 | - generate key pair in the front end 98 | 99 | - manage key pair in the browser cache 100 | 101 | ( including store, load, clear, etc.) 102 | 103 | **signer** offers: 104 | 105 | - high-level API for dApp to use the session key 106 | 107 | - load session key from 108 | 109 | 110 | 111 | # What’s the difference between AA 112 | 113 | **It works like AA (abstract account).** For example, we can design a kind of AA that can be signed by a main private key and some session keys. 114 | 115 | **But it will be more flexible than AA.** 116 | 117 | - EoA can not extend to AA. 118 | 119 | If you want to use session-key enable AA, you have to transfer your assets from your EoA to a new AA. This user experience might be terrible in some situations. 120 | 121 | By Session-Key Aspect, an EoA can send a transaction to bind it with Session-Key Aspect, and then the EoA will upgrade to a kind of AA that supports the session key. It needn’t transfer its assets. 122 | 123 | - Users only need to manage one key. 124 | 125 | If you use AA, you might manage several AA wallets with different function. By using Aspect to extend EoA, you just use one wallet with several extension functions. 126 | -------------------------------------------------------------------------------- /aspect/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Brief Intro 4 | 5 | This Aspect will recover the signer address from the transaction signature (r,s,v). 6 | 7 | Then, it verifies whether the signer is the session key of the EoA; if so, it will return the EoA address to the base layer. 8 | 9 | The base layer retrieves the address and sets it as the sender of this transaction. 10 | 11 | 12 | 13 | ## Project Layout 14 | 15 | ```bash 16 | . 17 | ├── README.md 18 | ├── asconfig.json 19 | ├── assembly 20 | │ ├── aspect <-- aspect code resides here 21 | │ │ └── aspect.ts <-- entry functions for the aspect 22 | │ └── index.ts 23 | ├── test_contracts <-- smart contracts for testing 24 | ├── tests <-- test 25 | ├── scripts <-- utilitity scripts, including deploying, binding and etc. 26 | │ ├── aspect-deploy.cjs 27 | │ ├── bind.cjs 28 | │ ├── contract-call.cjs 29 | │ └── contract-deploy.cjs 30 | ... 31 | ``` 32 | 33 | 34 | 35 | ## Quick start 36 | 37 | It's a [node.js](https://nodejs.org/en) project, so install the dependency first. 38 | 39 | ```shell 40 | npm install 41 | ``` 42 | 43 | 44 | 45 | Build the Aspect and the testing smart contract. 46 | 47 | ```shell 48 | npm run build 49 | ``` 50 | 51 | 52 | 53 | Create a testing wallet. 54 | 55 | ```shell 56 | npm run account:create 57 | ``` 58 | 59 | The private key will be put in the file `privateKey.txt`. 60 | 61 | The wallet address will be printed in the console, copy and require testnet **$ART** by this [faucet guide](https://docs.artela.network/develop/resources/faucet). 62 | 63 | > If you want to use your own test net wallet, you can create the file `privateKey.txt` and paste your private key into it. The private key format is a hex string like this `0xabcd....abcd`. 64 | 65 | 66 | 67 | Run the test!! 68 | 69 | ```shell 70 | npm run test 71 | ``` 72 | 73 | The script will run the test in the folder `tests`; you can run the specific case like this command `node tests/test_tx. js`. 74 | 75 | 76 | 77 | ## Implements Intro 78 | 79 | * The function `verifyTx` implements the session key verification logic. 80 | * The function `operation` implements the session key management logic. EoA calls this function to register, update, and delete its session keys. 81 | 82 | 83 | 84 | ## Testnet Session-key Aspect Usage 85 | 86 | #### **Session-key Aspect address (testnet)** 87 | 88 | lastest version: 0x06786bB59719d7DDD9D42457a16BbCD6953A7cab 89 | 90 | 91 | 92 | #### **Requirement** 93 | 94 | The smart contract needs to bind this Aspect to enable the session key feature for its users. 95 | 96 | If a smart contract wants to bind any Aspect, it **MUST** implement the [isOwner(address) ](https://docs.artela.network/develop/core-concepts/binding#contract-ownership-verification) method. Only the owner wallet can sign the binding transaction for its smart contract. 97 | 98 | 99 | 100 | #### **Bind Aspect** 101 | 102 | There are three ways to bind Aspects: 103 | 104 | * Use `Aspect-tool` console command 105 | * Use `artela-web3.js` to call Aspect system contract 106 | * Use online [visual dashboard](https://session-key-aspect.vercel.app/) to bind Session key aspect to your contract 107 | 108 | 109 | #### **Use Aspect-tool** 110 | 111 | In this way, you can bind Aspect by console command. 112 | 113 | Step 1: install [aspect-tool](https://docs.artela.network/develop/reference/aspect-tool/overview) 114 | 115 | ```shell 116 | npm install @artela/aspect-tool -g 117 | ``` 118 | 119 | Step 2: init a empty project 120 | 121 | ```shell 122 | mkdir empty-aspect && cd empty-aspect 123 | aspect-tool init 124 | npm install yargs 125 | ``` 126 | 127 | Step 3: use the script in this project 128 | 129 | ```shell 130 | npm run contract:bind -- --pkfile {smart-contract-owner-privateKey-path} --contract {smart-contract-address} --abi {smart-contract-abi-path} --aspectId '0x40a908F327B22922983061E9E6a87e785d6401BB' --gas '800000' 131 | ``` 132 | 133 | Learn more detailed steps in this [guide](https://docs.artela.network/develop/reference/aspect-tool/bind-aspect). 134 | 135 | 136 | 137 | #### Use artela-web3.js 138 | 139 | In this way, you need to write js script. Follow this [guide](https://docs.artela.network/develop/client/artela-web3.js#web3ethcontractbind) to bind your smart contract to Aspect by using `web3.js` in js. 140 | 141 | Here are brief steps: 142 | 143 | 1. using your `contract address` and `abi` construct a contract object by `web3.eth.contract` 144 | 2. using `web3.eth.contract.bind(options, callback)` to send tx to bind specific Aspect 145 | 146 | Example: 147 | 148 | ```js 149 | await contract.bind({ 150 | priority: 1, // <-- Priority of the aspect, int8 number, smaller number has higher priority. Aspect with higher priority will be executed first. 151 | aspectId: "0xABCD....ABCD", // <-- address of the aspect to bind with the contract, eg. 152 | aspectVersion: 1, // <-- Version of the aspect to bind with the contract 153 | }).send({ from: accounts[0], nonce, ...sendOptions }); 154 | ``` 155 | 156 | Learn more detailed steps in this [guide](https://docs.artela.network/develop/client/artela-web3.js#web3ethcontractbind). 157 | 158 | 159 | #### Use online visual dashboard 160 | Access https://session-key-aspect.vercel.app/ to bind your contract by metamask! 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /aspect/asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "debug": { 4 | "outFile": "build/debug.wasm", 5 | "textFile": "build/debug.wat", 6 | "sourceMap": true, 7 | "debug": true 8 | }, 9 | "release": { 10 | "outFile": "build/release.wasm", 11 | "textFile": "build/release.wat", 12 | "sourceMap": true, 13 | "optimizeLevel": 3, 14 | "shrinkLevel": 0, 15 | "converge": false, 16 | "noAssert": false 17 | } 18 | }, 19 | "options": { 20 | "bindings": "esm" 21 | } 22 | } -------------------------------------------------------------------------------- /aspect/assembly/aspect/aspect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigInt, 3 | BytesData, 4 | hexToUint8Array, 5 | IAspectOperation, 6 | ITransactionVerifier, 7 | OperationInput, 8 | parseCallMethod, 9 | stringToUint8Array, 10 | sys, 11 | TxVerifyInput, 12 | uint8ArrayToHex, 13 | UintData, 14 | } from "@artela/aspect-libs"; 15 | 16 | import {Protobuf} from "as-proto/assembly/Protobuf"; 17 | 18 | /** 19 | * Brief intro: 20 | * 21 | * This Aspect will recover signer address from the transaction signature (r,s,v). 22 | * And then it verify whether the signer is the session key of the EoA, if is, will return the EoA address to base layer. 23 | * Base layer retrieve the address and set it as the sender of this transaction. 24 | * 25 | * Implements: 26 | * 27 | * * Function 'verifyTx': implements the session key verification logic. 28 | * * Function 'operation': implement the session key management logic. EoA call this function to register, update and delete its session keys. 29 | */ 30 | export class Aspect implements IAspectOperation, ITransactionVerifier { 31 | 32 | // *************************************** 33 | // interface methods 34 | // *************************************** 35 | 36 | /** 37 | * Join point: transaction verify 38 | * 39 | * When baselayer process tx's verification, it will call this join point method 40 | * to verify transaction and retrieve the sender address of transaction. 41 | */ 42 | verifyTx(input: TxVerifyInput): Uint8Array { 43 | // validation data encode rules: 44 | // 20 bytes: from 45 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 46 | // 32 bytes: r 47 | // 32 bytes: s 48 | // 1 bytes: v 49 | 50 | // 0. decode validation data 51 | const params = uint8ArrayToHex(input.validationData); 52 | 53 | sys.require(params.length == 170, "illegal validation data, actual: " + params.length.toString()); 54 | const from = params.slice(0, 40); 55 | const r = params.slice(40, 104); 56 | const s = params.slice(104, 168); 57 | const v = params.slice(168, 170); 58 | 59 | // 1. verify sig 60 | const rawUnsignedHash = sys.hostApi.runtimeContext.get("tx.unsigned.hash"); 61 | const unsignedHash = Protobuf.decode(rawUnsignedHash, BytesData.decode); 62 | const msgHash = this.rmPrefix(uint8ArrayToHex(unsignedHash.data)); 63 | 64 | const recoverResult = sys.hostApi.crypto.ecRecover(msgHash, BigInt.fromString(v, 16), BigInt.fromString(r, 16), BigInt.fromString(s, 16)); 65 | const ret = hexToUint8Array(recoverResult); 66 | const sKey = uint8ArrayToHex(ret.subarray(12, 32)); 67 | sys.require(sKey != "", "illegal signature, verify fail"); 68 | 69 | // 2. match session key from Aspect's state 70 | // session keys in state are registered 71 | const encodeSKey = sys.aspect.readonlyState.get( 72 | SessionKey.getStateKey(this.rmPrefix(uint8ArrayToHex(input.tx!.to)), from, sKey) 73 | ).unwrap(); 74 | 75 | sys.require(encodeSKey != "", "illegal session key " + from + "-" + sKey); 76 | 77 | // 2. match session key 78 | const sKeyObj = new SessionKey(encodeSKey); 79 | 80 | // 3. verify session key scope 81 | const method = parseCallMethod(input.callData); 82 | sys.require(sKeyObj.getMethodArray().includes(this.rmPrefix(method)), 83 | "illegal session key scope, method isn't allowed. actual is " + method + ". detail: " + uint8ArrayToHex(input.callData)); 84 | 85 | // 4. verify expire block height 86 | const response = sys.hostApi.runtimeContext.get("block.header.number"); 87 | 88 | var blockNumber = Protobuf.decode(response, UintData.decode).data; 89 | 90 | // const currentBlockHeight = ctx.tx.content.unwrap().blockNumber; 91 | const expireBlockHeight = sKeyObj.getExpireBlockHeight(); 92 | const currentBlockHeight = blockNumber; 93 | sys.require(currentBlockHeight <= expireBlockHeight, 94 | "session key has expired; " + expireBlockHeight.toString() + " < " + currentBlockHeight.toString()); 95 | 96 | // 5. return main key 97 | return hexToUint8Array(from); 98 | } 99 | 100 | /** 101 | * operation is an Aspect call. 102 | * 103 | * @param ctx tx input 104 | * @param data 105 | * @return result of operation execution 106 | */ 107 | operation(input: OperationInput): Uint8Array { 108 | // calldata encode rule 109 | // * 2 bytes: op code 110 | // op codes lists: 111 | // 0x0001 | registerSessionKey 112 | // 113 | // ** 0x10xx means read only op ** 114 | // 0x1001 | getSessionKey 115 | // 0x1002 | verifySessionKeyScope 116 | // 0x1003 | verifySignature 117 | // 0x1004 | ecRecover 118 | // 0x1005 | getAllSessionKey 119 | // 120 | // * variable-length bytes: params 121 | // encode rule of params is defined by each method 122 | 123 | const calldata = uint8ArrayToHex(input.callData); 124 | const op = this.parseOP(calldata); 125 | const params = this.parsePrams(calldata); 126 | 127 | if (op == "0001") { 128 | this.registerSessionKey(this.rmPrefix(uint8ArrayToHex(input.tx!.from)), params); 129 | return new Uint8Array(0); 130 | } 131 | else if (op == "1001") { 132 | const ret = this.getSessionKey(params); 133 | return stringToUint8Array(ret); 134 | } 135 | else if (op == "1002") { 136 | const ret = this.verifySessionKeyScope(params); 137 | return ret ? stringToUint8Array("success") : stringToUint8Array("false"); 138 | } 139 | else if (op == "1003") { 140 | const ret = this.verifySignature(params); 141 | return ret ? stringToUint8Array("success") : stringToUint8Array("false"); 142 | } 143 | else if (op == "1004") { 144 | const ret = this.ecRecover(params); 145 | return stringToUint8Array(ret); 146 | } 147 | else if (op == "1005") { 148 | const ret = this.getAllSessionKey(params); 149 | return stringToUint8Array(ret); 150 | } 151 | else { 152 | sys.revert("unknown op"); 153 | } 154 | return new Uint8Array(0); 155 | } 156 | 157 | // *************************************** 158 | // internal methods 159 | // *************************************** 160 | 161 | parseOP(calldata: string): string { 162 | if (calldata.startsWith('0x')) { 163 | return calldata.substring(2, 6); 164 | } else { 165 | return calldata.substring(0, 4); 166 | } 167 | } 168 | 169 | parsePrams(calldata: string): string { 170 | if (calldata.startsWith('0x')) { 171 | return calldata.substring(6, calldata.length); 172 | } else { 173 | return calldata.substring(4, calldata.length); 174 | } 175 | } 176 | 177 | rmPrefix(data: string): string { 178 | if (data.startsWith('0x')) { 179 | return data.substring(2, data.length); 180 | } else { 181 | return data; 182 | } 183 | } 184 | 185 | registerSessionKey(eoa: string, params: string): void { 186 | /** 187 | * params encode rules: 188 | * 20 bytes: session key 189 | * eg. 1f9090aaE28b8a3dCeaDf281B0F12828e676c326 190 | * 20 bytes: contract address 191 | * eg. 388C818CA8B9251b393131C08a736A67ccB19297 192 | * 2 bytes: length of methods set 193 | * eg. 0002 194 | * variable-length: 4 bytes * length of methods set; methods set 195 | * eg. 0a0a0a0a0b0b0b0b 196 | * means there are two methods: ['0a0a0a0a', '0b0b0b0b'] 197 | * 8 bytes: expire block height 198 | */ 199 | 200 | const encodeKey = params + eoa; 201 | 202 | const sKeyObj = new SessionKey(encodeKey); 203 | sKeyObj.verify(); 204 | 205 | // save key 206 | const stateKey = sKeyObj.getStateKey(); 207 | sys.aspect.mutableState.get(stateKey).set(sKeyObj.getEncodeKey()); 208 | 209 | // save key index 210 | let encodeIndex = sys.aspect.mutableState.get(sKeyObj.getEoA()).unwrap(); 211 | const index = new SessionKeyIndexArray(encodeIndex); 212 | 213 | if (!index.decode().includes(stateKey)) { 214 | index.append(stateKey); 215 | sys.aspect.mutableState.get(sKeyObj.getEoA()).set(index.getEncodeKey()) 216 | } 217 | } 218 | 219 | getSessionKey(params: string): string { 220 | // params encode rules: 221 | // 32 bytes: stroge key of session key 222 | sys.require(params.length == 64, "illegal params"); 223 | return sys.aspect.mutableState.get(params.toLowerCase()).unwrap(); 224 | } 225 | 226 | getAllSessionKey(params: string): string { 227 | 228 | // params encode rules: 229 | // 20 bytes: EoA address 230 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 231 | sys.require(params.length == 40, "illegal params"); 232 | 233 | let encodeIndexObj = sys.aspect.mutableState.get(params.toLowerCase()); 234 | 235 | const index = new SessionKeyIndexArray(encodeIndexObj.unwrap()); 236 | 237 | if (index.getEncodeKey() == "") { 238 | return ""; 239 | } 240 | 241 | let allSessionKey = index.getEncodeKey().slice(0, 4); 242 | let indexArray = index.decode(); 243 | for (let i = 0; i < index.getSize().toInt32(); ++i) { 244 | allSessionKey += this.getSessionKey(indexArray[i]); 245 | } 246 | 247 | return allSessionKey; 248 | } 249 | 250 | verifySessionKeyScope(params: string): bool { 251 | // params encode rules: 252 | // 20 bytes: from 253 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 254 | // 20 bytes: to 255 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 256 | // 4 bytes: method 257 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 258 | // 20 bytes: signer 259 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 260 | 261 | sys.require(params.length == 128, "illegal params"); 262 | const from = params.slice(0, 40); 263 | const to = params.slice(40, 80); 264 | const method = params.slice(80, 88); 265 | const signer = params.slice(88, 128); 266 | 267 | return this.verifySessionKeyScope_(from, to, method, signer); 268 | } 269 | 270 | verifySignature(params: string): bool { 271 | // params encode rules: 272 | // 20 bytes: from 273 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 274 | // 20 bytes: to 275 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 276 | // 32 bytes: hash 277 | // 32 bytes: r 278 | // 32 bytes: s 279 | // 1 bytes: v 280 | sys.require(params.length == 274, "illegal params"); 281 | const from = params.slice(0, 40); 282 | const to = params.slice(40, 80); 283 | const hash = params.slice(80, 144); 284 | const r = params.slice(144, 208); 285 | const s = params.slice(208, 272); 286 | const v = params.slice(272, 274); 287 | 288 | return this.verifySignature_(from, to, hash, r, s, v); 289 | } 290 | 291 | ecRecover(params: string): string { 292 | // params encode rules: 293 | // 20 bytes: from 294 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 295 | // 20 bytes: to 296 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 297 | // 32 bytes: hash 298 | // 32 bytes: r 299 | // 32 bytes: s 300 | // 1 bytes: v 301 | sys.require(params.length == 274, "illegal params"); 302 | const from = params.slice(0, 40); 303 | const to = params.slice(40, 80); 304 | const hash = params.slice(80, 144); 305 | const r = params.slice(144, 208); 306 | const s = params.slice(208, 272); 307 | const v = params.slice(272, 274); 308 | 309 | return this.ecRecover_(hash, r, s, v); 310 | } 311 | 312 | verifySessionKeyScope_(from: string, to: string, method: string, signer: string): bool { 313 | 314 | const stateVal = sys.aspect.mutableState.get(SessionKey.getStateKey(to, from, signer)).unwrap(); 315 | 316 | sys.require(stateVal != '', "session key doesn't exist"); 317 | 318 | const sKey = new SessionKey(stateVal); 319 | return sKey.getMethodArray().includes(method); 320 | } 321 | 322 | verifySignature_(from: string, to: string, hash: string, r: string, s: string, v: string): bool { 323 | 324 | //[msgHash 32B][v 32B][r 32B][s 32B] 325 | const syscallInput = hash 326 | + "00000000000000000000000000000000000000000000000000000000000000" + v 327 | + r 328 | + s; 329 | 330 | const sKey = sys.hostApi.crypto.ecRecover(hash, BigInt.fromString(v, 16), BigInt.fromString(r, 16), BigInt.fromString(s, 16)); 331 | 332 | sys.require(sKey != "", "illegal signature, verify fail"); 333 | 334 | const ret = hexToUint8Array(sKey); 335 | const signer = uint8ArrayToHex(ret.subarray(12, 32)); 336 | if (signer == "") { 337 | return false; 338 | } 339 | 340 | const stateVal = sys.aspect.mutableState.get(SessionKey.getStateKey(to, from, signer)).unwrap(); 341 | 342 | if (stateVal == "") { 343 | return false; 344 | } 345 | 346 | return true; 347 | } 348 | 349 | ecRecover_(hash: string, r: string, s: string, v: string): string { 350 | 351 | //[msgHash 32B][v 32B][r 32B][s 32B] 352 | 353 | return sys.hostApi.crypto.ecRecover(hash, BigInt.fromString(v, 16), BigInt.fromString(r, 16), BigInt.fromString(s, 16)); 354 | 355 | } 356 | 357 | // *********************************** 358 | // base Aspect api 359 | // *********************************** 360 | isOwner(sender: Uint8Array): bool { return true; } 361 | 362 | } 363 | 364 | /** 365 | * SessionKey object 366 | * 1. getEncodeKey() will encode the key and return a hex string, which used in storage 367 | * 2. other methods will decode the key and return fields of the key 368 | * 369 | * SessionKey encode rules: 370 | * 20 bytes: session key public key 371 | * eg. 1f9090aaE28b8a3dCeaDf281B0F12828e676c326 372 | * 20 bytes: contract address 373 | * eg. 388C818CA8B9251b393131C08a736A67ccB19297 374 | * 2 bytes: length of methods set 375 | * eg. 0002 376 | * variable-length: 4 bytes * length of methods set; methods set 377 | * eg. 0a0a0a0a0b0b0b0b 378 | * means there are two methods: ['0a0a0a0a', '0b0b0b0b'] 379 | * 8 bytes: expire block height 380 | * 20 bytes: main key 381 | * eg. 388C818CA8B9251b393131C08a736A67ccB19297 382 | */ 383 | class SessionKey { 384 | private encodeKey: string; 385 | 386 | constructor(encode: string) { 387 | this.encodeKey = encode; 388 | } 389 | 390 | verify(): void { 391 | sys.require(this.encodeKey.length > 84, "illegal encode session key"); 392 | sys.require(this.encodeKey.length == 84 + 8 * this.getMethodCount().toInt32() + 16 + 40, "illegal encode session key"); 393 | } 394 | 395 | getEncodeKey(): string { 396 | return this.encodeKey; 397 | } 398 | 399 | getSKey(): string { 400 | return this.encodeKey.slice(0, 40); 401 | } 402 | 403 | getContractAddress(): string { 404 | return this.encodeKey.slice(40, 80); 405 | } 406 | 407 | getMethodCount(): BigInt { 408 | return BigInt.fromString(this.encodeKey.slice(80, 84), 16);; 409 | } 410 | 411 | getRawMethodsSet(): string { 412 | return this.encodeKey.slice(84, 84 + 8 * this.getMethodCount().toInt32()); 413 | } 414 | 415 | getMethodArray(): Array { 416 | 417 | const array = new Array(); 418 | for (let i = 0; i < this.getMethodCount().toInt32(); ++i) { 419 | array[i] = this.getRawMethodsSet().slice(8 * i, 8 * (i + 1)); 420 | } 421 | return array; 422 | } 423 | 424 | getExpireBlockHeight(): u64 { 425 | let sliceStart = 84 + 8 * this.getMethodCount().toInt32(); 426 | let sliceEnd = sliceStart + 16; 427 | let encodeBlockHeight = this.encodeKey.slice(sliceStart, sliceEnd); 428 | 429 | return BigInt.fromString(encodeBlockHeight, 16).toUInt64(); 430 | } 431 | 432 | getEoA(): string { 433 | return this.encodeKey.slice(84 + 8 * this.getMethodCount().toInt32() + 16, this.encodeKey.length).toLowerCase(); 434 | } 435 | 436 | getStateKey(): string { 437 | return uint8ArrayToHex( 438 | sys.hostApi.crypto.keccak(hexToUint8Array(this.getContractAddress() + this.getEoA() + this.getSKey()))).toLowerCase(); 439 | } 440 | 441 | static getStateKey(contract: string, eoa: string, sKey: string): string { 442 | return uint8ArrayToHex( 443 | sys.hostApi.crypto.keccak(hexToUint8Array(contract + eoa + sKey))).toLowerCase(); 444 | } 445 | } 446 | 447 | class SessionKeyIndexArray { 448 | private encodeKey: string; 449 | 450 | constructor(encode: string) { 451 | this.encodeKey = encode; 452 | } 453 | 454 | getEncodeKey(): string { 455 | return this.encodeKey; 456 | } 457 | 458 | getSize(): BigInt { 459 | return BigInt.fromString(this.encodeKey.slice(0, 4), 16); 460 | } 461 | 462 | append(key: string): void { 463 | if ("" == this.encodeKey) { 464 | this.encodeKey = "0001" + key; 465 | return; 466 | } 467 | 468 | this.encodeKey += key; 469 | let newSize = this.getSize().toInt32() + 1; 470 | this.encodeKey = this.encodeSize(newSize) + this.encodeKey.slice(4); 471 | } 472 | 473 | decode(): Array { 474 | 475 | if ("" == this.encodeKey) { 476 | return []; 477 | } 478 | 479 | const encodeArray = this.encodeKey.slice(4); 480 | const array = new Array(); 481 | for (let i = 0; i < this.getSize().toInt32(); ++i) { 482 | array[i] = encodeArray.slice(64 * i, 64 * (i + 1)); 483 | } 484 | return array; 485 | } 486 | 487 | encode(array: Array): string { 488 | let encodeString = this.encodeSize(array.length); 489 | for (let i = 0; i < array.length; ++i) { 490 | encodeString += this.rmPrefix(array[i]) 491 | } 492 | return encodeString; 493 | } 494 | 495 | encodeSize(size: i32): string { 496 | let sizeHex = size.toString(16); 497 | sizeHex = this.rmPrefix(sizeHex); 498 | sizeHex = sizeHex.padStart(4, "0"); 499 | return sizeHex; 500 | } 501 | 502 | rmPrefix(data: string): string { 503 | if (data.startsWith('0x')) { 504 | return data.substring(2, data.length); 505 | } else { 506 | return data; 507 | } 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /aspect/assembly/aspect/counter-storage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BigInt, 3 | ethereum, 4 | EthStateChange, 5 | State, 6 | StateChange, 7 | StateKey, 8 | StateChangeProperties, 9 | stringToUint8Array, 10 | arrayCopyPush, 11 | uint8ArrayToHex 12 | } from "@artela/aspect-libs"; 13 | 14 | export namespace CounterState { 15 | export class counter extends StateChange { 16 | 17 | constructor(addr: string, indices: Uint8Array[] = []) { 18 | super(new StateChangeProperties(addr, 'Counter.counter', indices)); 19 | } 20 | 21 | override unmarshalState(raw: EthStateChange): State { 22 | let valueHex = uint8ArrayToHex(raw.value); 23 | let value = BigInt.fromString(valueHex, 16); 24 | return new State(uint8ArrayToHex(raw.account), value, raw.callIndex); 25 | } 26 | } 27 | 28 | export class owner extends StateChange { 29 | 30 | constructor(addr: string, indices: Uint8Array[] = []) { 31 | super(new StateChangeProperties(addr, 'Counter.owner', indices)); 32 | } 33 | 34 | override unmarshalState(raw: EthStateChange): State { 35 | return new State(uint8ArrayToHex(raw.account), uint8ArrayToHex(raw.value), raw.callIndex); 36 | } 37 | } 38 | 39 | export class balances_MappingValue extends StateChange { 40 | 41 | constructor(addr: string, indices: Uint8Array[] = []) { 42 | super(new StateChangeProperties(addr, 'Counter.balances', indices)); 43 | } 44 | 45 | override unmarshalState(raw: EthStateChange): State { 46 | let valueHex = uint8ArrayToHex(raw.value); 47 | let value = BigInt.fromString(valueHex, 16); 48 | return new State(uint8ArrayToHex(raw.account), value, raw.callIndex); 49 | } 50 | } 51 | 52 | export class balances extends StateKey { 53 | 54 | constructor(addr: string, indices: Uint8Array[] = []) { 55 | super(new StateChangeProperties(addr, 'Counter.balances', indices)); 56 | } 57 | 58 | @operator("[]") 59 | get(key: string): balances_MappingValue { 60 | // @ts-ignore 61 | return new balances_MappingValue(this.__properties__.account, 62 | arrayCopyPush(this.__properties__.indices, this.parseKey(key))); 63 | } 64 | 65 | protected parseKey(key: string): Uint8Array { 66 | return ethereum.Address.fromHexString(key).encodeUint8Array(); 67 | } 68 | 69 | childrenIndexValue(index: u64): ethereum.Address { 70 | return ethereum.Address.fromUint8Array(this.__children__[index]); 71 | } 72 | 73 | childChangeAt(index: u64): balances_MappingValue { 74 | // @ts-ignore 75 | return new balances_MappingValue(this.__properties__.account, 76 | arrayCopyPush(this.__properties__.indices, this.__children__[index])); 77 | } 78 | } 79 | 80 | export class _balance_ extends StateChange { 81 | 82 | constructor(addr: string, indices: Uint8Array[] = []) { 83 | super(new StateChangeProperties(addr, '.balance', indices)); 84 | } 85 | 86 | override unmarshalState(raw: EthStateChange): State { 87 | let valueHex = uint8ArrayToHex(raw.value); 88 | let value = BigInt.fromString(valueHex, 16); 89 | return new State(uint8ArrayToHex(raw.account), value, raw.callIndex); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /aspect/assembly/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Aspect } from "./aspect/aspect"; 3 | import {allocate, entryPoint, execute} from "@artela/aspect-libs"; 4 | 5 | // 2.register aspect Instance 6 | const aspect = new Aspect() 7 | entryPoint.setAspect(aspect) 8 | entryPoint.setOperationAspect(aspect) 9 | 10 | // 3.must export it 11 | export {execute, allocate} -------------------------------------------------------------------------------- /aspect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "main": "index.js", 4 | "scripts": { 5 | "account:create": "node scripts/create-account.cjs", 6 | "contract:send": "node scripts/contract-send.cjs", 7 | "contract:call": "node scripts/contract-call.cjs", 8 | "aspect:deploy": "npm run aspect:build && node scripts/aspect-deploy.cjs", 9 | "aspect:build": "npm run asbuild:debug && npm run asbuild:release", 10 | "aspect:gen": "aspect-tool generate -i ./build/contract -o ./assembly/aspect", 11 | "asbuild:debug": "asc assembly/index.ts --disable bulk-memory -O0 --debug --runtime stub --exportRuntime --exportStart __aspect_start__ --target debug", 12 | "asbuild:release": "asc assembly/index.ts --disable bulk-memory -O3 --noAssert --runtime stub --exportRuntime --exportStart __aspect_start__ --target release", 13 | "contract:bind": "node scripts/bind.cjs", 14 | "contract:deploy": "node scripts/contract-deploy.cjs", 15 | "contract:build": "solc -o ./build/contract/ --abi --storage-layout --bin ./test_contracts/*.sol --overwrite", 16 | "build": "npm run contract:build && npm run aspect:gen && npm run aspect:build", 17 | "test": "npm run build && node tests/test_operation.cjs && node tests/test_tx.cjs" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@artela/aspect-libs": "^0.0.34", 24 | "@artela/web3": "^1.9.22", 25 | "@assemblyscript/loader": "^0.27.5", 26 | "as-proto": "^1.3.0", 27 | "ethereumjs-tx": "^2.1.2" 28 | }, 29 | "devDependencies": { 30 | "@artela/aspect-tool": "^0.0.53", 31 | "as-proto-gen": "^1.3.0", 32 | "assemblyscript": "^0.27.5", 33 | "yargs": "^17.7.2", 34 | "bignumber.js": "^9.0.1" 35 | }, 36 | "type": "module", 37 | "exports": { 38 | ".": { 39 | "import": "./build/release.js", 40 | "types": "./build/release.d.ts" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /aspect/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "https://betanet-rpc1.artela.network" 3 | } -------------------------------------------------------------------------------- /aspect/scripts/aspect-deploy.cjs: -------------------------------------------------------------------------------- 1 | "use strict" 2 | const Web3 = require("@artela/web3"); 3 | const fs = require("fs"); 4 | const argv = require('yargs') 5 | .string('node') 6 | .string('skfile') 7 | .string('gas') 8 | .string('wasm') 9 | .string('properties') 10 | .array('joinPoints') 11 | .argv; 12 | 13 | async function deploy() { 14 | 15 | const ARTELA_ADDR = "0x0000000000000000000000000000000000A27E14"; 16 | 17 | //---- web3 init--- 18 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 19 | 20 | // init connection to Artela node 21 | let node = (argv.node) ? String(argv.node) : configJson.node; 22 | if (!node) { 23 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 24 | process.exit(0) 25 | } 26 | const web3 = new Web3(node); 27 | let gasPrice = await web3.eth.getGasPrice(); 28 | 29 | 30 | //--skfile ./build/privateKey.txt 31 | let senderPriKey = String(argv.skfile) 32 | if (!senderPriKey || senderPriKey === 'undefined') { 33 | senderPriKey = "privateKey.txt" 34 | } 35 | if (!fs.existsSync(senderPriKey)) { 36 | console.log("'account' cannot be empty, please set by the parameter ' --skfile ./build/privateKey.txt'") 37 | process.exit(0) 38 | } 39 | let pk = fs.readFileSync(senderPriKey, 'utf-8'); 40 | let sender = web3.eth.accounts.privateKeyToAccount(pk.trim()); 41 | console.log("from address: ", sender.address); 42 | web3.eth.accounts.wallet.add(sender.privateKey); 43 | 44 | 45 | const propertiesJson = argv.properties 46 | let properties = [] 47 | if (propertiesJson && propertiesJson !== 'undefined') { 48 | properties = JSON.parse(propertiesJson); 49 | } 50 | const joinPointsJson = argv.joinPoints 51 | let joinPoints = [] 52 | if (joinPointsJson && joinPointsJson !== 'undefined') { 53 | joinPoints =joinPointsJson 54 | } 55 | 56 | //read wasm code 57 | let aspectCode = ""; 58 | // --wasm ./build/release.wasm 59 | let wasmPath = String(argv.wasm) 60 | if (!wasmPath || wasmPath === 'undefined') { 61 | aspectCode = fs.readFileSync('./build/release.wasm', {encoding: "hex"}); 62 | } else { 63 | aspectCode = fs.readFileSync(wasmPath, {encoding: "hex"}); 64 | } 65 | if (!aspectCode || aspectCode === "" || aspectCode === 'undefined') { 66 | console.log("aspectCode cannot be empty") 67 | process.exit(0) 68 | } 69 | 70 | // to deploy aspect 71 | let aspect = new web3.atl.Aspect(); 72 | let deploy = await aspect.deploy({ 73 | data: '0x' + aspectCode, 74 | properties, 75 | joinPoints, 76 | paymaster: sender.address, 77 | proof: '0x0', 78 | }); 79 | 80 | let tx = { 81 | from: sender.address, 82 | data: deploy.encodeABI(), 83 | to: ARTELA_ADDR, 84 | gasPrice, 85 | gas: !parseInt(argv.gas) | 9000000 86 | } 87 | let signedTx = await web3.atl.accounts.signTransaction(tx, sender.privateKey); 88 | console.log("sending signed transaction..."); 89 | let ret = await web3.atl.sendSignedTransaction(signedTx.rawTransaction) 90 | .on('receipt', receipt => { 91 | console.log(receipt); 92 | }); 93 | let aspectID = ret.aspectAddress; 94 | console.log("ret: ", ret); 95 | console.log("== deploy aspectID ==", aspectID) 96 | } 97 | 98 | deploy().then(); 99 | -------------------------------------------------------------------------------- /aspect/scripts/bind.cjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | const Web3 = require("@artela/web3"); 5 | const fs = require("fs"); 6 | var argv = require('yargs') 7 | .string('node') 8 | .string('pkfile') 9 | .string('contract') 10 | .string('aspectId') 11 | .string('gas') 12 | .string('abi') 13 | .argv; 14 | 15 | async function bind() { 16 | // init connection to Artela node 17 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 18 | const ASPECT_ADDR = "0x0000000000000000000000000000000000A27E14"; 19 | 20 | let node = (argv.node) ? String(argv.node) : configJson.node; 21 | if (!node) { 22 | console.log("'node' cannot be empty, please set by the parameter or project.config.json") 23 | process.exit(0) 24 | } 25 | const web3 = new Web3(node); 26 | let gasPrice = await web3.eth.getGasPrice(); 27 | 28 | 29 | //--pkfile ./build/privateKey.txt 30 | let senderPriKey = String(argv.pkfile) 31 | if (!senderPriKey || senderPriKey === 'undefined') { 32 | senderPriKey = "privateKey.txt" 33 | } 34 | if (!fs.existsSync(senderPriKey)) { 35 | console.log("'account' cannot be empty, please set by the parameter ' --pkfile ./build/privateKey.txt'") 36 | process.exit(0) 37 | } 38 | let pk = fs.readFileSync(senderPriKey, 'utf-8'); 39 | let sender = web3.eth.accounts.privateKeyToAccount(pk.trim()); 40 | web3.eth.accounts.wallet.add(sender.privateKey); 41 | 42 | 43 | // --contract {smart-contract-address} 44 | let contractAddress = String(argv.contract) 45 | if (!contractAddress || contractAddress === 'undefined') { 46 | console.log("'contractAddress' cannot be empty, please set by the parameter ' --contract 0xxxx'") 47 | process.exit(0) 48 | } 49 | 50 | // --aspectId {aspect-Id} 51 | let aspectId = String(argv.aspectId) 52 | if (!aspectId || aspectId === 'undefined') { 53 | console.log("'aspectId' cannot be empty, please set by the parameter' --aspectId 0xxxx'") 54 | process.exit(0) 55 | } 56 | 57 | // --abi xxx/xxx.abi 58 | const abiPath = String(argv.abi) 59 | let abi = null 60 | if (abiPath && abiPath !== 'undefined') { 61 | abi = JSON.parse(fs.readFileSync(abiPath, "utf-8").toString()); 62 | } else { 63 | console.log("'abi' cannot be empty, please set by the parameter' --abi xxx/xxx.abi'") 64 | process.exit(0) 65 | } 66 | 67 | // do aspect bind 68 | let storageInstance = new web3.eth.Contract(abi, contractAddress); 69 | // bind the smart contract with aspect 70 | let bind = await storageInstance.bind({ 71 | priority: 1, 72 | aspectId: aspectId, 73 | aspectVersion: 1, 74 | }) 75 | 76 | let tx = { 77 | from: sender.address, 78 | data: bind.encodeABI(), 79 | gasPrice, 80 | to: ASPECT_ADDR, 81 | gas: !parseInt(argv.gas) | 9000000 82 | } 83 | 84 | let signedTx = await web3.eth.accounts.signTransaction(tx, sender.privateKey); 85 | console.log("sending signed transaction..."); 86 | await web3.eth.sendSignedTransaction(signedTx.rawTransaction) 87 | .on('receipt', receipt => { 88 | console.log(receipt); 89 | }); 90 | console.log("== aspect bind success =="); 91 | 92 | } 93 | 94 | bind().then(); 95 | -------------------------------------------------------------------------------- /aspect/scripts/contract-call.cjs: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | 4 | // import required libs 5 | const fs = require('fs'); 6 | const Web3 = require('@artela/web3'); 7 | var argv = require('yargs') 8 | .string('node') 9 | .string('pkfile') 10 | .string('args') 11 | .string('contract') 12 | .string('method') 13 | .string('abi') 14 | .argv; 15 | 16 | 17 | async function call() { 18 | // init connection to Artela node 19 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 20 | let node = (argv.node)?String(argv.node):configJson.node; 21 | if(!node){ 22 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 23 | process.exit(0) 24 | } 25 | const web3 = new Web3(node); 26 | 27 | //--pkfile ./build/privateKey.txt 28 | let senderPriKey = String(argv.pkfile) 29 | if (!senderPriKey || senderPriKey === 'undefined') { 30 | senderPriKey = "privateKey.txt" 31 | } 32 | if (!fs.existsSync(senderPriKey)) { 33 | console.log("'account' cannot be empty, please set by the parameter ' --pkfile ./build/privateKey.txt'") 34 | process.exit(0) 35 | } 36 | let pk = fs.readFileSync(senderPriKey, 'utf-8'); 37 | let sender = web3.eth.accounts.privateKeyToAccount(pk.trim()); 38 | console.log("from address: ", sender.address); 39 | web3.eth.accounts.wallet.add(sender.privateKey); 40 | 41 | 42 | // --contract 0x9999999999999999999999999999999999999999 43 | const contractAddr = argv.contract; 44 | if(!contractAddr){ 45 | console.log("'contract address' cannot be empty, please set by the parameter ' --contract 0x9999999999999999999999999999999999999999'") 46 | process.exit(0) 47 | } 48 | 49 | // --abi xxx/xxx.abi 50 | const abiPath = String(argv.abi) 51 | let abi = null 52 | if (abiPath && abiPath !== 'undefined') { 53 | abi = JSON.parse(fs.readFileSync(abiPath, "utf-8").toString()); 54 | } else { 55 | console.log("'abi' cannot be empty, please set by the parameter' --abi xxx/xxx.abi'") 56 | process.exit(0) 57 | } 58 | // --args [55] 59 | const inputs = argv.args; 60 | let parameters=[]; 61 | if(inputs && inputs!=='undefined') { 62 | parameters = JSON.parse(inputs); 63 | } 64 | //--method count 65 | const method = argv.method; 66 | if(!method || method==='undefined') { 67 | console.log("'method' cannot be empty, please set by the parameter ' --method {method-name}'") 68 | process.exit(0) 69 | } 70 | 71 | let storageInstance = new web3.eth.Contract(abi, contractAddr); 72 | let instance = await storageInstance.methods[method](...parameters).call(); 73 | console.log("==== reuslt===" + instance); 74 | } 75 | 76 | call().then(); 77 | -------------------------------------------------------------------------------- /aspect/scripts/contract-deploy.cjs: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | 4 | // import required libs 5 | const fs = require('fs'); 6 | const Web3 = require("@artela/web3"); 7 | var argv = require('yargs') 8 | .string('node') 9 | .string('pkfile') 10 | .string('bytecode') 11 | .string('abi') 12 | .string('gas') 13 | .string('args') 14 | .argv; 15 | 16 | 17 | async function deploy() { 18 | 19 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 20 | // init connection to Artela node 21 | let node = (argv.node) ? String(argv.node) : configJson.node; 22 | if (!node) { 23 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 24 | process.exit(0) 25 | } 26 | const web3 = new Web3(node); 27 | 28 | const deployParams = { 29 | data: null, 30 | arguments: null, 31 | } 32 | // get bytecode by path --bytecode ./build/contract/xxx.bin 33 | let bytecodePath = String(argv.bytecode) 34 | let byteTxt = "" 35 | if (!bytecodePath) { 36 | console.log("'bytecode' cannot be empty, please set by the parameter ' --bytecode ./build/contract/xxx.bin'") 37 | process.exit(0) 38 | } else { 39 | byteTxt = fs.readFileSync(bytecodePath, "utf-8").toString().trim(); 40 | if (!byteTxt) { 41 | console.log("bytecode cannot be empty") 42 | process.exit(0) 43 | } 44 | if (byteTxt.startsWith("0x")) { 45 | byteTxt = byteTxt.slice(2); 46 | } 47 | deployParams.data = byteTxt.trim() 48 | } 49 | // --args [55] 50 | const inputs = argv.args; 51 | if (inputs && inputs !== 'undefined') { 52 | deployParams.arguments = JSON.parse(inputs) 53 | } 54 | 55 | //--abi ./build/contract/xxx.abi 56 | let abiPath = String(argv.abi) 57 | let abiTxt = "" 58 | if (!abiPath) { 59 | console.log("'abi' cannot be empty, please set by the parameter ' --abi ./build/contract/xxx.abi'") 60 | process.exit(0) 61 | } else { 62 | abiTxt = fs.readFileSync(abiPath, "utf-8").toString().trim(); 63 | if (!abiTxt) { 64 | console.log("'abi' json cannot be empty") 65 | process.exit(0) 66 | } 67 | } 68 | 69 | //--pkfile ./build/privateKey.txt 70 | let senderPriKey = String(argv.pkfile) 71 | if (!senderPriKey || senderPriKey === 'undefined') { 72 | senderPriKey = "privateKey.txt" 73 | } 74 | if (!fs.existsSync(senderPriKey)) { 75 | console.log("'account' cannot be empty, please set by the parameter ' --pkfile ./build/privateKey.txt'") 76 | process.exit(0) 77 | } 78 | let pk = fs.readFileSync(senderPriKey, 'utf-8'); 79 | let account = web3.eth.accounts.privateKeyToAccount(pk.trim()); 80 | console.log("from address: ", account.address); 81 | web3.eth.accounts.wallet.add(account.privateKey); 82 | 83 | 84 | // deploy demo contract 85 | let contractAddress; 86 | { 87 | const contractAbi = JSON.parse(abiTxt); 88 | 89 | // instantiate an instance of demo contract 90 | let tokenContract = new web3.eth.Contract(contractAbi); 91 | 92 | // deploy token contract 93 | let tokenDeploy = tokenContract.deploy(deployParams); 94 | let nonceVal = await web3.eth.getTransactionCount(account.address); 95 | 96 | let tokenTx = { 97 | from: account.address, 98 | data: tokenDeploy.encodeABI(), 99 | nonce: nonceVal, 100 | gas: !parseInt(argv.gas) | 7000000 101 | } 102 | 103 | 104 | let signedTokenTx = await web3.eth.accounts.signTransaction(tokenTx, account.privateKey); 105 | console.log('deploy contract tx hash: ' + signedTokenTx.transactionHash); 106 | await web3.eth.sendSignedTransaction(signedTokenTx.rawTransaction) 107 | .on('receipt', receipt => { 108 | console.log(receipt); 109 | console.log("contract address: ", receipt.contractAddress); 110 | contractAddress = receipt.contractAddress; 111 | }); 112 | } 113 | console.log(`--contractAccount ${account.address} --contractAddress ${contractAddress}`); 114 | 115 | } 116 | 117 | deploy().then(); 118 | 119 | -------------------------------------------------------------------------------- /aspect/scripts/contract-send.cjs: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | 4 | // import required libs 5 | const fs = require('fs'); 6 | const Web3 = require('@artela/web3'); 7 | var argv = require('yargs') 8 | .string('node') 9 | .string('pkfile') 10 | .string('args') 11 | .string('contract') 12 | .string('gas') 13 | .string('method') 14 | .string('abi') 15 | .argv; 16 | 17 | 18 | async function send() { 19 | // init connection to Artela node 20 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 21 | // init connection to Artela node 22 | let node = (argv.node)?String(argv.node):configJson.node; 23 | if(!node){ 24 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 25 | process.exit(0) 26 | } 27 | const web3 = new Web3(node); 28 | let gasPrice = await web3.eth.getGasPrice(); 29 | 30 | //--pkfile ./build/privateKey.txt 31 | let senderPriKey = String(argv.pkfile) 32 | if (!senderPriKey || senderPriKey === 'undefined') { 33 | senderPriKey = "privateKey.txt" 34 | } 35 | if (!fs.existsSync(senderPriKey)) { 36 | console.log("'account' cannot be empty, please set by the parameter ' --pkfile ./build/privateKey.txt'") 37 | process.exit(0) 38 | } 39 | let pk = fs.readFileSync(senderPriKey, 'utf-8'); 40 | let sender = web3.eth.accounts.privateKeyToAccount(pk.trim()); 41 | console.log("from address: ", sender.address); 42 | web3.eth.accounts.wallet.add(sender.privateKey); 43 | 44 | 45 | // --contract 0x9999999999999999999999999999999999999999 46 | const contractAddr = argv.contract; 47 | if(!contractAddr){ 48 | console.log("'contract address' cannot be empty, please set by the parameter ' --contract 0x9999999999999999999999999999999999999999'") 49 | process.exit(0) 50 | } 51 | 52 | // --abi xxx/xxx.abi 53 | const abiPath = String(argv.abi) 54 | let abi = null 55 | if (abiPath && abiPath !== 'undefined') { 56 | abi = JSON.parse(fs.readFileSync(abiPath, "utf-8").toString()); 57 | } else { 58 | console.log("'abi' cannot be empty, please set by the parameter' --abi xxx/xxx.abi'") 59 | process.exit(0) 60 | } 61 | // --args [55] 62 | const inputs = argv.args; 63 | let parameters=[]; 64 | if(inputs && inputs!=='undefined') { 65 | parameters = JSON.parse(inputs); 66 | } 67 | //--method count 68 | const method = argv.method; 69 | if(!method || method==='undefined') { 70 | console.log("'method' cannot be empty, please set by the parameter ' --method {method-name}'") 71 | process.exit(0) 72 | } 73 | 74 | let storageInstance = new web3.eth.Contract(abi, contractAddr); 75 | let instance = storageInstance.methods[method](...parameters); 76 | 77 | let tx = { 78 | from: sender.address, 79 | to: contractAddr, 80 | data: instance.encodeABI(), 81 | gasPrice, 82 | gas: !parseInt(argv.gas) | 4000000 83 | } 84 | let signedTx = await web3.eth.accounts.signTransaction(tx, sender.privateKey); 85 | console.log('call contract tx hash: ' + signedTx.transactionHash); 86 | 87 | await web3.eth.sendSignedTransaction(signedTx.rawTransaction) 88 | .on('receipt', receipt => { 89 | console.log(receipt); 90 | }); 91 | } 92 | send().then(); 93 | -------------------------------------------------------------------------------- /aspect/scripts/create-account.cjs: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | 4 | // import required libs 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const Web3 = require("@artela/web3"); 8 | var argv = require('yargs') 9 | .string('pkfile') 10 | .argv; 11 | 12 | 13 | async function create() { 14 | 15 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 16 | // init connection to Artela node 17 | let node = (argv.node)?String(argv.node):configJson.node; 18 | if(!node){ 19 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 20 | process.exit(0) 21 | } 22 | const web3 = new Web3(node); 23 | 24 | let privateFile = 'privateKey.txt'; // <-- your private key here, if not exist, create new one 25 | if(argv.pkfile){ 26 | privateFile=argv.pkfile; 27 | } 28 | let account; 29 | if (fs.existsSync(privateFile)) { 30 | let pk = fs.readFileSync(privateFile, 'utf-8'); 31 | account = web3.atl.accounts.privateKeyToAccount(pk.trim()); 32 | } else { 33 | account = web3.atl.accounts.create(); 34 | const dirPath = path.dirname(privateFile); 35 | if (!fs.existsSync(dirPath)) { 36 | fs.mkdirSync(dirPath, { recursive: true }); 37 | } 38 | fs.writeFileSync(privateFile, account.privateKey); 39 | } 40 | 41 | // add account to wallet 42 | web3.atl.accounts.wallet.add(account.privateKey); 43 | console.log("address: ", account.address); 44 | } 45 | 46 | create().then(); 47 | -------------------------------------------------------------------------------- /aspect/scripts/transfer.cjs: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // import required libs 4 | const fs = require('fs'); 5 | const Web3 = require("@artela/web3"); 6 | var argv = require('yargs') 7 | .string('skfile') 8 | .string('from') 9 | .argv; 10 | 11 | 12 | async function f() { 13 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 14 | // init connection to Artela node 15 | let node = (argv.node) ? String(argv.node) : configJson.node; 16 | if (!node) { 17 | console.log("'node' cannot be empty, please set by the parameter or artela.config.json") 18 | process.exit(0) 19 | } 20 | console.log("node: " + node) 21 | 22 | const web3 = new Web3(node); 23 | 24 | let privateFile = String(argv.skfile) 25 | if (!privateFile || privateFile === 'undefined') { 26 | privateFile = "privateKey.txt" 27 | } 28 | console.log("privateKey :file " + privateFile) 29 | let account; 30 | if (fs.existsSync(privateFile)) { 31 | let pk = fs.readFileSync(privateFile, 'utf-8'); 32 | account = web3.eth.accounts.privateKeyToAccount(pk.trim()); 33 | } else { 34 | console.log("invalid private key") 35 | process.exit(0) 36 | } 37 | // add account to wallet 38 | web3.atl.accounts.wallet.add(account.privateKey); 39 | console.log("receiver address: ", account.address); 40 | 41 | const receiver = account.address; 42 | 43 | let accounts = await web3.eth.getAccounts(); 44 | let senderAddr = accounts[0] 45 | 46 | let from = String(argv.from) 47 | if (from && from!=="undefined") { 48 | let sender = web3.eth.accounts.privateKeyToAccount(from); 49 | web3.atl.accounts.wallet.add(sender.privateKey); 50 | senderAddr = sender.address 51 | } 52 | 53 | // retrieve current nonce 54 | const balance = await web3.eth.getBalance(senderAddr); 55 | console.log('sender: '+senderAddr+' ' + balance); 56 | 57 | 58 | // transfer account from bank to local account 59 | // the params of getTransactionCount is bank address. 60 | let bankNonce = await web3.atl.getTransactionCount(senderAddr); 61 | let tx1 = { 62 | 'from': senderAddr, 63 | 'to': receiver, 64 | 'value': web3.utils.toWei('100', 'ether'), // transfer 1 eth 65 | 'gas': 2000000, 66 | 'gaslimit': 4000000, 67 | 'nonce': bankNonce 68 | }; 69 | // send transaction 70 | await web3.atl.sendTransaction(tx1).on('receipt', receipt => { 71 | console.log('transferred from bank to local account'); 72 | console.log(receipt); 73 | }); 74 | 75 | // retrieve current nonce 76 | const receiverBalance = await web3.eth.getBalance(receiver); 77 | console.log('===receiver balance:' + receiverBalance); 78 | 79 | } 80 | 81 | f().then(); 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /aspect/test_contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity >=0.8.2 <0.9.0; 3 | 4 | contract Counter { 5 | uint256 private counter; 6 | address private owner; 7 | 8 | constructor() { 9 | owner = msg.sender; 10 | } 11 | function isOwner(address user) external view returns (bool result) { 12 | return user == owner; 13 | } 14 | function add(uint256 number) public { 15 | counter = counter + number; 16 | } 17 | function get() external view returns (uint256 result) { 18 | return counter; 19 | } 20 | 21 | // Vault Contract 22 | // 存储每个地址的余额 23 | mapping(address => uint) public balances; 24 | 25 | // 充值事件 26 | event Deposit(address indexed sender, uint amount); 27 | 28 | // 取现事件 29 | event Withdraw(address indexed receiver, uint amount); 30 | 31 | // 充值函数 32 | function deposit() public payable { 33 | require(msg.value > 0, "Deposit amount must be greater than 0"); 34 | balances[msg.sender] += msg.value; 35 | emit Deposit(msg.sender, msg.value); 36 | } 37 | 38 | // 取现函数 39 | function withdraw(uint amount) public { 40 | require(balances[msg.sender] >= amount, "Insufficient balance"); 41 | balances[msg.sender] -= amount; 42 | payable(msg.sender).transfer(amount); 43 | emit Withdraw(msg.sender, amount); 44 | } 45 | 46 | // 查询余额 47 | function getBalance() public view returns (uint) { 48 | return balances[msg.sender]; 49 | } 50 | 51 | // 查询余额 52 | function checkBalance(address acc) public view returns (uint) { 53 | return balances[acc]; 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /aspect/tests/pressure_test.cjs: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // imports 4 | const Web3 = require('@artela/web3'); 5 | const fs = require("fs"); 6 | const {numberToHex} = require("@artela/web3-utils"); 7 | const BigNumber = require('bignumber.js'); 8 | const assert = require("assert"); 9 | const EventEmitter = require('events'); 10 | 11 | const contractBin = fs.readFileSync('./build/contract/Counter.bin', "utf-8"); 12 | const abi = fs.readFileSync('./build/contract/Counter.abi', "utf-8") 13 | const contractABI = JSON.parse(abi); 14 | const EthereumTx = require('ethereumjs-tx').Transaction; 15 | let aspectCode = fs.readFileSync('./build/release.wasm', { 16 | encoding: "hex" 17 | }); 18 | 19 | // preparation 20 | 21 | // ****************************************** 22 | // init web3 and private key 23 | // ****************************************** 24 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 25 | const web3 = new Web3(configJson.node); 26 | 27 | let sk = fs.readFileSync("privateKey.txt", 'utf-8'); 28 | const mainAccount = web3.eth.accounts.privateKeyToAccount(sk.trim()); 29 | web3.eth.accounts.wallet.add(mainAccount.privateKey); 30 | 31 | // ****************************************** 32 | // load some basic info 33 | // ****************************************** 34 | let gasPrice, chainId; 35 | 36 | // ****************************************** 37 | // init aspect client 38 | // ****************************************** 39 | // instantiate an instance of the aspect core contract 40 | let aspectCore = web3.atl.aspectCore(); 41 | 42 | // ****************************************** 43 | // test data 44 | // ****************************************** 45 | let mainKey = rmPrefix(mainAccount.address); 46 | 47 | // ****************************************** 48 | // prepare 1. deploy contract 49 | // ****************************************** 50 | async function multiEoAWithMultiSessionKey() { 51 | let nonce = await web3.eth.getTransactionCount(mainAccount.address); 52 | 53 | let contract = await deployContract(nonce++); 54 | let aspect = await deployAspect(nonce++); 55 | 56 | await bindAspect(aspect, contract, nonce++); 57 | 58 | const eoaAccountNum = 10000; 59 | const skAccountNum = 10000; 60 | 61 | console.log("[generate EoA accounts]", eoaAccountNum) 62 | let accounts = await generateAccounts(eoaAccountNum, "0.01", nonce); 63 | console.log("[generate EoA accounts]", "done") 64 | 65 | console.log("[bind EoA with Aspect]", eoaAccountNum) 66 | let txsToSend = []; 67 | for (let i = 0; i < accounts.length; i++) { 68 | txsToSend.push(await generateBindEoATx(aspect, accounts[i])); 69 | } 70 | await sendTransactionsAtFixedRate(txsToSend, 4) 71 | console.log("[bind EoA with Aspect]", "done") 72 | 73 | console.log("[generate session key accounts]", skAccountNum) 74 | nonce = await web3.eth.getTransactionCount(mainAccount.address); 75 | let skAccounts = await generateAccounts(skAccountNum, 0, nonce); 76 | console.log("[generate session key accounts]", "done") 77 | 78 | let contractCallData = contract.methods.add([1]).encodeABI(); 79 | let contractCallMethod = rmPrefix(contractCallData).substring(0, 8); 80 | 81 | let currentBlockHeight = await web3.eth.getBlockNumber(); 82 | let expireBlockHeight = currentBlockHeight + 10000000; 83 | 84 | console.log("[register session key]", skAccountNum) 85 | txsToSend = []; 86 | for (let i = 0; i < skAccounts.length; i++) { 87 | const eoaNonce = await web3.eth.getTransactionCount(accounts[i].address); 88 | txsToSend.push(await generateRegisterSessionKeyTx(aspect, accounts[i], eoaNonce, skAccounts[i], contract, contractCallMethod, expireBlockHeight)); 89 | } 90 | await sendTransactionsAtFixedRate(txsToSend, 4) 91 | console.log("[register session key]", "done") 92 | 93 | console.log("[send session key tx]", skAccountNum) 94 | txsToSend = []; 95 | let nonces = {}; 96 | for (let i = 0; i < skAccounts.length; i++) { 97 | if (!nonces[accounts[i].address]) { 98 | nonces[accounts[i].address] = await web3.eth.getTransactionCount(accounts[i].address); 99 | } 100 | txsToSend.push(await generateSessionKeyTx(skAccounts[i], accounts[i], nonces[accounts[i].address]++, contract, contractCallData, false)); 101 | } 102 | await sendTransactionsAtFixedRate(txsToSend, 4) 103 | console.log("[send session key tx]", "done") 104 | } 105 | 106 | async function oneEoAWithMultiSessionKey() { 107 | let nonce = await web3.eth.getTransactionCount(mainAccount.address); 108 | 109 | let contract = await deployContract(nonce++); 110 | let aspect = await deployAspect(nonce++); 111 | 112 | await bindAspect(aspect, contract, nonce++); 113 | 114 | const eoaAccountNum = 1; 115 | const skAccountNum = 10000; 116 | 117 | console.log("[generate EoA accounts]", eoaAccountNum) 118 | let accounts = await generateAccounts(eoaAccountNum, "0.01", nonce); 119 | console.log("[generate EoA accounts]", "done") 120 | 121 | console.log("[bind EoA with Aspect]", eoaAccountNum) 122 | let txsToSend = []; 123 | for (let i = 0; i < accounts.length; i++) { 124 | txsToSend.push(await generateBindEoATx(aspect, accounts[i])); 125 | } 126 | await sendTransactionsAtFixedRate(txsToSend, 4) 127 | console.log("[bind EoA with Aspect]", "done") 128 | 129 | console.log("[generate session key accounts]", skAccountNum) 130 | nonce = await web3.eth.getTransactionCount(mainAccount.address); 131 | let skAccounts = await generateAccounts(skAccountNum, 0, nonce); 132 | console.log("[generate session key accounts]", "done") 133 | 134 | let contractCallData = contract.methods.add([1]).encodeABI(); 135 | let contractCallMethod = rmPrefix(contractCallData).substring(0, 8); 136 | 137 | let currentBlockHeight = await web3.eth.getBlockNumber(); 138 | let expireBlockHeight = currentBlockHeight + 10000000; 139 | 140 | console.log("[register session key]", skAccountNum) 141 | txsToSend = []; 142 | let eoaNonce = await web3.eth.getTransactionCount(accounts[0].address); 143 | for (let i = 0; i < skAccounts.length; i++) { 144 | txsToSend.push(await generateRegisterSessionKeyTx(aspect, accounts[0], eoaNonce++, skAccounts[i], contract, contractCallMethod, expireBlockHeight)); 145 | } 146 | await sendTransactionsAtFixedRate(txsToSend, 4) 147 | console.log("[register session key]", "done") 148 | 149 | console.log("[send session key tx]", skAccountNum) 150 | txsToSend = []; 151 | eoaNonce = await web3.eth.getTransactionCount(accounts[0].address); 152 | for (let i = 0; i < skAccounts.length; i++) { 153 | txsToSend.push(await generateSessionKeyTx(skAccounts[i], accounts[0], eoaNonce++, contract, contractCallData, false)); 154 | } 155 | await sendTransactionsAtFixedRate(txsToSend, 4) 156 | console.log("[send session key tx]", "done") 157 | } 158 | 159 | async function oneEoAWithOneSessionKey() { 160 | let nonce = await web3.eth.getTransactionCount(mainAccount.address); 161 | 162 | let contract = await deployContract(nonce++); 163 | let aspect = await deployAspect(nonce++); 164 | 165 | await bindAspect(aspect, contract, nonce++); 166 | 167 | const eoaAccountNum = 1; 168 | const skAccountNum = 1; 169 | 170 | console.log("[generate EoA accounts]", eoaAccountNum) 171 | let accounts = await generateAccounts(eoaAccountNum, "0.01", nonce); 172 | console.log("[generate EoA accounts]", "done") 173 | 174 | console.log("[bind EoA with Aspect]", eoaAccountNum) 175 | let txsToSend = []; 176 | for (let i = 0; i < accounts.length; i++) { 177 | txsToSend.push(await generateBindEoATx(aspect, accounts[i])); 178 | } 179 | await sendTransactionsAtFixedRate(txsToSend, 4) 180 | console.log("[bind EoA with Aspect]", "done") 181 | 182 | console.log("[generate session key accounts]", skAccountNum) 183 | nonce = await web3.eth.getTransactionCount(mainAccount.address); 184 | let skAccounts = await generateAccounts(skAccountNum, 0, nonce); 185 | console.log("[generate session key accounts]", "done") 186 | 187 | let contractCallData = contract.methods.add([1]).encodeABI(); 188 | let contractCallMethod = rmPrefix(contractCallData).substring(0, 8); 189 | 190 | let currentBlockHeight = await web3.eth.getBlockNumber(); 191 | let expireBlockHeight = currentBlockHeight + 10000000; 192 | 193 | console.log("[register session key]", skAccountNum) 194 | txsToSend = []; 195 | let eoaNonce = await web3.eth.getTransactionCount(accounts[0].address); 196 | for (let i = 0; i < skAccounts.length; i++) { 197 | txsToSend.push(await generateRegisterSessionKeyTx(aspect, accounts[0], eoaNonce++, skAccounts[i], contract, contractCallMethod, expireBlockHeight)); 198 | } 199 | await sendTransactionsAtFixedRate(txsToSend, 4) 200 | console.log("[register session key]", "done") 201 | 202 | console.log("[send session key tx]", skAccountNum) 203 | while (true) { 204 | txsToSend = []; 205 | eoaNonce = await web3.eth.getTransactionCount(accounts[0].address); 206 | for (let i = 0; i < 100; i++) { 207 | txsToSend.push(await generateSessionKeyTx(skAccounts[0], accounts[0], eoaNonce++, contract, contractCallData, false)); 208 | } 209 | await sendTransactionsAtFixedRate(txsToSend, 2) 210 | } 211 | console.log("[send session key tx]", "done") 212 | } 213 | 214 | async function generateAccounts(numAccounts, initialFund, nonce) { 215 | let accounts = []; 216 | let fundingTxs = []; 217 | for (let i = 0; i < numAccounts; i++) { 218 | let account = web3.eth.accounts.create(); 219 | if (initialFund) { 220 | const tx = { 221 | from: mainAccount.address, 222 | to: account.address, 223 | value: web3.utils.toWei(initialFund, "ether"), 224 | gas: 21000, 225 | gasPrice: gasPrice, 226 | nonce: nonce++ 227 | }; 228 | const signed = await web3.eth.accounts.signTransaction(tx, mainAccount.privateKey) 229 | const txWithExpect = {raw: signed.rawTransaction, tx: tx}; 230 | fundingTxs.push(txWithExpect); 231 | } 232 | accounts.push(account); 233 | } 234 | if (fundingTxs.length > 0) { 235 | await sendTransactionsAtFixedRate(fundingTxs, 4); 236 | } 237 | return accounts; 238 | } 239 | 240 | function sendTransactionsAtFixedRate(txs, tps) { 241 | return new Promise(async (resolve, reject) => { 242 | let transactionIndex = 0; 243 | let finished = 0; 244 | const totalTransactions = txs.length; 245 | const interval = 1000 / tps; 246 | 247 | const sendTransaction = async () => { 248 | if (transactionIndex < totalTransactions) { 249 | const tx = txs[transactionIndex++]; 250 | web3.eth.sendSignedTransaction(tx.raw).catch(e => { 251 | console.log("[send transaction]", "fail", transactionIndex, tx.tx.from, tx.tx.to, tx.tx.nonce, tx.tx.value, e) 252 | assert(!tx.shouldFail, "[send transaction] transaction should not fail: " + e); 253 | reject(e); 254 | }).then(r => { 255 | console.log("[send transaction]", "success", transactionIndex, tx.tx.from, tx.tx.to, tx.tx.nonce, tx.tx.value) 256 | finished++; 257 | if (finished === totalTransactions) { 258 | resolve(); 259 | } 260 | }); 261 | } else { 262 | clearInterval(intervalId); 263 | } 264 | }; 265 | 266 | const intervalId = setInterval(sendTransaction, interval); 267 | }); 268 | } 269 | 270 | async function generateSessionKeyTx(sKeyAccount, eoa, eoaNonce, contract, callData, fail) { 271 | let tx = { 272 | from: sKeyAccount.address, 273 | nonce: eoaNonce, 274 | gasPrice, 275 | gas: 8000000, 276 | data: callData, 277 | to: contract.options.address, 278 | chainId 279 | }; 280 | let signedTx = await web3.eth.accounts.signTransaction(tx, sKeyAccount.privateKey); 281 | let validationData = "0x" 282 | + rmPrefix(eoa.address) 283 | + padStart(rmPrefix(signedTx.r), 64, "0") 284 | + padStart(rmPrefix(signedTx.s), 64, "0") 285 | + rmPrefix(getOriginalV(signedTx.v, chainId)); 286 | let encodedData = web3.eth.abi.encodeParameters(['bytes', 'bytes'], 287 | [validationData, callData]); 288 | encodedData = '0xCAFECAFE' + web3.utils.keccak256(encodedData).slice(2, 10) + encodedData.slice(2); 289 | const skTx = { 290 | nonce: numberToHex(eoaNonce), 291 | gasPrice: numberToHex(gasPrice), 292 | gas: numberToHex(8000000), 293 | data: encodedData, 294 | to: contract.options.address, 295 | chainId: numberToHex(chainId) 296 | } 297 | 298 | const raw = '0x' + new EthereumTx(skTx).serialize().toString('hex'); 299 | 300 | return { 301 | raw: raw, 302 | tx: tx, 303 | shouldFail: fail 304 | }; 305 | } 306 | 307 | async function generateRegisterSessionKeyTx(aspect, eoa, eoaNonce, sKeyAccount, contract, contractCallMethod, expireBlockHeight) { 308 | let sKey = rmPrefix(sKeyAccount.address); 309 | let sKeyContract = rmPrefix(contract.options.address); 310 | 311 | console.log("[register session key]", sKey, sKeyContract, contractCallMethod, expireBlockHeight); 312 | let op = 313 | "0x0001" 314 | + sKey 315 | + sKeyContract 316 | + "0001" + contractCallMethod 317 | + rmPrefix(web3.eth.abi.encodeParameter('uint256', expireBlockHeight)).slice(48, 64); 318 | 319 | const operationData = aspect.operation(op).encodeABI(); 320 | const tx = { 321 | from: eoa.address, 322 | nonce: eoaNonce, 323 | gasPrice, 324 | gas: 8000000, 325 | to: aspectCore.options.address, 326 | data: operationData, 327 | chainId 328 | } 329 | 330 | const signed = await web3.eth.accounts.signTransaction(tx, eoa.privateKey); 331 | 332 | return { 333 | raw: signed.rawTransaction, 334 | tx: tx 335 | }; 336 | } 337 | 338 | async function generateBindEoATx(aspect, eoa) { 339 | const nonce = await web3.eth.getTransactionCount(eoa.address); 340 | const bindingData = aspectCore.methods.bind(aspect.options.address, 1, eoa.address, 1).encodeABI(); 341 | const tx = { 342 | from: eoa.address, 343 | nonce: nonce, 344 | gasPrice, 345 | gas: 4000000, 346 | to: aspectCore.options.address, 347 | data: bindingData, 348 | chainId 349 | } 350 | 351 | const signed = await web3.eth.accounts.signTransaction(tx, eoa.privateKey); 352 | 353 | return { 354 | raw: signed.rawTransaction, 355 | tx: tx 356 | }; 357 | } 358 | 359 | async function bindAspect(aspect, contract, nonce) { 360 | console.log("[bind aspect]", aspect.options.address, contract.options.address); 361 | const bindingData = contract.bind({ 362 | priority: 1, 363 | aspectId: aspect.options.address, 364 | aspectVersion: 1, 365 | }).encodeABI(); 366 | const tx = { 367 | from: mainAccount.address, 368 | nonce: nonce, 369 | gasPrice, 370 | gas: 4000000, 371 | to: aspectCore.options.address, 372 | data: bindingData, 373 | chainId 374 | } 375 | const signedTx = await web3.eth.accounts.signTransaction(tx, mainAccount.privateKey); 376 | await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 377 | 378 | let aspects = await aspectCore.methods.aspectsOf(contract.options.address).call({}); 379 | let bounded = false; 380 | for (let i = 0; i < aspects.length; i++) { 381 | if (aspects[i].aspectId === aspect.options.address) { 382 | bounded = true; 383 | break; 384 | } 385 | } 386 | assert(bounded, "binding contract failed"); 387 | console.log("[bind aspect]", "done"); 388 | } 389 | 390 | async function deployContract(nonce) { 391 | console.log("[deploy contract]", "start"); 392 | let contract = new web3.eth.Contract(contractABI); 393 | contract = await contract.deploy({data: contractBin}).send({ 394 | from: mainAccount.address, 395 | nonce: nonce, 396 | gasPrice, 397 | gas: 4000000, 398 | }); 399 | console.log("[deploy contract]", contract.options.address); 400 | return contract; 401 | } 402 | 403 | async function deployAspect(nonce) { 404 | console.log("[deploy aspect]", "start"); 405 | let aspect = new web3.atl.Aspect(); 406 | let aspectDeployData = aspect.deploy({ 407 | data: '0x' + aspectCode, 408 | properties: [], 409 | joinPoints: ["VerifyTx"], 410 | paymaster: mainAccount.address, 411 | proof: '0x0' 412 | }).encodeABI(); 413 | const tx = { 414 | from: mainAccount.address, 415 | nonce: nonce, 416 | gasPrice, 417 | gas: 4000000, 418 | to: aspectCore.options.address, 419 | data: aspectDeployData, 420 | chainId 421 | } 422 | const signedTx = await web3.eth.accounts.signTransaction(tx, mainAccount.privateKey); 423 | const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 424 | aspect.options.address = receipt.aspectAddress; 425 | console.log("[deploy aspect]", aspect.options.address); 426 | return aspect; 427 | } 428 | 429 | function rmPrefix(data) { 430 | if (data.startsWith('0x')) { 431 | return data.substring(2, data.length); 432 | } else { 433 | return data; 434 | } 435 | } 436 | 437 | function getOriginalV(hexV, chainId_) { 438 | const v = new BigNumber(hexV, 16); 439 | const chainId = new BigNumber(chainId_); 440 | const chainIdMul = chainId.multipliedBy(2); 441 | 442 | const originalV = v.minus(chainIdMul).minus(8); 443 | 444 | const originalVHex = originalV.toString(16); 445 | 446 | return originalVHex; 447 | } 448 | 449 | function padStart(str, targetLength, padString) { 450 | targetLength = Math.max(targetLength, str.length); 451 | padString = String(padString || ' '); 452 | 453 | if (str.length >= targetLength) { 454 | return str; 455 | } else { 456 | targetLength = targetLength - str.length; 457 | if (targetLength > padString.length) { 458 | padString += padString.repeat(targetLength / padString.length); 459 | } 460 | return padString.slice(0, targetLength) + str; 461 | } 462 | } 463 | 464 | async function testEntry() { 465 | console.log("test start") 466 | // console.log("==== ⏳ multiEoAWithMultiSessionKey start ==="); 467 | // await multiEoAWithMultiSessionKey(); 468 | // console.log("==== ✅ multiEoAWithMultiSessionKey end ==="); 469 | // console.log("==== ⏳ oneEoAWithMultiSessionKey start ==="); 470 | // await oneEoAWithMultiSessionKey(); 471 | // console.log("==== ✅ oneEoAWithMultiSessionKey end ==="); 472 | await oneEoAWithOneSessionKey(); 473 | } 474 | 475 | async function preparation() { 476 | gasPrice = await web3.eth.getGasPrice(); 477 | chainId = await web3.eth.getChainId(); 478 | } 479 | 480 | preparation().then(r => { 481 | testEntry().then(r => { 482 | console.log("test done"); 483 | }); 484 | }); 485 | -------------------------------------------------------------------------------- /aspect/tests/test_operation.cjs: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const Web3 = require('@artela/web3'); 4 | const fs = require("fs"); 5 | const BigNumber = require('bignumber.js'); 6 | 7 | 8 | // ****************************************** 9 | // init web3 and private key 10 | // ****************************************** 11 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 12 | const web3 = new Web3(configJson.node); 13 | 14 | let sk = fs.readFileSync("privateKey.txt", 'utf-8'); 15 | const account = web3.eth.accounts.privateKeyToAccount(sk.trim()); 16 | web3.eth.accounts.wallet.add(account.privateKey); 17 | 18 | // ****************************************** 19 | // init aspect client 20 | // ****************************************** 21 | // instantiate an instance of the contract 22 | let aspectCore = web3.atl.aspectCore(); 23 | // instantiate an instance of aspect 24 | let aspect = new web3.atl.Aspect(); 25 | 26 | // ****************************************** 27 | // test data 28 | // ****************************************** 29 | let mainKey = rmPrefix(account.address); 30 | let sKey = rmPrefix(account.address);; 31 | let contract = "0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 32 | let method1 = "0A0A0A0A"; 33 | let method2 = "0B0B0B0B"; 34 | 35 | /** 36 | * begin test 37 | */ 38 | async function f() { 39 | 40 | console.log('start testing operation'); 41 | 42 | await deployAspect(); 43 | await testRegisterSessionKey(); 44 | await testGetSessionKey(); 45 | await testVerifySessionKeyScope(); 46 | await testVerifySignature(); 47 | await testEcRecover(); 48 | await testRegisterSessionKey2(); 49 | await testGetAllSessionKey(); 50 | } 51 | 52 | async function deployAspect() { 53 | // load aspect code and deploy 54 | let aspectCode = fs.readFileSync('./build/release.wasm', { 55 | encoding: "hex" 56 | }); 57 | 58 | let aspectDeployData = aspect.deploy({ 59 | data: '0x' + aspectCode, 60 | properties: [], 61 | joinPoints: ["VerifyTx"], 62 | paymaster: account.address, 63 | proof: '0x0' 64 | }).encodeABI(); 65 | 66 | let tx = await getOperationTx(aspectDeployData); 67 | 68 | console.log('signed Aspect deploy Tx'); 69 | 70 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 71 | 72 | console.log('send Aspect deploy Tx'); 73 | 74 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 75 | aspect.options.address = receipt.aspectAddress; 76 | 77 | console.log('receipt :\n', receipt); 78 | console.log('aspect address is: ', aspect.options.address); 79 | } 80 | 81 | async function testRegisterSessionKey() { 82 | 83 | printTestCase("testRegisterSessionKey: success"); 84 | 85 | let currentBlockHeight = await web3.eth.getBlockNumber(); 86 | let expireBlockHeight = currentBlockHeight + 20; 87 | 88 | let op = "0x0001"; 89 | let params = 90 | sKey 91 | + contract 92 | + "0002" + method1 + method2 93 | + web3.eth.abi.encodeParameter('uint256', expireBlockHeight).slice(48, 64); 94 | ; 95 | 96 | console.log("op: ", op); 97 | console.log("params: ", params); 98 | 99 | let calldata = aspect.operation(op + params).encodeABI(); 100 | 101 | let ret = await web3.eth.call({ 102 | from: account.address, 103 | to: aspectCore.options.address, // contract address 104 | data: calldata 105 | }); 106 | 107 | let tx = await getOperationTx(calldata) 108 | 109 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 110 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 111 | 112 | console.log('register session key result: sucess'); 113 | console.log(receipt) 114 | } 115 | 116 | async function testRegisterSessionKey2() { 117 | 118 | printTestCase("testRegisterSessionKey2: success"); 119 | 120 | let currentBlockHeight = await web3.eth.getBlockNumber(); 121 | let expireBlockHeight = currentBlockHeight + 20; 122 | 123 | let op = "0x0001"; 124 | let params = 125 | sKey.slice(2) + "CA" // register another key 126 | + contract 127 | + "0002" + method1 + method2 128 | + web3.eth.abi.encodeParameter('uint256', expireBlockHeight).slice(48, 64); 129 | ; 130 | 131 | console.log("op: ", op); 132 | console.log("params: ", params); 133 | 134 | let calldata = aspect.operation(op + params).encodeABI(); 135 | 136 | let ret = await web3.eth.call({ 137 | from: account.address, 138 | to: aspectCore.options.address, // contract address 139 | data: calldata 140 | }); 141 | 142 | let tx = await getOperationTx(calldata) 143 | 144 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 145 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 146 | 147 | console.log('register session key result: sucess'); 148 | console.log(receipt) 149 | } 150 | 151 | async function testGetSessionKey() { 152 | printTestCase("testGetSessionKey: success"); 153 | 154 | let op = "0x1001"; 155 | let queryKey = web3.utils.keccak256("0x" + contract + mainKey + sKey); 156 | let params = rmPrefix(queryKey); 157 | let calldata = aspect.operation(op + params).encodeABI(); 158 | 159 | console.log("op: ", op); 160 | console.log("params: ", params); 161 | 162 | let ret = await web3.eth.call({ 163 | to: aspectCore.options.address, // contract address 164 | data: calldata 165 | }); 166 | 167 | console.log("ret ", ret); 168 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 169 | } 170 | 171 | async function testVerifySessionKeyScope() { 172 | 173 | printTestCase("testVerifySessionKeyScope: success"); 174 | 175 | let op = "0x1002"; 176 | let params = 177 | mainKey 178 | + contract 179 | + method1 180 | + sKey; 181 | let calldata = aspect.operation(op + params).encodeABI(); 182 | 183 | console.log("op: ", op); 184 | console.log("params: ", params); 185 | 186 | let ret = await web3.eth.call({ 187 | to: aspectCore.options.address, // contract address 188 | data: calldata 189 | }); 190 | console.log("ret ", ret); 191 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 192 | 193 | printTestCase("verifySessionKeyScope: fail"); 194 | 195 | op = "0x1002"; 196 | params = 197 | mainKey 198 | + contract 199 | + "0C0C0C0C" // illegal method 200 | + sKey; 201 | 202 | calldata = aspect.operation(op + params).encodeABI(); 203 | 204 | console.log("op: ", op); 205 | console.log("params: ", params); 206 | 207 | ret = await web3.eth.call({ 208 | to: aspectCore.options.address, // contract address 209 | data: calldata 210 | }); 211 | 212 | console.log("ret ", ret); 213 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 214 | } 215 | 216 | async function testVerifySignature() { 217 | let chainId = await web3.eth.getChainId(); 218 | 219 | printTestCase("testVerifySignature: success"); 220 | 221 | let testCalldata = "010101"; 222 | let tx = await getOperationTx(testCalldata); 223 | 224 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 225 | 226 | console.log("signedTx: ", signedTx); 227 | console.log("v: ", getOriginalV(signedTx.v, chainId)); 228 | 229 | let op = "0x1003" 230 | let params = 231 | mainKey 232 | + contract 233 | + rmPrefix(signedTx.messageHash) 234 | + rmPrefix(signedTx.r) 235 | + rmPrefix(signedTx.s) 236 | + rmPrefix(getOriginalV(signedTx.v, chainId)); 237 | 238 | console.log("op: ", op); 239 | console.log("params: ", params); 240 | 241 | let calldata = aspect.operation(op + params).encodeABI(); 242 | 243 | let ret = await web3.eth.call({ 244 | to: aspectCore.options.address, // contract address 245 | data: calldata 246 | }); 247 | 248 | console.log("ret ", ret); 249 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 250 | 251 | 252 | printTestCase("testVerifySignature: error sig"); 253 | 254 | tx = await getOperationTx(testCalldata); 255 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 256 | 257 | console.log("signedTx: ", signedTx); 258 | console.log("v: ", getOriginalV(signedTx.v, chainId)); 259 | 260 | op = "0x1003" 261 | params = 262 | mainKey 263 | + contract 264 | + rmPrefix(signedTx.messageHash) 265 | + rmPrefix(signedTx.r) 266 | + rmPrefix(signedTx.s) 267 | + "AA"; // illegal v 268 | 269 | console.log("op: ", op); 270 | console.log("params: ", params); 271 | 272 | calldata = aspect.operation(op + params).encodeABI(); 273 | 274 | ret = await web3.eth.call({ 275 | to: aspectCore.options.address, // contract address 276 | data: calldata 277 | }); 278 | console.log("ret ", ret); 279 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 280 | 281 | 282 | printTestCase("testVerifySignature: error from"); 283 | 284 | tx = await getOperationTx(testCalldata); 285 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 286 | console.log("signedTx: ", signedTx); 287 | console.log("v: ", getOriginalV(signedTx.v, chainId)); 288 | 289 | op = "0x1003" 290 | params = 291 | "CAFE857467b61f2e4b1a614a0d560cd75c0c076f" // illegal from 292 | + contract 293 | + rmPrefix(signedTx.messageHash) 294 | + rmPrefix(signedTx.r) 295 | + rmPrefix(signedTx.s) 296 | + rmPrefix(getOriginalV(signedTx.v, chainId)); 297 | 298 | console.log("op: ", op); 299 | console.log("params: ", params); 300 | 301 | calldata = aspect.operation(op + params).encodeABI(); 302 | 303 | ret = await web3.eth.call({ 304 | to: aspectCore.options.address, // contract address 305 | data: calldata 306 | }); 307 | console.log("ret ", ret); 308 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 309 | } 310 | 311 | async function testEcRecover() { 312 | let chainId = await web3.eth.getChainId(); 313 | 314 | printTestCase("testEcRecover: success"); 315 | 316 | let testCalldata = "010101"; 317 | let tx = await getOperationTx(testCalldata); 318 | 319 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 320 | console.log("signedTx: ", signedTx); 321 | console.log("v: ", getOriginalV(signedTx.v, chainId)); 322 | 323 | let op = "0x1004" 324 | let params = 325 | mainKey 326 | + contract 327 | + rmPrefix(signedTx.messageHash) 328 | + rmPrefix(signedTx.r) 329 | + rmPrefix(signedTx.s) 330 | + rmPrefix(getOriginalV(signedTx.v, chainId)); 331 | 332 | console.log("op: ", op); 333 | console.log("params: ", params); 334 | 335 | let calldata = aspect.operation(op + params).encodeABI(); 336 | 337 | let ret = await web3.eth.call({ 338 | to: aspectCore.options.address, // contract address 339 | data: calldata 340 | }); 341 | console.log("ret ", ret); 342 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 343 | 344 | printTestCase("testEcRecover: fail"); 345 | 346 | tx = await getOperationTx(testCalldata); 347 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 348 | console.log("signedTx: ", signedTx); 349 | console.log("v: ", getOriginalV(signedTx.v, chainId)); 350 | 351 | op = "0x1004" 352 | params = 353 | mainKey 354 | + contract 355 | + rmPrefix(signedTx.messageHash) 356 | + rmPrefix(signedTx.r) 357 | + rmPrefix(signedTx.s) 358 | + "AA"; // illegal v 359 | 360 | console.log("op: ", op); 361 | console.log("params: ", params); 362 | 363 | calldata = aspect.operation(op + params).encodeABI(); 364 | 365 | ret = await web3.eth.call({ 366 | to: aspectCore.options.address, // contract address 367 | data: calldata 368 | }); 369 | console.log("ret ", ret); 370 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 371 | } 372 | 373 | async function testGetAllSessionKey() { 374 | printTestCase("testGetAllSessionKey: success"); 375 | 376 | let op = "0x1005"; 377 | let params = mainKey; 378 | let calldata = aspect.operation(op + params).encodeABI(); 379 | 380 | console.log("op: ", op); 381 | console.log("params: ", params); 382 | 383 | let ret = await web3.eth.call({ 384 | to: aspectCore.options.address, // contract address 385 | data: calldata 386 | }); 387 | 388 | console.log("ret ", ret); 389 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 390 | } 391 | 392 | function rmPrefix(data) { 393 | if (data.startsWith('0x')) { 394 | return data.substring(2, data.length); 395 | } else { 396 | return data; 397 | } 398 | } 399 | 400 | function getOriginalV(hexV, chainId_) { 401 | const v = new BigNumber(hexV, 16); 402 | const chainId = new BigNumber(chainId_); 403 | const chainIdMul = chainId.multipliedBy(2); 404 | 405 | const originalV = v.minus(chainIdMul).minus(8); 406 | 407 | const originalVHex = originalV.toString(16); 408 | 409 | return originalVHex; 410 | } 411 | 412 | function printTestCase(intro) { 413 | console.log("\n\n" + 414 | "// ******************************************\n" + 415 | "// " + intro + " \n" + 416 | "// ******************************************\n\n"); 417 | } 418 | async function getOperationTx(calldata) { 419 | 420 | let nonce = await web3.eth.getTransactionCount(account.address); 421 | let gasPrice = await web3.eth.getGasPrice(); 422 | let chainId = await web3.eth.getChainId(); 423 | 424 | let tx = { 425 | from: account.address, 426 | nonce: nonce, 427 | gasPrice, 428 | gas: 8000000, 429 | data: calldata, 430 | to: aspectCore.options.address, 431 | chainId 432 | } 433 | 434 | console.log('tx: \n', tx); 435 | 436 | return tx; 437 | } 438 | 439 | f().then(); 440 | -------------------------------------------------------------------------------- /aspect/tests/test_tx.cjs: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | //todo modify it 4 | 5 | const Web3 = require('@artela/web3'); 6 | const fs = require("fs"); 7 | const { numberToHex } = require("@artela/web3-utils"); 8 | const BigNumber = require('bignumber.js'); 9 | 10 | const contractBin = fs.readFileSync('./build/contract/Counter.bin', "utf-8"); 11 | const abi = fs.readFileSync('./build/contract/Counter.abi', "utf-8"); 12 | const contractABI = JSON.parse(abi); 13 | const EthereumTx = require('ethereumjs-tx').Transaction; 14 | 15 | const demoContractOptions = { 16 | data: contractBin 17 | }; 18 | function rmPrefix(data) { 19 | if (data.startsWith('0x')) { 20 | return data.substring(2, data.length); 21 | } else { 22 | return data; 23 | } 24 | } 25 | 26 | function getOriginalV(hexV, chainId_) { 27 | const v = new BigNumber(hexV, 16); 28 | const chainId = new BigNumber(chainId_); 29 | const chainIdMul = chainId.multipliedBy(2); 30 | 31 | const originalV = v.minus(chainIdMul).minus(8); 32 | 33 | const originalVHex = originalV.toString(16); 34 | 35 | return originalVHex; 36 | } 37 | 38 | function padStart(str, targetLength, padString) { 39 | targetLength = Math.max(targetLength, str.length); 40 | padString = String(padString || ' '); 41 | 42 | if (str.length >= targetLength) { 43 | return str; 44 | } else { 45 | targetLength = targetLength - str.length; 46 | if (targetLength > padString.length) { 47 | padString += padString.repeat(targetLength / padString.length); 48 | } 49 | return padString.slice(0, targetLength) + str; 50 | } 51 | } 52 | 53 | async function f() { 54 | console.log('start running demo'); 55 | 56 | // ****************************************** 57 | // init web3 and private key 58 | // ****************************************** 59 | const configJson = JSON.parse(fs.readFileSync('./project.config.json', "utf-8").toString()); 60 | const web3 = new Web3(configJson.node); 61 | 62 | let sk = fs.readFileSync("privateKey.txt", 'utf-8'); 63 | const account = web3.eth.accounts.privateKeyToAccount(sk.trim()); 64 | web3.eth.accounts.wallet.add(account.privateKey); 65 | const balance = await web3.eth.getBalance(account.address); 66 | console.log("node:" + configJson.node + " sender:"+account.address+" balance:"+balance) 67 | 68 | 69 | let gasPrice = await web3.eth.getGasPrice(); 70 | let chainId = await web3.eth.getChainId(); 71 | let nonce = await web3.eth.getTransactionCount(account.address); 72 | let aspectCore = web3.atl.aspectCore(); 73 | 74 | // ****************************************** 75 | // prepare 1. deploy contract 76 | // ****************************************** 77 | 78 | let contract = new web3.eth.Contract(contractABI); 79 | let deployData = contract.deploy(demoContractOptions).encodeABI(); 80 | let tx = { 81 | from: account.address, 82 | nonce: nonce++, 83 | gasPrice, 84 | gas: 4000000, 85 | data: deployData, 86 | chainId 87 | } 88 | 89 | let signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 90 | console.log("signed contract deploy tx : \n", signedTx); 91 | 92 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 93 | contract.options.address = receipt.contractAddress; 94 | console.log('contract address is: ', contract.options.address); 95 | 96 | // ****************************************** 97 | // prepare 2. deploy aspect 98 | // ****************************************** 99 | 100 | // load aspect code and deploy 101 | let aspectCode = fs.readFileSync('./build/release.wasm', { 102 | encoding: "hex" 103 | }); 104 | 105 | // instantiate an instance of aspect 106 | let aspect = new web3.atl.Aspect(); 107 | let aspectDeployData = aspect.deploy({ 108 | data: '0x' + aspectCode, 109 | properties: [], 110 | joinPoints:["VerifyTx"], 111 | paymaster: account.address, 112 | proof: '0x0' 113 | }).encodeABI(); 114 | 115 | tx = { 116 | from: account.address, 117 | nonce: nonce++, 118 | gasPrice, 119 | gas: 4000000, 120 | to: aspectCore.options.address, 121 | data: aspectDeployData, 122 | chainId 123 | } 124 | 125 | console.log('signed Aspect deploy Tx'); 126 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 127 | console.log('send Aspect deploy Tx'); 128 | receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 129 | aspect.options.address = receipt.aspectAddress; 130 | console.log('aspect address is: ', aspect.options.address); 131 | 132 | // ****************************************** 133 | // prepare 3. binding contract to aspect 134 | // ****************************************** 135 | 136 | // binding with smart contract 137 | let contractBindingData = await contract.bind({ 138 | priority: 1, 139 | aspectId: aspect.options.address, 140 | aspectVersion: 1, 141 | }).encodeABI(); 142 | 143 | tx = { 144 | from: account.address, 145 | nonce: nonce++, 146 | gasPrice, 147 | gas: 4000000, 148 | data: contractBindingData, 149 | to: aspectCore.options.address, 150 | chainId 151 | } 152 | 153 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 154 | receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 155 | console.log(`binding contract result:`); 156 | console.log(receipt); 157 | 158 | let ret2 = await aspectCore.methods.boundAddressesOf(aspect.options.address).call(); 159 | console.log("binding result:", ret2) 160 | 161 | // ****************************************** 162 | // start testing session keys 163 | // ****************************************** 164 | 165 | // ****************************************** 166 | // step 1. binding EoA to aspect 167 | // ****************************************** 168 | 169 | // binding with EoA 170 | let eoaBindingData = await aspectCore.methods.bind(aspect.options.address, 1, account.address, 1).encodeABI(); 171 | 172 | tx = { 173 | from: account.address, 174 | nonce: nonce++, 175 | gasPrice, 176 | gas: 4000000, 177 | data: eoaBindingData, 178 | to: aspectCore.options.address, 179 | chainId 180 | } 181 | 182 | console.log('binding data:', eoaBindingData); 183 | 184 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 185 | receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 186 | console.log(`binding EoA result:`); 187 | console.log(receipt); 188 | 189 | ret2 = await aspectCore.methods.boundAddressesOf(aspect.options.address).call(); 190 | 191 | console.log("binding result:", ret2) 192 | 193 | // ****************************************** 194 | // step 1. register session key 195 | // ****************************************** 196 | 197 | // create session keys 198 | let sKeyPrivKey = web3.eth.accounts.create(web3.utils.randomHex(32)).privateKey; 199 | let sKeyAccount = web3.eth.accounts.privateKeyToAccount(sKeyPrivKey); 200 | let sKeyPrivKey2 = web3.eth.accounts.create(web3.utils.randomHex(32)).privateKey; 201 | let sKeyAccount2 = web3.eth.accounts.privateKeyToAccount(sKeyPrivKey2); 202 | 203 | let mainKey = rmPrefix(account.address); 204 | let sKey = rmPrefix(sKeyAccount.address); 205 | let sKeyContract = rmPrefix(contract.options.address); 206 | 207 | let contractCallData = contract.methods.add([1]).encodeABI(); 208 | let contractCallMethod = rmPrefix(contractCallData).substring(0, 8); 209 | 210 | console.log("\n\n" + 211 | "// ******************************************\n" + 212 | "// test registerSessionKey \n" + 213 | "// ******************************************\n\n"); 214 | 215 | let currentBlockHeight = await web3.eth.getBlockNumber(); 216 | console.log("currentBlockHeight :", currentBlockHeight); 217 | let expireBlockHeight = currentBlockHeight + 100; // ~10s 218 | 219 | // let op = "0x0001" + sKey + sKeyContract + "0001" + contractCallMethod + expireBlockHeight; 220 | let op = 221 | "0x0001" 222 | + sKey 223 | + sKeyContract 224 | + "0001" + contractCallMethod 225 | + rmPrefix(web3.eth.abi.encodeParameter('uint256', expireBlockHeight)).slice(48, 64); 226 | 227 | let sessionKeyRegData = aspect.operation(op).encodeABI(); 228 | console.log("op: ", op); 229 | console.log("calldata: ", sessionKeyRegData); 230 | 231 | tx = { 232 | from: account.address, 233 | nonce: nonce++, 234 | gasPrice, 235 | gas: 8000000, 236 | data: sessionKeyRegData, 237 | to: aspectCore.options.address, 238 | chainId 239 | } 240 | //0x0001 e2f8857467b61f2e4b1a614a0d560cd75c0c076f0250032b4a11478969dc4caaa11ecc2ea98cfc1200020A0A0A0A0B0B0B0B 241 | 242 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 243 | receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 244 | console.log('register session key result: '); 245 | console.log(receipt) 246 | 247 | // ****************************************** 248 | // test getSessionKey: success 249 | // ****************************************** 250 | console.log("\n\n" + 251 | "// ******************************************\n" + 252 | "// test getSessionKey: success \n" + 253 | "// ******************************************\n\n"); 254 | 255 | let queryKey = web3.utils.keccak256("0x" + sKeyContract + mainKey + sKey); 256 | op = "0x1001" + rmPrefix(queryKey); 257 | sessionKeyRegData = aspect.operation(op).encodeABI(); 258 | 259 | console.log("op: ", op); 260 | console.log("calldata: ", sessionKeyRegData); 261 | 262 | let ret = await web3.eth.call({ 263 | to: aspectCore.options.address, 264 | data: sessionKeyRegData 265 | }); 266 | console.log("ret ", ret); 267 | console.log("ret ", web3.eth.abi.decodeParameter('string', ret)); 268 | 269 | // ****************************************** 270 | // test sign by main key: success 271 | // ****************************************** 272 | console.log("\n\n" + 273 | "// ******************************************\n" + 274 | "// test sign by main key: success \n" + 275 | "// ******************************************\n\n"); 276 | 277 | tx = { 278 | from: account.address, 279 | nonce: nonce++, 280 | gasPrice, 281 | gas: 8000000, 282 | data: contractCallData, 283 | to: contract.options.address, 284 | chainId 285 | } 286 | 287 | console.log(tx) 288 | 289 | signedTx = await web3.eth.accounts.signTransaction(tx, account.privateKey); 290 | receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 291 | console.log(`call contract with session key result: `); 292 | console.log(receipt); 293 | 294 | // ****************************************** 295 | // step 2. sign tx by session key 296 | // ****************************************** 297 | 298 | // ****************************************** 299 | // test sign by skey: success 300 | // ****************************************** 301 | console.log("\n\n" + 302 | "// ******************************************\n" + 303 | "// test sign by skey: success \n" + 304 | "// ******************************************\n\n"); 305 | 306 | tx = { 307 | from: sKeyAccount.address, 308 | nonce: nonce++, 309 | gasPrice, 310 | gas: 8000000, 311 | data: contractCallData, 312 | to: contract.options.address, 313 | chainId 314 | } 315 | 316 | web3.eth.accounts.wallet.add(sKeyAccount.privateKey); 317 | signedTx = await web3.eth.accounts.signTransaction(tx, sKeyAccount.privateKey); 318 | console.log("sign tx : ", signedTx); 319 | // params encode rules: 320 | // 20 bytes: from 321 | // eg. e2f8857467b61f2e4b1a614a0d560cd75c0c076f 322 | // 32 bytes: r 323 | // 32 bytes: s 324 | // 1 bytes: v 325 | 326 | let validationData = "0x" 327 | + mainKey 328 | + padStart(rmPrefix(signedTx.r), 64, "0") 329 | + padStart(rmPrefix(signedTx.s), 64, "0") 330 | + rmPrefix(getOriginalV(signedTx.v, chainId)); 331 | 332 | console.log("validationData : ", validationData); 333 | console.log("contractCallData : ", contractCallData); 334 | let encodedData = web3.eth.abi.encodeParameters(['bytes', 'bytes'], 335 | [validationData, contractCallData]); 336 | 337 | // new calldata: magic prefix + checksum(encodedData) + encodedData(validation data + raw calldata) 338 | // 0xCAFECAFE is a magic prefix, 339 | encodedData = '0xCAFECAFE' + web3.utils.keccak256(encodedData).slice(2, 10) + encodedData.slice(2); 340 | console.log("encodedData : ", encodedData); 341 | tx = { 342 | // from: sKeyAccount.address, 343 | nonce: numberToHex(nonce - 1), 344 | gasPrice: numberToHex(gasPrice), 345 | gas: numberToHex(8000000), 346 | data: encodedData, 347 | to: contract.options.address, 348 | chainId: numberToHex(chainId) 349 | } 350 | 351 | let aspectRet2 = await aspectCore.methods.aspectsOf(contract.options.address).call({}); 352 | console.log("contract bind result:", contract.options.address,aspectRet2) 353 | 354 | 355 | // wait for block committing 356 | let rawTx = '0x' + new EthereumTx(tx).serialize().toString('hex'); 357 | receipt = await web3.eth.sendSignedTransaction(rawTx); 358 | console.log(`call contract with session key result: `); 359 | console.log(receipt); 360 | 361 | // ****************************************** 362 | // test sign by skey: fail due to illegal skey 363 | // ****************************************** 364 | console.log("\n\n" + 365 | "// ******************************************\n" + 366 | "// test sign by skey: fail due to illegal skey \n" + 367 | "// ******************************************\n\n"); 368 | 369 | tx = { 370 | from: sKeyAccount.address, 371 | nonce: nonce++, 372 | gasPrice, 373 | gas: 8000000, 374 | data: contractCallData, 375 | to: contract.options.address, 376 | chainId 377 | } 378 | 379 | web3.eth.accounts.wallet.add(sKeyAccount2.privateKey); 380 | signedTx = await web3.eth.accounts.signTransaction(tx, sKeyAccount2.privateKey); 381 | console.log("sign tx : ", signedTx); 382 | 383 | validationData = mainKey + rmPrefix(signedTx.r) + rmPrefix(signedTx.s) + rmPrefix(getOriginalV(signedTx.v, chainId)); 384 | 385 | console.log("validationData : ", validationData); 386 | console.log("contractCallData : ", contractCallData); 387 | encodedData = web3.eth.abi.encodeParameters(['bytes', 'bytes'], 388 | ["0x" + validationData, contractCallData]); 389 | 390 | encodedData = web3.eth.abi.encodeParameters(['bytes', 'bytes'], 391 | ["0x" + validationData, contractCallData]); 392 | 393 | tx = { 394 | from: sKeyAccount.address, 395 | nonce: numberToHex(nonce - 1), 396 | gasPrice: numberToHex(gasPrice), 397 | gas: numberToHex(8000000), 398 | data: encodedData, 399 | to: contract.options.address, 400 | chainId: numberToHex(chainId) 401 | } 402 | 403 | rawTx = '0x' + new EthereumTx(tx).serialize().toString('hex'); 404 | 405 | try { 406 | receipt = await web3.eth.sendSignedTransaction(rawTx); 407 | throw new Error('this case must be error.'); 408 | } catch (error) { 409 | // console.error(error); 410 | console.log(`pass: call contract with illgal session key`); 411 | } 412 | 413 | console.log(`all test cases pass`); 414 | } 415 | 416 | f().then(); 417 | -------------------------------------------------------------------------------- /aspect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/assembly.json", 3 | "include": [ 4 | "./assembly/**/*.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /img/2023-11-09-15.32.38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/img/2023-11-09-15.32.38.png -------------------------------------------------------------------------------- /img/2023-11-09-15.37.26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/img/2023-11-09-15.37.26.png -------------------------------------------------------------------------------- /img/2023-11-09-15.52.20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/img/2023-11-09-15.52.20.png -------------------------------------------------------------------------------- /img/2023-11-09-15.54.18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/img/2023-11-09-15.54.18.png -------------------------------------------------------------------------------- /img/2023-11-09-16.16.17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/img/2023-11-09-16.16.17.png -------------------------------------------------------------------------------- /js_client/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | TBD 4 | 5 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | `SessionKeyAspectClient` is a JavaScript library for managing session keys on the Artela [Session-key Aspect](https://github.com/artela-network/session-key-aspect ). It provides a set of APIs to interact with Aspect for session key operations. 4 | 5 | 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install @artela/session-key-aspect-client 11 | ``` 12 | 13 | 14 | 15 | ## Quick start 16 | 17 | Import related lib on your node.js project. 18 | 19 | ```js 20 | const SessionKeyAspectClient = require('session-key-aspect-client'); 21 | const Web3 = require('@artela/web3'); 22 | ``` 23 | 24 | Init `SessionKeyAspectClient` 25 | 26 | ```js 27 | // 1. init web3 client 28 | const testnetRpc = "https://betanet-rpc1.artela.network" 29 | const web3 = new Web3(testnetRpc); 30 | 31 | // 2. init session key client 32 | const testAspectAddress = "0x06786bB59719d7DDD9D42457a16BbCD6953A7cab"; 33 | const aspectClient = new SessionKeyAspectClient(web3, testAspectAddress); 34 | 35 | ``` 36 | 37 | Register a session key and query it. 38 | 39 | ```js 40 | // init your main key 41 | const yourWalletPrivateKey = "0xCAFE....CAFE"; 42 | const account = web3.eth.accounts.privateKeyToAccount(yourWalletPrivateKey); 43 | 44 | // register session key 45 | const testSessionKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 46 | const testContract = "0330032b4a11478969dc4caaa11ecc2ea98cfcFF"; 47 | const testMethods = ["0A0A0A0A", "0B0B0B0B"]; 48 | const testExpireBlockNumber = 2000; 49 | 50 | let ret = await aspectClient.registerSessionKey(account, testSessionKeyAddress, testContract, testMethods, testExpireBlockNumber); 51 | console.log(ret); 52 | 53 | // query this session key 54 | let sessionKey = await aspectClient.getSessionKey(account.address, testSessionKeyAddress, testContract); 55 | console.log(sessionKey) 56 | ``` 57 | 58 | 59 | 60 | ## Usage 61 | 62 | **Usage 1.** Use `registerSessionKey` to sign and send the session key. 63 | 64 | **Usage 2.** Use `registerSessionKeyUnsignTx` to construct an unsigned transaction and then sign and send it by your web3 client. 65 | 66 | **Usage 3.** Use `registerSessionKeyByMetamask` to require Metamask to sign and send the register transaction. 67 | 68 | 69 | 70 | ## API Reference 71 | 72 | ### 1. `registerSessionKey` 73 | 74 | Registers a new session key. 75 | 76 | - **Parameters**: 77 | 78 | - `account` - object: The web3.js account object. Learn more: [web3.eth.accounts](https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#privatekeytoaccount) 79 | - `sessionKeyAddress` - string: The session key address. 80 | - `bindingContractAddress` - string: The contract address to bind. 81 | - `bindingMethodSigSet` - string[]: An array of method signatures. 82 | - `expireBlockNumber` - number: The number of blocks until expiration (default 1000). 83 | 84 | - **Returns**: An object containing the success status and transaction receipt. 85 | 86 | - `success` - boolean: true | false 87 | 88 | - `receipt` - object: a web3.js receipt object. Learn more: [receipt](https://web3js.readthedocs.io/en/v1.10.0/web3-eth.html?highlight=receipt#gettransactionreceipt) 89 | 90 | - **Example**: 91 | 92 | ```js 93 | // init your main key 94 | const yourWalletPrivateKey = "0xCAFE....CAFE"; 95 | const account = web3.eth.accounts.privateKeyToAccount(yourWalletPrivateKey); 96 | 97 | // register session key 98 | const testSessionKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 99 | const testContract = "0330032b4a11478969dc4caaa11ecc2ea98cfcFF"; 100 | const testMethods = ["0A0A0A0A", "0B0B0B0B"]; 101 | const testExpireBlockNumber = 2000; 102 | 103 | let ret = await aspectClient.registerSessionKey(account, testSessionKeyAddress, testContract, testMethods, testExpireBlockNumber); 104 | ``` 105 | 106 | ### 2. `registerSessionKeyByMetamask` 107 | 108 | Registers a session key through MetaMask. Call this method will ask for Metamask signatrure. 109 | 110 | - **Parameters**: 111 | 112 | - `accountAddress` - string: The address of the account. 113 | - `sessionKeyAddress` - string: The session key address. 114 | - `bindingContractAddress` - string: The contract address to bind. 115 | - `bindingMethodSigSet` - string[]: An array of method signatures. 116 | - `expireBlockNumber` - number: The number of blocks until expiration (default 1000). 117 | 118 | - **Returns**: Transaction hash. 119 | - **Example**: 120 | 121 | ```js 122 | // init client 123 | const web3 = new Web3(window.ethereum); 124 | const testAspectAddress = "0x06786bB59719d7DDD9D42457a16BbCD6953A7cab"; 125 | let aspectClient = new SessionKeyAspectClient(web3, aspectAddress); 126 | 127 | 128 | await window.ethereum.enable(); 129 | 130 | // call api 131 | const walletAddress = await window.ethereum.request({ method: 'eth_requestAccounts' }); 132 | 133 | const testSessionKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 134 | const testContract = "0330032b4a11478969dc4caaa11ecc2ea98cfcFF"; 135 | const testMethods = ["0A0A0A0A", "0B0B0B0B"]; 136 | const testExpireBlockNumber = 2000; 137 | 138 | await aspectClient.registerSessionKeyByMetamask(walletAddress, testSessionKeyAddress, testContract, testMethods, testExpireBlockNumber); 139 | 140 | ``` 141 | 142 | 143 | 144 | ### 3. `registerSessionKeyUnsignTx` 145 | 146 | Generates an unsigned transaction for registering a session key. Then, the caller signs and sends the transaction by themself. 147 | 148 | - **Parameters**: Similar to `registerSessionKeyByMetamask`. 149 | - **Returns**: Unsigned transaction object. 150 | 151 | - `from` - string: main key address 152 | - `to` - string: Aspect system contract address 153 | - `gas` - number: gas of this tx 154 | - `data` - string: call data of register session key 155 | 156 | - **Example**: 157 | 158 | ```js 159 | 160 | // get unsign tx 161 | let unsignTx = await aspectClient.registerSessionKeyUnsignTx(account.address, testSessionKeyAddress, testContract, testMethods, 20); 162 | console.log(unsignTx); 163 | 164 | // sign and send it 165 | let signedTx = await web3.eth.accounts.signTransaction(unsignTx, account.privateKey); 166 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 167 | console.log(receipt); 168 | 169 | // query from blockchain 170 | sessionKey = await aspectClient.getSessionKey(account.address, testSessionKeyAddress, testContract); 171 | console.log(sessionKey); 172 | ``` 173 | 174 | 175 | 176 | ### 4. `getSessionKey` 177 | 178 | Retrieves the session key. 179 | 180 | - **Parameters**: 181 | - `walletAddress` - string: The wallet address. E.g. `0xCAFE...CAFE` 182 | - `sessionKeyAddress` - string: The session key address. E.g. `0xCAFE...CAFE` 183 | - `bindingContractAddress` - string: The contract address. E.g. `0xCAFE...CAFE` 184 | - **Returns**: Session key object. 185 | 186 | - `walletAddress` - string: the main key 187 | - `sessionKeyAddress` - string: the session key 188 | - `bindingContractAddress` - string: the binding contract of this session key 189 | - `bindingMethodSet` - string[]: the binding contract method set of this session key 190 | - `expireBlockHeight` - number: the expire block height of this session key 191 | 192 | - **Example:** 193 | 194 | ```js 195 | // register session key 196 | const testMainKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 197 | const testSessionKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 198 | const testContract = "0330032b4a11478969dc4caaa11ecc2ea98cfcFF"; 199 | 200 | // query this session key 201 | let sessionKey = await aspectClient.getSessionKey(testMainKeyAddress, testSessionKeyAddress, testContract); 202 | console.log(sessionKey) 203 | ``` 204 | 205 | 206 | 207 | ### 5. `getSessioinKeyExpireHeight` 208 | 209 | Retrieves the expiration height of the session key. 210 | 211 | - **Parameters**: Similar to `getSessionKey`. 212 | 213 | - **Returns**: Number; The block height at which the session key expires. 214 | 215 | 216 | 217 | ### 6. `getAllSessionKey` 218 | 219 | Retrieves all session keys of specific EoA 220 | 221 | - **Parameters**: 222 | - `walletAddress` - string: The EoA address. E.g. `0xCAFE...CAFE` 223 | - **Returns**: Session key object array. The object struct is similar to `getSessionKey`. 224 | 225 | 226 | 227 | ### 7. `bindEoA` 228 | 229 | Bind EoA to the aspect. 230 | 231 | - **Parameters**: 232 | 233 | - `account` - object: The web3.js account object of EoA. Learn more: [web3.eth.accounts](https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#privatekeytoaccount) 234 | 235 | - **Returns**: An object containing the success status and transaction receipt. 236 | 237 | - `success` - boolean: true | false 238 | 239 | - `receipt` - object: a web3.js receipt object. Learn more: [receipt](https://web3js.readthedocs.io/en/v1.10.0/web3-eth.html?highlight=receipt#gettransactionreceipt) 240 | 241 | - **Example**: 242 | 243 | ```js 244 | // init your main key 245 | const yourWalletPrivateKey = "0xCAFE....CAFE"; 246 | const account = web3.eth.accounts.privateKeyToAccount(yourWalletPrivateKey); 247 | 248 | let ret = await aspectClient.bindEoA(account); 249 | ``` 250 | 251 | ### 252 | 253 | ### 8. `unbindEoAByMetamask` 254 | 255 | Bind EoA to the Aspect through MetaMask. Call this method will ask for Metamask signatrure. 256 | 257 | - **Parameters**: 258 | - `accountAddress` - string: The address of the EoA. 259 | - **Returns**: Transaction hash. 260 | 261 | 262 | 263 | ### 9. `unbindEoA` 264 | 265 | Undind EoA from the Aspect. 266 | 267 | - **Parameters**: 268 | 269 | - `account` - object: The web3.js account object of EoA. Learn more: [web3.eth.accounts](https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#privatekeytoaccount) 270 | 271 | - **Returns**: An object containing the success status and transaction receipt. 272 | 273 | - `success` - boolean: true | false 274 | 275 | - `receipt` - object: a web3.js receipt object. Learn more: [receipt](https://web3js.readthedocs.io/en/v1.10.0/web3-eth.html?highlight=receipt#gettransactionreceipt) 276 | 277 | - **Example**: 278 | 279 | ```js 280 | // init your main key 281 | const yourWalletPrivateKey = "0xCAFE....CAFE"; 282 | const account = web3.eth.accounts.privateKeyToAccount(yourWalletPrivateKey); 283 | 284 | let ret = await aspectClient.unbindEoA(account); 285 | ``` 286 | 287 | ### 288 | 289 | ### 10. `unbindEoAByMetamask` 290 | 291 | Unbind EoA from the Aspect through MetaMask. Call this method will ask for Metamask signatrure. 292 | 293 | - **Parameters**: 294 | - `accountAddress` - string: The address of the EoA. 295 | - **Returns**: String; Transaction hash. 296 | 297 | 298 | 299 | ### 11. `bindContract` 300 | 301 | Bind a smart contract to the Aspect. 302 | 303 | - **Parameters**: 304 | 305 | - `account` - object: The owner of the smart contract. It's a web3.js account object of EoA. Learn more: [web3.eth.accounts](https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#privatekeytoaccount) 306 | - `contractAddress` - string: The smart contract address. 307 | 308 | - **Returns**: An object containing the success status and transaction receipt. 309 | 310 | - `success` - boolean: true | false 311 | 312 | - `receipt` - object: a web3.js receipt object. Learn more: [receipt](https://web3js.readthedocs.io/en/v1.10.0/web3-eth.html?highlight=receipt#gettransactionreceipt) 313 | 314 | - **Example**: 315 | 316 | ```js 317 | // init your main key 318 | const yourWalletPrivateKey = "0xCAFE....CAFE"; 319 | const account = web3.eth.accounts.privateKeyToAccount(yourWalletPrivateKey); 320 | 321 | let ret = await aspectClient.bindContract(account); 322 | ``` 323 | 324 | ### 325 | 326 | ### 12. `bindContractByMetamask` 327 | 328 | Bind a smart contract to the Aspect through MetaMask. Call this method will ask for Metamask signatrure. 329 | 330 | - **Parameters**: 331 | - `accountAddress` - string: The owner of the smart contract. 332 | - `contractAddress` - string: The smart contract address. 333 | - **Returns**: Transaction hash. 334 | 335 | - **Example**: 336 | 337 | ```js 338 | // init your main key 339 | const eoaAddress = "0xCAFE....CAFE"; 340 | const smartContractAddress = "0xCAFE....CAFE"; 341 | 342 | let ret = await aspectClient.bindContract(eoaAddress, smartContractAddress); 343 | ``` 344 | 345 | 346 | 347 | ### 13. `ifBinding` 348 | 349 | Query if is a EoA or smart contract bound to the Aspect. 350 | 351 | - **Parameters**: 352 | - `address` - string: The address of EoA or smart contract. E.g. `0xCAFE...CAFE` 353 | - **Returns**: Bool 354 | 355 | 356 | 357 | ### 14. `getBindingAccount` 358 | 359 | Query all address binding to this Aspect. 360 | 361 | - **Returns**: String array of address 362 | 363 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/index.js: -------------------------------------------------------------------------------- 1 | const SessionKeyAspectClient = require('./src/aspect_client'); 2 | 3 | module.exports = SessionKeyAspectClient; 4 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artela/sessioin-key-aspect-client", 3 | "version": "1.0.4", 4 | "description": "session key aspect client that provides session key management functionality", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack --mode production" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/artela-network/session-key-aspect.git" 13 | }, 14 | "keywords": [ 15 | "artela", 16 | "session", 17 | "key", 18 | "aspect" 19 | ], 20 | "author": "cp", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/artela-network/session-key-aspect/issues" 24 | }, 25 | "homepage": "https://github.com/artela-network/session-key-aspect#readme", 26 | "dependencies": { 27 | "@artela/web3": "^1.9.22", 28 | "buffer": "^6.0.3", 29 | "web3": "^4.3.0", 30 | "@ethereumjs/tx": "^5.1.0" 31 | }, 32 | "devDependencies": { 33 | "webpack": "^5.89.0", 34 | "webpack-cli": "^5.1.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/src/aspect_client.js: -------------------------------------------------------------------------------- 1 | const {Buffer} = require('buffer'); 2 | const {BigNumber} = require('bignumber.js'); 3 | const {LegacyTransaction:EthereumTx} = require('@ethereumjs/tx') 4 | 5 | class SessionKeyAspectClient { 6 | 7 | constructor(web3client, aspectAddress) { 8 | this.web3 = web3client; 9 | 10 | this.aspectCore = this.web3.atl.aspectCore(); 11 | this.aspect = new this.web3.atl.Aspect(); 12 | this.aspect.options.address = aspectAddress; 13 | } 14 | 15 | async registerSessionKey(account, sessionKeyAddress, bindingContractAddress, bindingMethodSigSet, expireBlockNumber = 1000) { 16 | 17 | let tx = await this.registerSessionKeyUnsignTx(account.address, sessionKeyAddress, bindingContractAddress, bindingMethodSigSet, expireBlockNumber); 18 | 19 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, account.privateKey); 20 | let receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); 21 | 22 | return { 23 | success: receipt.status, 24 | receipt: receipt 25 | }; 26 | } 27 | 28 | async registerSessionKeyByMetamask(walletAddress, sessionKeyAddress, bindingContractAddress, bindingMethodSigSet, expireBlockNumber = 1000) { 29 | 30 | let tx = await this.registerSessionKeyUnsignTx(walletAddress, sessionKeyAddress, bindingContractAddress, bindingMethodSigSet, expireBlockNumber); 31 | 32 | let metamaskTx = { 33 | from: tx.from, 34 | to: tx.to, 35 | value: "0x00", 36 | gas: "0x" + tx.gas.toString(16), 37 | data: tx.data 38 | } 39 | 40 | console.log("metamaskTx:", metamaskTx); 41 | let txHash = await this.web3.eth.sendTransaction(metamaskTx); 42 | 43 | return txHash; 44 | } 45 | 46 | async registerSessionKeyUnsignTx(walletAddress, sessionKeyAddress, bindingContractAddress, bindingMethodSigSet, expireBlockNumber = 1000) { 47 | 48 | sessionKeyAddress = this.rmPrefix(sessionKeyAddress); 49 | bindingContractAddress = this.rmPrefix(bindingContractAddress); 50 | 51 | let methodSetSize = this.uint8ToHex(bindingMethodSigSet.length); 52 | let currentBlockHeight = await this.web3.eth.getBlockNumber(); 53 | let expireBlockHeight = currentBlockHeight + expireBlockNumber; 54 | 55 | let op = "0x0001"; 56 | let params = 57 | sessionKeyAddress 58 | + bindingContractAddress 59 | + methodSetSize + this.processAndConcatStrings(bindingMethodSigSet) 60 | + this.rmPrefix(this.web3.eth.abi.encodeParameter('uint256', expireBlockHeight)).slice(48, 64); 61 | 62 | let calldata = this.aspect.operation(op + params).encodeABI(); 63 | 64 | let tx = { 65 | from: walletAddress, 66 | to: this.aspectCore.options.address, 67 | gas: 8000000, 68 | data: calldata 69 | } 70 | 71 | return tx; 72 | } 73 | 74 | async createUnsignTx(walletAddress, sKeyPrivKey, contractCallData, toAddress, value= 0) { 75 | let sKeyAccount = this.web3.eth.accounts.privateKeyToAccount(sKeyPrivKey); 76 | let gasPrice = await this.web3.eth.getGasPrice(); 77 | let nonce = await this.web3.eth.getTransactionCount(walletAddress); 78 | let chainId = await this.web3.eth.getChainId(); 79 | 80 | let gas = 8000000; 81 | let gasLimit = 8000000; 82 | 83 | let tx = { 84 | from: sKeyAccount.address, 85 | nonce: nonce, 86 | gasPrice, 87 | gas: gas, 88 | data: contractCallData, 89 | to: toAddress, 90 | chainId, 91 | gasLimit: gasLimit, 92 | value: value ? value : 0 93 | } 94 | 95 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, sKeyPrivKey); 96 | 97 | let validationData = "0x" 98 | + walletAddress.slice(2) 99 | + this.padStart(this.rmPrefix(signedTx.r), 64, "0") 100 | + this.padStart(this.rmPrefix(signedTx.s), 64, "0") 101 | + this.rmPrefix(this.getOriginalV(signedTx.v, chainId)); 102 | 103 | let encodedData = this.web3.eth.abi.encodeParameters(['bytes', 'bytes'], 104 | [validationData, contractCallData]); 105 | 106 | // new calldata: magic prefix + checksum(encodedData) + encodedData(validation data + raw calldata) 107 | // 0xCAFECAFE is a magic prefix, 108 | encodedData = '0xCAFECAFE' + this.web3.utils.keccak256(encodedData).slice(2, 10) + encodedData.slice(2); 109 | 110 | console.log("encodedData:", encodedData); 111 | 112 | tx = { 113 | from: walletAddress, 114 | nonce: this.toPaddedHexString(nonce), 115 | gasPrice: this.toPaddedHexString(gasPrice), 116 | gas: this.toPaddedHexString(gas), 117 | data: encodedData, 118 | to: toAddress, 119 | chainId: this.toPaddedHexString(chainId), 120 | gasLimit: this.toPaddedHexString(gasLimit), 121 | value: this.toPaddedHexString(value) 122 | } 123 | 124 | return '0x' + this.bytesToHex(EthereumTx.fromTxData(tx).serialize()); 125 | } 126 | 127 | async getSessionKey(walletAddress, sessionKeyAddress, bindingContractAddress) { 128 | 129 | walletAddress = this.rmPrefix(walletAddress); 130 | sessionKeyAddress = this.rmPrefix(sessionKeyAddress); 131 | bindingContractAddress = this.rmPrefix(bindingContractAddress); 132 | 133 | let op = "0x1001"; 134 | let queryKey = this.web3.utils.keccak256("0x" + bindingContractAddress + walletAddress + sessionKeyAddress); 135 | let params = this.rmPrefix(queryKey); 136 | let calldata = this.aspect.operation(op + params).encodeABI(); 137 | 138 | let ret = await this.web3.eth.call({ 139 | to: this.aspectCore.options.address, // contract address 140 | data: calldata 141 | }); 142 | 143 | let encodeKey = this.web3.eth.abi.decodeParameter('string', ret); 144 | 145 | return this.decodeSessionKey(encodeKey); 146 | } 147 | 148 | async getAllSessionKey(walletAddress) { 149 | 150 | walletAddress = this.rmPrefix(walletAddress); 151 | 152 | let op = "0x1005"; 153 | let params = this.rmPrefix(walletAddress); 154 | let calldata = this.aspect.operation(op + params).encodeABI(); 155 | 156 | let ret = await this.web3.eth.call({ 157 | to: this.aspectCore.options.address, // contract address 158 | data: calldata 159 | }); 160 | 161 | let encodeKey = this.web3.eth.abi.decodeParameter('string', ret); 162 | 163 | encodeKey = encodeKey.slice(4); 164 | 165 | let sessionKeys = new Array(); 166 | while (encodeKey.length != 0) { 167 | let popMethodSize = parseInt(encodeKey.slice(80, 84), 16); 168 | let currentEncodeKeySize = 84 + 8 * popMethodSize + 16 + 40; 169 | let popEncodeKey = encodeKey.slice(0, currentEncodeKeySize); 170 | encodeKey = encodeKey.slice(currentEncodeKeySize); 171 | 172 | sessionKeys.push(this.decodeSessionKey(popEncodeKey)); 173 | } 174 | 175 | return sessionKeys; 176 | } 177 | 178 | async getSessioinKeyExpireHeight(walletAddress, sessionKeyAddress, bindingContractAddress) { 179 | let sessionKey = await this.getSessionKey(walletAddress, sessionKeyAddress, bindingContractAddress); 180 | return sessionKey.expireBlockHeight 181 | } 182 | 183 | async bindEoA(account) { 184 | let eoaBindingData = await this.aspectCore.methods.bind(this.aspect.options.address, 1, account.address, 1).encodeABI(); 185 | 186 | let tx = { 187 | from: account.address, 188 | to: this.aspectCore.options.address, 189 | gas: 4000000, 190 | data: eoaBindingData, 191 | } 192 | 193 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, account.privateKey); 194 | let receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); 195 | 196 | return { 197 | success: receipt.status, 198 | receipt: receipt 199 | } 200 | } 201 | 202 | async bindEoAByMetamask(walletAddress) { 203 | let eoaBindingData = await this.aspectCore.methods.bind(this.aspect.options.address, 1, walletAddress, 1).encodeABI(); 204 | 205 | let gas = 4000000; 206 | let metamaskTx = { 207 | from: walletAddress, 208 | to: this.aspectCore.options.address, 209 | // value: "0x00", 210 | gas: "0x" + gas.toString(16), 211 | data: eoaBindingData, 212 | // nonce: 0 213 | } 214 | 215 | console.log("metamaskTx:", metamaskTx); 216 | let txHash = await this.web3.eth.sendTransaction(metamaskTx); 217 | console.log("txHash:", txHash); 218 | 219 | return txHash; 220 | } 221 | 222 | async unbindEoA(account) { 223 | let eoaBindingData = await this.aspectCore.methods.unbind(this.aspect.options.address, account.address).encodeABI(); 224 | console.log(eoaBindingData); 225 | let tx = { 226 | from: account.address, 227 | to: this.aspectCore.options.address, 228 | gas: 4000000, 229 | data: eoaBindingData, 230 | } 231 | 232 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, account.privateKey); 233 | let receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); 234 | console.log(receipt); 235 | 236 | return { 237 | success: receipt.status, 238 | receipt: receipt 239 | } 240 | } 241 | 242 | async unbindEoAByMetamask(walletAddress) { 243 | let eoaBindingData = await this.aspectCore.methods.unbind(this.aspect.options.address, walletAddress).encodeABI(); 244 | 245 | let gas = 4000000; 246 | let metamaskTx = { 247 | from: walletAddress, 248 | to: this.aspectCore.options.address, 249 | value: "0x00", 250 | gas: "0x" + gas.toString(16), 251 | data: eoaBindingData 252 | } 253 | 254 | console.log("metamaskTx:", metamaskTx); 255 | let txHash = await this.web3.eth.sendTransaction(metamaskTx); 256 | 257 | return txHash; 258 | } 259 | 260 | async ifBinding(address) { 261 | let ret = await this.aspectCore.methods.boundAddressesOf(this.aspect.options.address).call({}); 262 | 263 | return ret.map(str => str.toLowerCase()).includes(address.toLowerCase()); 264 | } 265 | 266 | async getBindingAccount() { 267 | let ret = await this.aspectCore.methods.boundAddressesOf(this.aspect.options.address).call({}); 268 | 269 | return ret; 270 | } 271 | 272 | async bindContract(account, contractAddress) { 273 | let eoaBindingData = await this.aspectCore.methods.bind(this.aspect.options.address, 1, contractAddress, 1).encodeABI(); 274 | 275 | let tx = { 276 | from: account.address, 277 | to: this.aspectCore.options.address, 278 | gas: 4000000, 279 | data: eoaBindingData, 280 | } 281 | 282 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, account.privateKey); 283 | let receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); 284 | 285 | return { 286 | success: receipt.status, 287 | receipt: receipt 288 | } 289 | } 290 | 291 | async bindContractByMetamask(walletAddress, contractAddress) { 292 | let eoaBindingData = await this.aspectCore.methods.bind(this.aspect.options.address, 1, contractAddress, 1).encodeABI(); 293 | 294 | let gas = 4000000; 295 | let metamaskTx = { 296 | from: walletAddress, 297 | to: this.aspectCore.options.address, 298 | value: "0x00", 299 | gas: "0x" + gas.toString(16), 300 | data: eoaBindingData 301 | } 302 | 303 | console.log("metamaskTx:", metamaskTx); 304 | let txHash = await this.web3.eth.sendTransaction(metamaskTx); 305 | 306 | return txHash; 307 | } 308 | 309 | async unbindContract(account, contractAddress) { 310 | let eoaBindingData = await this.aspectCore.methods.unbind(this.aspect.options.address, contractAddress).encodeABI(); 311 | 312 | let tx = { 313 | from: account.address, 314 | to: this.aspectCore.options.address, 315 | gas: 4000000, 316 | data: eoaBindingData, 317 | } 318 | 319 | let signedTx = await this.web3.eth.accounts.signTransaction(tx, account.privateKey); 320 | let receipt = await this.web3.eth.sendSignedTransaction(signedTx.rawTransaction); 321 | 322 | return { 323 | success: receipt.status, 324 | receipt: receipt 325 | } 326 | } 327 | 328 | async unbindContractByMetamask(walletAddress, contractAddress) { 329 | let eoaBindingData = await this.aspectCore.methods.unbind(this.aspect.options.address, contractAddress).encodeABI(); 330 | 331 | let gas = 4000000; 332 | let metamaskTx = { 333 | from: walletAddress, 334 | to: this.aspectCore.options.address, 335 | value: "0x00", 336 | gas: "0x" + gas.toString(16), 337 | data: eoaBindingData 338 | } 339 | 340 | console.log("metamaskTx:", metamaskTx); 341 | let txHash = await this.web3.eth.sendTransaction(metamaskTx); 342 | 343 | return txHash; 344 | } 345 | 346 | decodeSessionKey(encodeKey) { 347 | // * SessionKey encode rules: 348 | // * 20 bytes: session key public key 349 | // * eg. 1f9090aaE28b8a3dCeaDf281B0F12828e676c326 350 | // * 20 bytes: contract address 351 | // * eg. 388C818CA8B9251b393131C08a736A67ccB19297 352 | // * 2 bytes: length of methods set 353 | // * eg. 0002 354 | // * variable-length: 4 bytes * length of methods set; methods set 355 | // * eg. 0a0a0a0a0b0b0b0b 356 | // * means there are two methods: ['0a0a0a0a', '0b0b0b0b'] 357 | // * 8 bytes: expire block height 358 | // * 20 bytes: main key 359 | // * eg. 388C818CA8B9251b393131C08a736A67ccB19297 360 | 361 | if (encodeKey.length == 0) { 362 | return { 363 | walletAddress: "", 364 | sessionKeyAddress: "", 365 | bindingContractAddress: "", 366 | bindingMethodSet: [], 367 | expireBlockHeight: 0 368 | } 369 | } 370 | 371 | if (encodeKey.length < 84) { 372 | throw new Error('illegal encode session key, length is :' + encodeKey.length); 373 | } 374 | 375 | let sessionKeyAddress = "0x" + encodeKey.slice(0, 40); 376 | let bindingContractAddress = "0x" + encodeKey.slice(40, 80); 377 | let methodSize = parseInt(encodeKey.slice(80, 84), 16); 378 | 379 | if (encodeKey.length < (84 + 8 * methodSize + 16 + 40)) { 380 | throw new Error('illegal encode session key, length is :' + encodeKey.length); 381 | } 382 | 383 | let expireBlockHeightPosition = 84 + 8 * methodSize; 384 | let bindingMethodSet = this.decodEncodeMethods(encodeKey.slice(84, expireBlockHeightPosition)); 385 | let expireBlockHeight = parseInt(encodeKey.slice(expireBlockHeightPosition, expireBlockHeightPosition + 16), 16); 386 | let walletAddress = "0x" + encodeKey.slice(expireBlockHeightPosition + 16); 387 | 388 | return { 389 | walletAddress: walletAddress, 390 | sessionKeyAddress: sessionKeyAddress, 391 | bindingContractAddress: bindingContractAddress, 392 | bindingMethodSet: bindingMethodSet, 393 | expireBlockHeight: expireBlockHeight 394 | } 395 | } 396 | 397 | decodEncodeMethods(str) { 398 | const result = []; 399 | for (let i = 0; i < str.length; i += 8) { 400 | result.push("0x" + str.substring(i, i + 8)); 401 | } 402 | return result; 403 | } 404 | 405 | rmPrefix(str) { 406 | return str.startsWith('0x') ? str.substring(2) : str; 407 | } 408 | 409 | getOriginalV(hexV, chainId_) { 410 | const v = new BigNumber(hexV, 16); 411 | const chainId = new BigNumber(chainId_); 412 | const chainIdMul = chainId.multipliedBy(2); 413 | 414 | const originalV = v.minus(chainIdMul).minus(8); 415 | 416 | const originalVHex = originalV.toString(16); 417 | 418 | return originalVHex; 419 | } 420 | 421 | processAndConcatStrings(strings) { 422 | return strings.map(str => { 423 | const processedStr = this.rmPrefix(str); 424 | if (processedStr.length !== 8) { 425 | throw new Error(`Processed string length is not 8: ${processedStr}`); 426 | } 427 | return processedStr; 428 | }).join(''); 429 | } 430 | 431 | uint8ToHex(value) { 432 | if (value < 0 || value > 255) { 433 | throw new Error('Value is out of range for a uint8'); 434 | } 435 | 436 | let buffer = Buffer.alloc(2); 437 | buffer.writeInt16BE(value, 0); 438 | return buffer.toString('hex').padStart(4, '0'); 439 | } 440 | 441 | padStart(str, targetLength, padString) { 442 | targetLength = Math.max(targetLength, str.length); 443 | padString = String(padString || ' '); 444 | 445 | if (str.length >= targetLength) { 446 | return str; 447 | } else { 448 | targetLength = targetLength - str.length; 449 | if (targetLength > padString.length) { 450 | padString += padString.repeat(targetLength / padString.length); 451 | } 452 | return padString.slice(0, targetLength) + str; 453 | } 454 | } 455 | 456 | bytesToHex(bytes) { 457 | return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); 458 | } 459 | 460 | toPaddedHexString(num) { 461 | let hex = num.toString(16); 462 | 463 | if (hex.length % 2 !== 0) { 464 | hex = '0' + hex; 465 | } 466 | 467 | return '0x' + hex; 468 | } 469 | } 470 | 471 | module.exports = SessionKeyAspectClient; 472 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@artela/web3": "^1.9.22", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "buffer": "^6.0.3", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4", 15 | "sessioin-key-aspect-client": "^1.0.2" 16 | }, 17 | "devDependencies": { 18 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/client/public/favicon.ico -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Session-key Aspect 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/client/public/logo192.png -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/client/public/logo512.png -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | align-items: flex-start; 6 | gap: 20px; /* 两个元素之间的间隔 */ 7 | width: 1000px; 8 | margin: auto; /* 在页面中居中 */ 9 | margin-bottom: 80px; 10 | } 11 | 12 | .center-container-aspect { 13 | display: flex; /* 使用 Flexbox 布局 */ 14 | justify-content: center; /* 水平居中 */ 15 | align-items: center; /* 垂直居中 */ 16 | flex-direction: column; /* 子元素垂直排列 */ 17 | 18 | /* 边框和圆角样式 */ 19 | border: 2px solid #000000; /* 黑色边框 */ 20 | border-radius: 10px; /* 圆角 */ 21 | 22 | /* 宽度和高度根据内容自适应 */ 23 | width: 1000px; 24 | height: auto; 25 | 26 | /* 其他样式 */ 27 | margin: auto; /* 在页面中居中 */ 28 | margin-top: 20px; 29 | } 30 | 31 | .center-container { 32 | display: flex; /* 使用 Flexbox 布局 */ 33 | justify-content: center; /* 水平居中 */ 34 | align-items: center; /* 垂直居中 */ 35 | flex-direction: column; /* 子元素垂直排列 */ 36 | 37 | /* 边框和圆角样式 */ 38 | border: 2px solid #000000; /* 黑色边框 */ 39 | border-radius: 10px; /* 圆角 */ 40 | 41 | /* 保留大部分样式不变,只调整 width 和 margin */ 42 | width: 480px; 43 | 44 | /* 其他样式 */ 45 | margin-top: 20px; 46 | } 47 | 48 | .input-container { 49 | margin-bottom: 20px; /* 增加输入框底部的外边距 */ 50 | } 51 | 52 | .button-container { 53 | margin-top: 20px; /* 增加按钮顶部的外边距 */ 54 | } 55 | 56 | .session-key-input { 57 | padding: 8px; 58 | border: 1px solid #ccc; 59 | border-radius: 4px; 60 | width: 300px; /* 可以调整输入框的宽度 */ 61 | } 62 | 63 | .register-button { 64 | padding: 10px 20px; 65 | font-size: 20px; 66 | cursor: pointer; 67 | background-color: #4CAF50; 68 | color: white; 69 | border: none; 70 | border-radius: 5px; 71 | } 72 | 73 | .title { 74 | font-weight: bold; 75 | font-size: 20px; 76 | } 77 | 78 | .title-h1 { 79 | font-weight: bold; 80 | color: #4CAF50; 81 | font-size: 26px; 82 | } 83 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Web3 from '@artela/web3'; 3 | import './App.css'; 4 | 5 | const SessionKeyAspectClient = require('sessioin-key-aspect-client'); 6 | 7 | const App = () => { 8 | 9 | const testMethods = "0A0A0A0A,0B0B0B0B"; 10 | const testAspectAddress = "0x9639aEa2F55E44a5B352A62C80976faE574b7d97"; 11 | 12 | const [aspectAddress, setAspectAddress] = useState(testAspectAddress); 13 | const [walletAddress, setWalletAddress] = useState(''); 14 | const [sessionKey, setSessionKey] = useState(generateRandomEthereumAddress()); 15 | const [contractAddress, setContractAddress] = useState(generateRandomEthereumAddress()); 16 | const [methods, setMethods] = useState(testMethods); 17 | const [syncData, setSyncData] = useState({ 18 | walletAddress: '', 19 | sessionKeyAddress: '', 20 | bindingContractAddress: '', 21 | bindingMethodSet: [], 22 | expireBlockHeight: '' 23 | }); 24 | 25 | const [mainKeyQP, setMainKeyQP] = useState(''); 26 | const [sessionKeyQP, setSessionKeyQP] = useState(''); 27 | const [contractQP, setContractQP] = useState(''); 28 | const [queryResultQP, setQueryResultQP] = useState({}); 29 | const [queryBindingResult, setQueryBindingResult] = useState("-"); 30 | const [contractC, setContractC] = useState(""); 31 | const [queryBindingResultC, setQueryBindingResultC] = useState("-"); 32 | const [allSessionKeys, setAllSessionKeys] = useState("[]"); 33 | 34 | const web3 = new Web3(window.ethereum); 35 | let aspectClient = new SessionKeyAspectClient(web3, aspectAddress); 36 | 37 | useEffect(() => { 38 | aspectClient = new SessionKeyAspectClient(web3, aspectAddress); 39 | 40 | const getAccount = async () => { 41 | if (window.ethereum) { 42 | try { 43 | const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); 44 | if (accounts.length > 0) { 45 | setWalletAddress(accounts[0]); 46 | } else { 47 | console.error('No accounts found'); 48 | } 49 | } catch (error) { 50 | console.error('Error connecting to MetaMask:', error); 51 | } 52 | } else { 53 | console.error('MetaMask not available'); 54 | } 55 | }; 56 | 57 | getAccount(); 58 | 59 | const syncUp = async () => { 60 | const data = await aspectClient.getSessionKey(walletAddress, sessionKey, contractAddress); 61 | setSyncData(data); 62 | }; 63 | 64 | const intervalId = setInterval(() => { 65 | syncUp(); 66 | }, 1000); 67 | 68 | return () => clearInterval(intervalId); 69 | }, [sessionKey, walletAddress, contractAddress, aspectAddress]); 70 | 71 | const handleQuery = async () => { 72 | const data = await aspectClient.getSessionKey(mainKeyQP, sessionKeyQP, contractQP); 73 | setQueryResultQP(data); 74 | }; 75 | 76 | function generateRandomEthereumAddress() { 77 | let address = '0x'; 78 | const characters = '0123456789abcdef'; 79 | for (let i = 0; i < 40; i++) { 80 | address += characters.charAt(Math.floor(Math.random() * characters.length)); 81 | } 82 | return address; 83 | } 84 | 85 | const handleRegister = async () => { 86 | try { 87 | await window.ethereum.enable(); 88 | 89 | let testMethods = methods.split(",").map(method => method.trim()) 90 | 91 | await aspectClient.registerSessionKeyByMetamask(walletAddress, sessionKey, contractAddress, testMethods, 20); 92 | 93 | } catch (error) { 94 | console.error('registerSessionKeyByMetamask fail', error); 95 | } 96 | }; 97 | 98 | const handleQueryBinding = async () => { 99 | try { 100 | 101 | let result = await aspectClient.ifBinding(walletAddress) 102 | 103 | setQueryBindingResult(result ? "true" : "false"); 104 | 105 | } catch (error) { 106 | console.error('handleQueryBinding fail', error); 107 | } 108 | }; 109 | 110 | const handleBind = async () => { 111 | try { 112 | await aspectClient.bindEoAByMetamask(walletAddress); 113 | 114 | } catch (error) { 115 | console.error('bindEoAByMetamask fail', error); 116 | } 117 | }; 118 | 119 | const handleUnBind = async () => { 120 | try { 121 | await aspectClient.unbindEoAByMetamask(walletAddress); 122 | } catch (error) { 123 | console.error('unbindEoAByMetamask fail', error); 124 | } 125 | }; 126 | 127 | const handleQueryBindingC = async () => { 128 | try { 129 | 130 | let result = await aspectClient.ifBinding(contractC) 131 | 132 | setQueryBindingResultC(result ? "true" : "false"); 133 | 134 | } catch (error) { 135 | console.error('handleQueryBindingC fail', error); 136 | } 137 | }; 138 | 139 | const handleBindC = async () => { 140 | try { 141 | await aspectClient.bindContractByMetamask(walletAddress, contractC); 142 | } catch (error) { 143 | console.error('bindContractByMetamask fail', error); 144 | } 145 | }; 146 | 147 | const handleUnBindC = async () => { 148 | try { 149 | await aspectClient.unbindContractByMetamask(walletAddress, contractC); 150 | } catch (error) { 151 | console.error('unbindContractByMetamask fail', error); 152 | } 153 | }; 154 | 155 | const handleGetAllSessionKeys = async () => { 156 | try { 157 | let ret = await aspectClient.getAllSessionKey(walletAddress); 158 | setAllSessionKeys(JSON.stringify(ret, null, 2)); 159 | } catch (error) { 160 | console.error('handleGetAllSessionKeys fail', error); 161 | } 162 | }; 163 | 164 | return ( 165 |
166 |
167 |

Session Key Aspect

168 |
169 | { 173 | setAspectAddress(e.target.value) 174 | }} 175 | className="session-key-input" 176 | /> 177 |
178 |
179 |
180 |
181 |

Query session key

182 |

param1: main key address

183 | setMainKeyQP(e.target.value)} 188 | className="session-key-input" 189 | /> 190 |

param2: session key address

191 | setSessionKeyQP(e.target.value)} 196 | className="session-key-input" 197 | /> 198 |

param3: contract address

199 | setContractQP(e.target.value)} 204 | className="session-key-input" 205 | /> 206 |

207 | 208 |
209 |
{JSON.stringify(queryResultQP, null, 2)}
210 |
211 |
212 |
213 |

Test registered by Metamask

214 |

Connected Wallet

215 |

{walletAddress}

216 |

Random session key

217 |
218 | { 222 | console.log("e.target.value", e.target.value); 223 | setSessionKey(e.target.value) 224 | }} 225 | className="session-key-input" 226 | /> 227 |
228 |

Binding contract and methods

229 |
230 | { 234 | console.log("e.target.value", e.target.value); 235 | setContractAddress(e.target.value) 236 | }} 237 | className="session-key-input" 238 | /> 239 |
240 |
241 | { 245 | console.log("e.target.value", e.target.value); 246 | setMethods(e.target.value) 247 | }} 248 | className="session-key-input" 249 | /> 250 |
251 |
252 | 253 |
254 |

Query this session key from blockchain

255 |
256 |
{JSON.stringify(syncData, null, 2)}
257 |
258 |
259 |
260 |

Bind EoA to Aspect

261 |

Connected Wallet

262 |

{walletAddress}

263 |
264 | 265 |
266 |
267 | 268 |
269 |
270 | 271 |
272 |

Result

273 |
274 |
{queryBindingResult}
275 |
276 |
277 |
278 |

Bind contract to Aspect

279 |

Connected Wallet

280 |

{walletAddress}

281 |

params 1: contract address

282 |
283 | { 288 | setContractC(e.target.value) 289 | }} 290 | className="session-key-input" 291 | /> 292 |
293 |
294 | 295 |
296 |
297 | 298 |
299 |
300 | 301 |
302 |

Result

303 |
304 |
{queryBindingResultC}
305 |
306 |
307 |
308 |

Your session keys

309 |

Connected Wallet

310 |

{walletAddress}

311 |
312 | 313 |
314 |

Result

315 |
316 |
{allSessionKeys}
317 |
318 |
319 |
320 |
321 | 322 | ); 323 | } 324 | 325 | export default App; 326 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@artela/web3": "^1.9.22", 7 | "@artela/web3-utils": "^1.9.22", 8 | "@ethereumjs/tx": "^5.1.0", 9 | "@rainbow-me/rainbowkit": "^1.3.2", 10 | "@testing-library/jest-dom": "^5.17.0", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "assert": "^2.1.0", 14 | "bignumber.js": "^9.1.2", 15 | "buffer": "^6.0.3", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-scripts": "^5.0.1", 19 | "sessioin-key-aspect-client": "file:../../../../js_client/session_key_aspect_client/", 20 | "stream-browserify": "^3.0.0", 21 | "typescript": "^5.0.2", 22 | "viem": "^1.21.3", 23 | "wagmi": "^1.4.12", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "overrides": { 39 | "react-scripts": { 40 | "typescript": "^5" 41 | } 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | } 55 | } -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/demo/public/favicon.ico -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Session-key Aspect 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/demo/public/logo192.png -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artela-network/session-key-aspect/67a7f8c648b79861c89e2aab197cc912825df84b/js_client/session_key_aspect_client/test/demo/public/logo512.png -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | align-items: flex-start; 6 | gap: 20px; /* 两个元素之间的间隔 */ 7 | width: 1000px; 8 | margin: auto; /* 在页面中居中 */ 9 | margin-bottom: 10px; 10 | } 11 | 12 | .center-container-aspect { 13 | display: flex; /* 使用 Flexbox 布局 */ 14 | justify-content: center; /* 水平居中 */ 15 | align-items: center; /* 垂直居中 */ 16 | flex-direction: column; /* 子元素垂直排列 */ 17 | 18 | /* 边框和圆角样式 */ 19 | border: 2px solid #000000; /* 黑色边框 */ 20 | border-radius: 10px; /* 圆角 */ 21 | 22 | /* 宽度和高度根据内容自适应 */ 23 | width: 1000px; 24 | height: auto; 25 | 26 | /* 其他样式 */ 27 | margin: auto; /* 在页面中居中 */ 28 | margin-top: 20px; 29 | } 30 | 31 | .center-container { 32 | display: flex; /* 使用 Flexbox 布局 */ 33 | justify-content: center; /* 水平居中 */ 34 | align-items: center; /* 垂直居中 */ 35 | flex-direction: column; /* 子元素垂直排列 */ 36 | 37 | /* 边框和圆角样式 */ 38 | border: 2px solid #000000; /* 黑色边框 */ 39 | border-radius: 10px; /* 圆角 */ 40 | 41 | /* 保留大部分样式不变,只调整 width 和 margin */ 42 | width: 480px; 43 | 44 | /* 其他样式 */ 45 | margin-top: 20px; 46 | } 47 | 48 | .input-container { 49 | margin-bottom: 20px; /* 增加输入框底部的外边距 */ 50 | } 51 | 52 | .button-container { 53 | margin-top: 20px; /* 增加按钮顶部的外边距 */ 54 | } 55 | 56 | .session-key-input { 57 | padding: 8px; 58 | border: 1px solid #ccc; 59 | border-radius: 4px; 60 | width: 300px; /* 可以调整输入框的宽度 */ 61 | } 62 | 63 | .register-button { 64 | padding: 10px 20px; 65 | font-size: 20px; 66 | cursor: pointer; 67 | background-color: #4CAF50; 68 | color: white; 69 | border: none; 70 | border-radius: 5px; 71 | } 72 | 73 | .register-button.disabled { 74 | /* 禁用状态的按钮样式 */ 75 | background-color: grey; 76 | cursor: not-allowed; 77 | } 78 | 79 | .title { 80 | font-weight: bold; 81 | font-size: 20px; 82 | } 83 | 84 | .title-h1 { 85 | font-weight: bold; 86 | color: #4CAF50; 87 | font-size: 26px; 88 | } 89 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Web3 from '@artela/web3'; 3 | import { LegacyTransaction as EthereumTx } from '@ethereumjs/tx' 4 | import BigNumber from 'bignumber.js'; 5 | import { ConnectButton } from '@rainbow-me/rainbowkit'; 6 | 7 | import './App.css'; 8 | 9 | 10 | const SessionKeyAspectClient = require('sessioin-key-aspect-client'); 11 | 12 | const App = () => { 13 | 14 | const testContract = "0xA19A094b2D912D9FEB42Ce4148773Ecf0fc98EAa"; 15 | const testContractAbi = [{ "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [{ "internalType": "uint256", "name": "number", "type": "uint256" }], "name": "add", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "get", "outputs": [{ "internalType": "uint256", "name": "result", "type": "uint256" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "address", "name": "user", "type": "address" }], "name": "isOwner", "outputs": [{ "internalType": "bool", "name": "result", "type": "bool" }], "stateMutability": "view", "type": "function" }]; 16 | const testMethods = "0x1003e2d2"; 17 | const testAspectAddress = "0xA901631341c52De56F4DFA571dcBaEb0b13017FD"; 18 | 19 | const [aspectAddress, setAspectAddress] = useState(testAspectAddress); 20 | 21 | const [walletAddress, setWalletAddress] = useState(''); 22 | const [counter, setCounter] = useState('-'); 23 | const [isAddButtonDisabled, setIsAddButtonDisabled] = useState(false); 24 | const [isAddButton2Disabled, setIsAddButton2Disabled] = useState(false); 25 | 26 | // unbind > empty > active > expired 27 | const [sessionKeyStatus, setSessionKeyStatus] = useState("unbind"); 28 | const [expiredHeight, setExpiredHeight] = useState("-"); 29 | 30 | const metamask = new Web3(window.ethereum); 31 | const web3Artela = new Web3("https://betanet-rpc1.artela.network"); 32 | // const web3Artela = new Web3("http://127.0.0.1:8545"); 33 | let aspectClient = new SessionKeyAspectClient(metamask, aspectAddress); 34 | let aspectClientArtela = new SessionKeyAspectClient(web3Artela, aspectAddress); 35 | 36 | const contract = new metamask.eth.Contract(testContractAbi, testContract); 37 | 38 | useEffect(() => { 39 | 40 | aspectClient = new SessionKeyAspectClient(metamask, aspectAddress); 41 | aspectClientArtela = new SessionKeyAspectClient(web3Artela, aspectAddress); 42 | 43 | syncUp(); 44 | syncSKeyStatus(); 45 | }, [counter, walletAddress, isAddButtonDisabled, expiredHeight]); 46 | 47 | const syncUp = async () => { 48 | let ret = await web3Artela.eth.call({ 49 | data: contract.methods.get().encodeABI(), 50 | to: contract.options.address, 51 | }) 52 | 53 | let data = web3Artela.eth.abi.decodeParameter('uint256', ret); 54 | 55 | setCounter(data); 56 | }; 57 | 58 | // sessionKeyStatus 59 | // unbind > empty > active > expired 60 | const syncSKeyStatus = async () => { 61 | let isBinding = await aspectClient.ifBinding(walletAddress); 62 | 63 | if (!isBinding) { 64 | setSessionKeyStatus("unbind") 65 | return; 66 | } 67 | 68 | const storeKey = loadFromLocalStorage(walletAddress); 69 | if (!storeKey) { 70 | setSessionKeyStatus("empty") 71 | return; 72 | } 73 | 74 | let sKeyAccount = web3Artela.eth.accounts.privateKeyToAccount(storeKey); 75 | let height = await aspectClient.getSessioinKeyExpireHeight(walletAddress, sKeyAccount.address, testContract); 76 | 77 | setExpiredHeight(height.toString()); 78 | 79 | if (height < await web3Artela.eth.getBlockNumber()) { 80 | setSessionKeyStatus("expired") 81 | return; 82 | } else { 83 | setSessionKeyStatus("active") 84 | return; 85 | } 86 | }; 87 | 88 | const saveToLocalStorage = (wallet, input) => { 89 | localStorage.setItem('session_key_for_counter_contract' + wallet, input); 90 | }; 91 | 92 | const loadFromLocalStorage = (wallet) => { 93 | const storedData = localStorage.getItem('session_key_for_counter_contract' + wallet); 94 | return storedData; 95 | }; 96 | 97 | const handleAdd = async () => { 98 | 99 | if (undefined == walletAddress || "" == walletAddress) { 100 | await configYourMetamask(); 101 | return; 102 | } 103 | 104 | setIsAddButtonDisabled(true); 105 | 106 | let contractCallData = contract.methods.add([1]).encodeABI(); 107 | let gas = 4000000; 108 | let metamaskTx = { 109 | from: walletAddress, 110 | to: testContract, 111 | value: "0x00", 112 | gas: "0x" + gas.toString(16), 113 | data: contractCallData 114 | } 115 | 116 | console.log("metamaskTx:", metamaskTx); 117 | let txHash = ""; 118 | let txError = false; 119 | metamask.eth.sendTransaction(metamaskTx, function (error, hash) { 120 | console.log(txHash); 121 | }).on('transactionHash', function (hash) { 122 | txHash = hash; 123 | }).on('error', function () { txError = true }); 124 | 125 | while ("" == txHash && !txError) { 126 | await new Promise(resolve => setTimeout(resolve, 500)); 127 | console.log("txHash:" + txHash); 128 | } 129 | 130 | if (txError) { 131 | setIsAddButtonDisabled(false); 132 | return; 133 | } 134 | 135 | let txReceipt = false; 136 | while (!txReceipt) { 137 | let tx = await web3Artela.eth.getTransaction(txHash) 138 | 139 | console.log("tx:" + tx); 140 | if (tx != null) { 141 | txReceipt = true; 142 | } 143 | await new Promise(resolve => setTimeout(resolve, 500)); 144 | } 145 | 146 | await syncUp(); 147 | 148 | setIsAddButtonDisabled(false); 149 | }; 150 | 151 | const configYourMetamask = async () => { 152 | let ethereum = window.ethereum 153 | if (!ethereum) { 154 | alert("this demo require metamask"); 155 | return; 156 | } 157 | 158 | const networkParams = { 159 | chainId: '0x2E2E', // 十六进制链ID 160 | chainName: 'Artela', 161 | rpcUrls: ['https://betanet-rpc1.artela.network/'], 162 | nativeCurrency: { 163 | name: 'Artela', 164 | symbol: 'ART', // 最多 5 个字符 165 | decimals: 18, 166 | }, 167 | blockExplorerUrls: ['https://betanet-scan.artela.network/'], 168 | }; 169 | 170 | try { 171 | // 尝试切换到指定网络 172 | await ethereum.request({ 173 | method: 'wallet_switchEthereumChain', 174 | params: [{ chainId: networkParams.chainId }], 175 | }); 176 | } catch (switchError) { 177 | // 如果指定网络未添加,则尝试添加 178 | if (switchError.code === 4902) { 179 | try { 180 | await ethereum.request({ 181 | method: 'wallet_addEthereumChain', 182 | params: [networkParams], 183 | }); 184 | } catch (addError) { 185 | console.error(addError); 186 | } 187 | } 188 | console.error(switchError); 189 | } 190 | 191 | try { 192 | const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); 193 | if (accounts.length > 0) { 194 | setWalletAddress(accounts[0]); 195 | } else { 196 | console.error('No accounts found'); 197 | } 198 | } catch (error) { 199 | console.error('Error connecting to MetaMask:', error); 200 | } 201 | } 202 | 203 | const handleAddWithSessionKey = async () => { 204 | if (undefined == walletAddress || "" == walletAddress) { 205 | await configYourMetamask(); 206 | return; 207 | } 208 | 209 | setIsAddButton2Disabled(true); 210 | await handleAddWithSessionKey_(); 211 | setIsAddButton2Disabled(false); 212 | } 213 | 214 | const handleAddWithSessionKey_ = async () => { 215 | 216 | let sessionKeyStatus_ = sessionKeyStatus; 217 | console.log("sessionKeyStatus_:" + sessionKeyStatus_); 218 | if (sessionKeyStatus_ == "unbind") { 219 | const result = window.confirm('Your EoA need to bind Aspect to enable session key!'); 220 | if (!result) { 221 | return; 222 | } 223 | 224 | await aspectClient.bindEoAByMetamask(walletAddress); 225 | 226 | sessionKeyStatus_ = "empty"; 227 | setSessionKeyStatus(sessionKeyStatus_) 228 | } 229 | 230 | console.log("sessionKeyStatus_:" + sessionKeyStatus_); 231 | if (sessionKeyStatus_ == "empty" || sessionKeyStatus_ == "expired") { 232 | const result = window.confirm('Create session key for your EoA!'); 233 | if (!result) { 234 | return; 235 | } 236 | 237 | let sKeyPrivKey = web3Artela.eth.accounts.create(web3Artela.utils.randomHex(32)).privateKey; 238 | let sKeyAccount = web3Artela.eth.accounts.privateKeyToAccount(sKeyPrivKey); 239 | console.log("create session key: ", sKeyAccount.address); 240 | await aspectClient.registerSessionKeyByMetamask(walletAddress, sKeyAccount.address, testContract, [testMethods], 48 * 60 * 60); 241 | 242 | saveToLocalStorage(walletAddress, sKeyPrivKey); 243 | 244 | sessionKeyStatus_ = "active"; 245 | setSessionKeyStatus(sessionKeyStatus_) 246 | } 247 | 248 | console.log("sessionKeyStatus_:" + sessionKeyStatus_); 249 | let contractCallData = contract.methods.add([1]).encodeABI(); 250 | let sKeyPrivKey = loadFromLocalStorage(walletAddress); 251 | 252 | let rawTx= await aspectClient.createUnsignTx(walletAddress,sKeyPrivKey,contractCallData,contract.options.address); 253 | 254 | let receipt = await web3Artela.eth.sendSignedTransaction(rawTx); 255 | console.log(`call contract with session key result: `); 256 | console.log(receipt); 257 | 258 | syncUp(); 259 | syncSKeyStatus(); 260 | }; 261 | 262 | 263 | 264 | return ( 265 |
266 |
267 |
268 |

Introduction

269 | This is Counter contract integrated with session key Aspect. 270 | When calling it using EoA, wallet interaction is required. 271 | But with session keys, it doesn't require any interaction. 272 |

Before Testing

273 | 274 |

275 |
276 |
277 | 278 |
279 |
280 |

Counter Contract

281 |

count: {counter}

282 | 283 |

call contract by EoA

284 | 285 | 286 |

call contract by session key

287 | 288 |

seseion key status: {sessionKeyStatus}

289 |

seseion key will expired at block#{expiredHeight}

290 |
291 |
292 |
293 | ); 294 | } 295 | 296 | export default App; 297 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/context/Web3Modal.js: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | getDefaultWallets, 5 | RainbowKitProvider, 6 | } from '@rainbow-me/rainbowkit'; 7 | import '@rainbow-me/rainbowkit/styles.css'; 8 | import { configureChains, createConfig, WagmiConfig } from 'wagmi'; 9 | 10 | import { alchemyProvider } from 'wagmi/providers/alchemy'; 11 | import { publicProvider } from 'wagmi/providers/public'; 12 | 13 | const artela = { 14 | id: 11822, 15 | name: 'Artela', 16 | network: 'artela-testnet-2', 17 | iconUrl: 'https://framerusercontent.com/images/xLv7JZ8nzPaZ9zk7j63YbRZHqY.png', 18 | iconBackground: '#fff', 19 | nativeCurrency: { 20 | decimals: 18, 21 | name: 'Artela', 22 | symbol: 'ART', 23 | }, 24 | rpcUrls: { 25 | public: { http: ['https://betanet-rpc1.artela.network/'] }, 26 | default: { http: ['https://betanet-rpc1.artela.network/'] }, 27 | }, 28 | blockExplorers: { 29 | default: { name: 'SnowTrace', url: 'https://betanet-scan.artela.network/' }, 30 | etherscan: { name: 'SnowTrace', url: 'https://betanet-scan.artela.network/' }, 31 | }, 32 | testnet: false, 33 | }; 34 | 35 | const { chains, publicClient } = configureChains( 36 | [artela], 37 | [ 38 | alchemyProvider({ apiKey: "b4dmYzJ5ztY18ziStcClJ_jRjEdieQqo" }), 39 | publicProvider() 40 | ] 41 | ); 42 | 43 | const { connectors } = getDefaultWallets({ 44 | appName: 'My RainbowKit App', 45 | projectId: '080999b35ff4ade22e595b9aeb16db24', 46 | chains 47 | }); 48 | 49 | const wagmiConfig = createConfig({ 50 | autoConnect: true, 51 | connectors, 52 | publicClient 53 | }) 54 | 55 | export function Web3Modal(children) { 56 | return 57 | 58 | {children} 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | import { 8 | getDefaultWallets, 9 | RainbowKitProvider, 10 | } from '@rainbow-me/rainbowkit'; 11 | import '@rainbow-me/rainbowkit/styles.css'; 12 | import { configureChains, createConfig, WagmiConfig } from 'wagmi'; 13 | 14 | import { alchemyProvider } from 'wagmi/providers/alchemy'; 15 | import { publicProvider } from 'wagmi/providers/public'; 16 | 17 | const artela = { 18 | id: 11822, 19 | name: 'Artela', 20 | network: 'artela-testnet-2', 21 | iconUrl: 'https://framerusercontent.com/images/xLv7JZ8nzPaZ9zk7j63YbRZHqY.png', 22 | iconBackground: '#fff', 23 | nativeCurrency: { 24 | decimals: 18, 25 | name: 'Artela', 26 | symbol: 'ART', 27 | }, 28 | rpcUrls: { 29 | public: { http: ['https://betanet-rpc1.artela.network/'] }, 30 | default: { http: ['https://betanet-rpc1.artela.network/'] }, 31 | }, 32 | blockExplorers: { 33 | default: { name: 'SnowTrace', url: 'https://betanet-scan.artela.network/' }, 34 | etherscan: { name: 'SnowTrace', url: 'https://betanet-scan.artela.network/' }, 35 | }, 36 | testnet: false, 37 | }; 38 | 39 | const { chains, publicClient } = configureChains( 40 | [artela], 41 | [ 42 | alchemyProvider({ apiKey: "b4dmYzJ5ztY18ziStcClJ_jRjEdieQqo" }), 43 | publicProvider() 44 | ] 45 | ); 46 | 47 | const { connectors } = getDefaultWallets({ 48 | appName: 'My RainbowKit App', 49 | projectId: '080999b35ff4ade22e595b9aeb16db24', 50 | chains 51 | }); 52 | 53 | const wagmiConfig = createConfig({ 54 | autoConnect: true, 55 | connectors, 56 | publicClient 57 | }) 58 | 59 | const root = ReactDOM.createRoot(document.getElementById('root')); 60 | root.render( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | 72 | // If you want to start measuring performance in your app, pass a function 73 | // to log results (for example: reportWebVitals(console.log)) 74 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 75 | reportWebVitals(); 76 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/demo/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/test/sessionKeyAspectClient.test.js: -------------------------------------------------------------------------------- 1 | const SessionKeyAspectClient = require('../index'); 2 | 3 | const Web3 = require('@artela/web3'); 4 | const fs = require("fs"); 5 | 6 | // ****************************************** 7 | // init web3 and private key 8 | // ****************************************** 9 | // const testnetRpc = "https://betanet-rpc1.artela.network" 10 | const testnetRpc = "http://47.254.2.74:8545" 11 | const web3 = new Web3(testnetRpc); 12 | 13 | let sk = fs.readFileSync("./test/privateKey.txt", 'utf-8'); 14 | const account = web3.eth.accounts.privateKeyToAccount(sk.trim()); 15 | web3.eth.accounts.wallet.add(account.privateKey); 16 | 17 | const testAspectAddress = "0xA9d50e86c6CD9c88f58751b3eB8653da0C723D10"; 18 | const testSessionKeyAddress = "0x0250032b4a11478969dc4caaa11ecc2ea98cfc12"; 19 | const testContract = "0x5E7EE0d74Fb66f1A1371056d4E113A6F6aA231E9"; 20 | const testMethods = ["0A0A0A0A", "0B0B0B0B"]; 21 | 22 | async function f() { 23 | 24 | // usage 1 25 | console.log("// usage 1") 26 | const aspectClient = new SessionKeyAspectClient(web3, testAspectAddress); 27 | let ret = await aspectClient.registerSessionKey(account, testSessionKeyAddress, testContract, testMethods, 20); 28 | console.log(ret); 29 | 30 | let sessionKey = await aspectClient.getSessionKey(account.address, testSessionKeyAddress, testContract); 31 | console.log(sessionKey) 32 | let expireHeight = await aspectClient.getSessioinKeyExpireHeight(account.address, testSessionKeyAddress, testContract); 33 | console.log(expireHeight) 34 | 35 | // usage 2 36 | console.log("// usage 2") 37 | let unsignTx = await aspectClient.registerSessionKeyUnsignTx(account.address, testSessionKeyAddress, testContract, testMethods, 20); 38 | console.log(unsignTx); 39 | 40 | let signedTx = await web3.eth.accounts.signTransaction(unsignTx, account.privateKey); 41 | let receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction); 42 | console.log(receipt); 43 | 44 | sessionKey = await aspectClient.getSessionKey(account.address, testSessionKeyAddress, testContract); 45 | console.log(sessionKey) 46 | 47 | // query all key 48 | let sessionKeys = await aspectClient.getAllSessionKey(account.address); 49 | console.log("all sessionKeys", sessionKeys); 50 | 51 | // test bind EoA 52 | // bind 53 | console.log("// binding address") 54 | ret = await aspectClient.bindEoA(account); 55 | console.log(ret); 56 | console.log(await aspectClient.ifBinding(account.address)); 57 | 58 | // query binding address list 59 | console.log("// query binding address") 60 | ret = await aspectClient.getBindingAccount(); 61 | console.log(ret); 62 | 63 | // unbind 64 | console.log("// unbinding address") 65 | ret = await aspectClient.unbindEoA(account); 66 | console.log(ret); 67 | 68 | // query binding address list 69 | console.log("// query binding address") 70 | ret = await aspectClient.getBindingAccount(); 71 | console.log(ret); 72 | console.log(await aspectClient.ifBinding(account.address)); 73 | 74 | // test bind Contract 75 | // query binding address list 76 | console.log("// query binding contract") 77 | ret = await aspectClient.ifBinding(testContract); 78 | console.log("contract is binding: " + ret); 79 | 80 | // bind contract 81 | console.log("// bind contract") 82 | ret = await aspectClient.bindContract(account, testContract); 83 | console.log("binding ret: ", ret); 84 | 85 | ret = await aspectClient.ifBinding(testContract); 86 | console.log("contract is binding: " + ret); 87 | 88 | // unbind contract 89 | console.log("// unbind contract") 90 | ret = await aspectClient.unbindContract(account, testContract); 91 | console.log("unbinding ret: ", ret); 92 | 93 | ret = await aspectClient.ifBinding(testContract); 94 | console.log("contract is binding: " + ret); 95 | 96 | // bind contract 97 | console.log("// bind contract") 98 | ret = await aspectClient.bindContract(account, testContract); 99 | console.log("binding ret: ", ret); 100 | 101 | ret = await aspectClient.ifBinding(testContract); 102 | console.log("contract is binding: " + ret); 103 | 104 | } 105 | 106 | f().then(); -------------------------------------------------------------------------------- /js_client/session_key_aspect_client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | output: { 6 | path: path.resolve("./", 'dist'), 7 | filename: 'sessioin-key-aspect-client.bundle.js', 8 | library: 'sessioin-key-aspect-client', 9 | libraryTarget: 'umd', 10 | globalObject: 'this' 11 | }, 12 | resolve: { 13 | fallback: { 14 | events: false, 15 | abc: false, // do not include a polyfill for abc 16 | xyz: path.resolve(__dirname, 'path/to/file.js'), // include a polyfill for xyz 17 | }, 18 | }, 19 | // 其他配置... 20 | }; 21 | --------------------------------------------------------------------------------