├── .github └── workflows │ └── go.yml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── checklist.md ├── diemclient ├── client.go ├── client_test.go ├── diemclienttest │ ├── doc.go │ ├── event_builder.go │ └── transaction_builder.go ├── doc.go ├── testnet_integration_test.go └── types.go ├── diemid ├── account.go ├── account_test.go ├── bech32 │ └── bech32.go ├── doc.go ├── intent.go └── intent_test.go ├── diemjsonrpctypes ├── doc.go ├── jsonrpc.pb.go ├── testdata │ ├── get-account-transaction-with-events.json │ ├── get-account-transactions-with-events.json │ ├── get-account-with-child-vasp-role.json │ ├── get-account-with-designated-dealer-role.json │ ├── get-account-with-parent-vasp-role.json │ ├── get-account-with-unknown-role.json │ ├── get-currencies.json │ ├── get-latest-metadata.json │ ├── get-received-payment-events.json │ ├── get-sent-payment-events.json │ └── get-transactions-with-events.json └── types_test.go ├── diemkeys ├── auth_key.go ├── auth_key_test.go ├── doc.go ├── ed25519.go ├── ed25519_test.go ├── keys.go ├── keys_test.go ├── multi_ed25519.go └── multi_ed25519_test.go ├── diemsigner ├── doc.go ├── signer.go └── signer_test.go ├── diemtypes ├── address.go ├── address_test.go ├── currency_type.go ├── doc.go ├── hash.go ├── lcs.go ├── lcs_test.go ├── lib.go ├── subaddress.go └── subaddress_test.go ├── examples ├── address-encoder │ └── main.go ├── create-child-vasp-account │ └── main.go ├── exampleutils │ ├── account_balances.go │ └── submit_and_wait.go ├── intent-identifier │ └── main.go └── p2p-transfers │ └── main.go ├── go.mod ├── go.sum ├── jsonrpc ├── client.go ├── client_test.go ├── doc.go ├── error.go ├── jsonrpctest │ └── stub.go ├── request.go └── response.go ├── stdlib ├── doc.go └── lib.go ├── testnet ├── const.go ├── doc.go ├── faucet.go └── faucet_test.go └── txnmetadata ├── doc.go ├── metadata.go └── metadata_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Test 34 | run: make test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "diem"] 2 | path = diem 3 | url = https://github.com/diem/diem.git 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The project has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://developers.diem.com/docs/policies/code-of-conduct) so that you can understand what actions will and will not be tolerated. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to client-sdk-go 2 | 3 | This project welcomes contributions. 4 | 5 | ## Contributor License Agreement 6 | 7 | For pull request to be accepted by any Diem projects, a CLA must be signed. 8 | You will only need to do this once to work on any of Diem's open source 9 | projects. 10 | 11 | When submitting a PR, the diem-github-bot will check your comment for a valid CLA. If one is not found, then you will need to submit an Individual CLA for yourself or a Corporate CLA for your company. 12 | 13 | ## Issues 14 | 15 | client-sdk-go uses [GitHub issues](https://github.com/diem/client-sdk-go/issues) to track 16 | bugs. Please include necessary information and instructions to reproduce your 17 | issue. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go clean -cache -testcache 3 | go list ./... | grep examples | grep -v transaction-builder | xargs go build 4 | go list ./... | grep -v /examples/ | xargs go test 5 | 6 | cover: 7 | mkdir -p .tmp 8 | go test -covermode=count -coverprofile=.tmp/count.out ./... 9 | go tool cover -html=.tmp/count.out 10 | 11 | gen: 12 | cd diem && cargo build -p transaction-builder-generator && target/debug/generate-transaction-builders \ 13 | --language go \ 14 | --module-name stdlib \ 15 | --diem-package-name github.com/diem/client-sdk-go \ 16 | --with-diem-types "testsuite/generate-format/tests/staged/diem.yaml" \ 17 | --target-source-dir ".." \ 18 | "language/diem-framework/releases/legacy" \ 19 | "language/diem-framework/releases/artifacts/current" 20 | 21 | protoc: 22 | # protoc --go_out=. --go_opt=paths=source_relative ./diemjsonrpctypes/jsonrpc.proto 23 | protoc -Idiem/json-rpc/types/src/proto --go_out=./diemjsonrpctypes --go_opt=paths=source_relative jsonrpc.proto 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Note to readers:** On December 1, 2020, the Diem Association was renamed to Diem Association. The project repos are in the process of being migrated. All projects will remain available for use here until the migration to a new GitHub Organization is complete. 2 | 3 | # client-sdk-go 4 | 5 | [![API Reference](https://img.shields.io/badge/api-reference-blue.svg)](https://github.com/diem/diem/blob/master/json-rpc/json-rpc-spec.md) [![Apache V2 License](https://img.shields.io/badge/license-Apache%20V2-blue.svg)](../master/LICENSE) 6 | 7 | client-sdk-go is the official Diem Client SDK for the Go programming language. 8 | 9 | ## Overview of SDK's Packages 10 | 11 | - diemclient: diem JSON-RPC APIs client 12 | - jsonrpc: a JSON-RPC 2.0 SPEC client 13 | - diemkeys: keys utils, including generating public & private keys for testing, creating auth key and account address from public key. 14 | - diemsigner: sign transaction logic 15 | - txnmetadata: utils for creating peer to peer transaction metadata. (LIP-4) 16 | - diemid: encoding & decoding Diem Account Identifier and Intent URL. (LIP-5) 17 | - testnet: testnet utils 18 | - stdlib: move stdlib script utils. This is generated code, for constructing transaction script playload. 19 | - diemtypes: Diem on-chain data structure types. Mostly generated code with small extension code for attaching handy functions to generated types. 20 | - [examples](../../tree/master/examples): examples of how to use this SDK. 21 | - [submit transaction and wait](../master/examples/exampleutils/submit_and_wait.go): this example shows how to submit a transaction and wait for its result; it also shows how to handle a stale response error in various cases. 22 | - [create child VASP account](../master/examples/create-child-vasp-account/main.go): this example shows how to create ChildVASP account for a ParentVASP account. 23 | - [p2p transfer](../master/examples/p2p-transfers/main.go): this example shows 4 different types of p2p transfers between custodial accounts and non-custodial accounts. 24 | - [intent identifier](../master/examples/intent-identifier/main.go): this example shows how to use diemid for encoding and decoding the intent identifier / url. 25 | 26 | ## Installing 27 | 28 | Use `go get` to retrieve the SDK to add it to your `GOPATH` workspace, or 29 | project's Go module dependencies. 30 | 31 | go get github.com/diem/client-sdk-go 32 | 33 | To update the SDK use `go get -u` to retrieve the latest version of the SDK. 34 | 35 | go get -u github.com/diem/client-sdk-go 36 | 37 | 38 | ## Development 39 | 40 | *Run test* 41 | 42 | ``` 43 | make test 44 | ``` 45 | 46 | *Generate diemtypes & move stdlib script encoder & decoder* 47 | 48 | ``` 49 | git submodule update 50 | make gen 51 | ``` 52 | 53 | *Upgrade diemtypes and move stdlib* 54 | ``` 55 | git submodule update 56 | cd diem 57 | git pull origin master 58 | cd .. 59 | make gen 60 | ``` 61 | 62 | # API Documentation 63 | 64 | The Go Client SDK API documentation is currently available at [godoc.org](https://godoc.org/github.com/diem/client-sdk-go). 65 | 66 | # License 67 | 68 | [Apache License V2](../master/LICENSE) 69 | 70 | 71 | # Contributing 72 | 73 | [CONTRIBUTING](../master/CONTRIBUTING.md) 74 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | Please see Diem's 4 | [security policies](https://developers.diem.com/docs/policies/security) and 5 | procedures for reporting vulnerabilities. 6 | -------------------------------------------------------------------------------- /checklist.md: -------------------------------------------------------------------------------- 1 | 2 | # Basics 3 | 4 | - [x] module structure: 5 | - diem 6 | - diemclient: high level APIs interface, should support application to do easy mock / stub development. 7 | - jsonrpc: jsonrpc client 8 | - diemkeys: keys utils, generate public & private keys for testing, create auth key and account address. 9 | - diemsigner: sign transaction logic 10 | - txnmetadata: utils for creating peer to peer transaction metadata. (LIP-4) 11 | - diemid: encoding & decodeing Diem Account Identifier and Intent URL. (LIP-5) 12 | - stdlib: move stdlib script utils. 13 | - testnet: testnet utils, should include FaucetService for handling testnet mint. 14 | - diemtypes: Diem onchain data structure types. 15 | - [x] JSON-RPC 2.0 Spec: 16 | - [x] spec version validation. 17 | - [x] batch requests and responses handling. 18 | - [x] JSON-RPC client error handling should distinguish the following 3 type errors: 19 | - Transport layer error, e.g. HTTP call failure. 20 | - JSON-RPC protocol error: e.g. server responds to non json data, or can't be parsed into [Diem JSON-RPC SPEC][1] defined data structure, or missing result & error field. 21 | - JSON-RPC error: error returned from server. 22 | - [x] https 23 | - [x] Client connection pool. 24 | - [x] Handle stale responses: 25 | - [x] client tracks latest server response block version and timestamp, raise error when received server response contains stale version / timestamp. 26 | - [ ] last known blockchain version >= response version + 30: when connecting to a cluster of fullnodes, it is possible some fullnodes are behind the head couple versions. 27 | - [ ] last known blockchain timestamp >= response timestamp + 30 seconds. 28 | - [x] parse and use diem_chain_id, diem_ledger_version and diem_ledger_tiemstamp in the JSONRPC response. 29 | - [x] language specific standard release publish: e.g. java maven central repo, python pip 30 | - [x] Multi-network: initialize Client with chain id, JSON-RPC server URL 31 | - [x] Validate server chain id: client should be initialized with chain id and validate server response chain id is the same. 32 | - [x] Handle unsigned int64 data type properly 33 | - [x] [Multi-signatures support](https://github.com/diem/diem/blob/master/specifications/crypto/spec.md#multi-signatures) 34 | - [x] Transaction hash: for a given signed transaction, produce hash of the transaction executed. 35 | - hex-encode(sha3-256([]byte("Transaction")) + []byte {0} + signed transaction bytes) 36 | - [ ] Send request with "client sdk name / version" as HTTP User-Agent: this is for server to recognize client sdk version, so that server can block a specific client version if we found unacceptable bugs. 37 | 38 | # [LIP-4][7] support 39 | 40 | - [x] Non-custodial to custodial transaction 41 | - [x] Custodial to non-custodial transaction 42 | - [x] Custodial to Custodial transaction 43 | - [x] Refund 44 | 45 | # [LIP-5][2] support 46 | 47 | - [x] Encode and decode account identifier 48 | - [x] Encode and decode intent identifier 49 | 50 | # Read from Blockchain 51 | 52 | - [x] Get metadata 53 | - [x] Get currencies 54 | - [x] Get events 55 | - [x] Get transactions 56 | - [x] Get account 57 | - [x] Get account transaction 58 | - [x] Get account transactions 59 | - [x] Handle error response 60 | - [x] Serialize result JSON to typed data structure 61 | - [x] Forward compatible: ignore unknown fields in response 62 | - [x] Backward compatible: new fields are optional 63 | 64 | # Submit Transaction 65 | 66 | - [x] Submit [p2p transfer][3] transaction 67 | - [x] Submit other [Move Stdlib scripts][4] 68 | - [x] waitForTransaction(accountAddress, sequence, transcationHash, expirationTimeSec, timeout): 69 | - for given signed transaction sender address, sequence number, expiration time (or 5 sec timeout) to wait and validate execution result is executed, otherwise return/raise an error / flag to tell it is not executed. 70 | - when signedTransactionHash validation failed, it should return / raise TransactionSequenceNumberConflictError 71 | - when transaction execution vm_status is not "executed", it should return / raise TransactionExecutionFailure 72 | - when transaction expired, it should return / raise TransactionExpiredError: compare the transaction expirationTimeSec with response latest ledger timestamp. If response latest ledger timestamp >= transaction expirationTimeSec, then we are sure the transaction will never be executed successfully. 73 | - Note: response latest ledger timestamp unit is microsecond, expirationTimeSec's unit is second. 74 | 75 | # Testnet support 76 | 77 | - [x] Generate ed25519 private key, derive ed25519 public keys from private key. 78 | - [x] Generate Single auth-keys 79 | - [x] Generate MultiSig auth-keys 80 | - [x] Mint coins through [Faucet service][6] 81 | 82 | See [doc][5] for above concepts. 83 | 84 | # Examples 85 | 86 | - [x] [p2p transfer examples](https://github.com/diem/lip/blob/master/lips/lip-4.md#transaction-examples) 87 | - [x] refund p2p transfer example 88 | - [x] create childVASP example 89 | - [x] Intent identifier encoding, decoding example 90 | 91 | # Nice to have 92 | 93 | - [ ] Async client 94 | - [ ] CLI connects to testnet for trying out features. 95 | 96 | [1]: https://github.com/diem/diem/blob/master/json-rpc/json-rpc-spec.md "Diem JSON-RPC SPEC" 97 | [2]: https://github.com/diem/lip/blob/master/lips/lip-5.md "LIP-5" 98 | [3]: https://github.com/diem/diem/blob/master/language/stdlib/transaction_scripts/doc/peer_to_peer_with_metadata.md "P2P Transafer" 99 | [4]: https://github.com/diem/diem/tree/master/language/stdlib/transaction_scripts/doc "Move Stdlib scripts" 100 | [5]: https://github.com/diem/diem/blob/master/client/diem-dev/README.md "Diem Client Dev Doc" 101 | [6]: https://github.com/diem/diem/blob/master/json-rpc/docs/service_testnet_faucet.md "Faucet service" 102 | [7]: https://github.com/diem/lip/blob/master/lips/lip-4.md "Transaction Metadata Specification" 103 | -------------------------------------------------------------------------------- /diemclient/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclient 5 | 6 | import ( 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | "time" 12 | 13 | "github.com/avast/retry-go" 14 | "github.com/diem/client-sdk-go/diemtypes" 15 | "github.com/diem/client-sdk-go/jsonrpc" 16 | ) 17 | 18 | // List of supported methods 19 | const ( 20 | GetCurrencies jsonrpc.Method = "get_currencies" 21 | GetMetadata jsonrpc.Method = "get_metadata" 22 | GetAccount jsonrpc.Method = "get_account" 23 | GetAccountTransaction jsonrpc.Method = "get_account_transaction" 24 | GetAccountTransactions jsonrpc.Method = "get_account_transactions" 25 | GetTransactions jsonrpc.Method = "get_transactions" 26 | GetEvents jsonrpc.Method = "get_events" 27 | Submit jsonrpc.Method = "submit" 28 | 29 | VmStatusExecuted = "executed" 30 | ) 31 | 32 | // StaleResponseError is error for the case server response latest ledger state is older than 33 | // client knows 34 | type StaleResponseError struct { 35 | Client LedgerState 36 | Server LedgerState 37 | } 38 | 39 | // Error implements error interface 40 | func (e *StaleResponseError) Error() string { 41 | return fmt.Sprintf("stale response error: expected server response ledger %v >= %v", e.Server, e.Client) 42 | } 43 | 44 | // InvalidTransactionError is error for get a transaction with unexpected details (e.g. vm status is failure) 45 | type InvalidTransactionError struct { 46 | Transaction Transaction 47 | Msg string 48 | } 49 | 50 | // Error implements error interface 51 | func (e *InvalidTransactionError) Error() string { 52 | return e.Msg 53 | } 54 | 55 | // Client is Diem client implements high level APIs 56 | type Client interface { 57 | GetCurrencies() ([]*CurrencyInfo, error) 58 | GetMetadata() (*Metadata, error) 59 | GetMetadataByVersion(uint64) (*Metadata, error) 60 | GetAccount(diemtypes.AccountAddress) (*Account, error) 61 | GetAccountTransaction(diemtypes.AccountAddress, uint64, bool) (*Transaction, error) 62 | GetAccountTransactions(diemtypes.AccountAddress, uint64, uint64, bool) ([]*Transaction, error) 63 | GetTransactions(uint64, uint64, bool) ([]*Transaction, error) 64 | GetEvents(string, uint64, uint64) ([]*Event, error) 65 | Submit(signedTxnHex string) error 66 | SubmitTransaction(txn *diemtypes.SignedTransaction) error 67 | 68 | WaitForTransaction( 69 | address diemtypes.AccountAddress, 70 | seq uint64, 71 | hash string, 72 | expirationTimeSec uint64, 73 | timeout time.Duration, 74 | ) (*Transaction, error) 75 | WaitForTransaction2( 76 | txn *diemtypes.SignedTransaction, 77 | timeout time.Duration, 78 | ) (*Transaction, error) 79 | WaitForTransaction3( 80 | signedTxnHex string, 81 | timeout time.Duration, 82 | ) (*Transaction, error) 83 | 84 | LastResponseLedgerState() LedgerState 85 | UpdateLastResponseLedgerState(state LedgerState) error 86 | WithRetryOptions(opts ...retry.Option) Client 87 | } 88 | 89 | // New creates a `DiemClient` connect to given server URL. 90 | // It creates default jsonrpc client `http.Transport` config, if you need to customize 91 | // `http.Transport` config (for better connection pool production usage), call `NewWithJsonRpcClient` with 92 | // `jsonrpc.NewClientWithTransport(url, )` 93 | func New(chainID byte, url string) Client { 94 | return NewWithJsonRpcClient(chainID, jsonrpc.NewClient(url)) 95 | } 96 | 97 | // NewWithJsonRpcClient creates a `DiemClient` with given `jsonrpc.Client` 98 | func NewWithJsonRpcClient(chainID byte, rpc jsonrpc.Client) Client { 99 | return &client{chainID: chainID, rpc: rpc, retryOpts: []retry.Option{retry.LastErrorOnly(true)}} 100 | } 101 | 102 | // LedgerState represents response DiemLedgerTimestampusec & DiemLedgerVersion 103 | type LedgerState struct { 104 | TimestampUsec uint64 105 | Version uint64 106 | } 107 | 108 | type client struct { 109 | chainID byte 110 | rpc jsonrpc.Client 111 | mux sync.RWMutex 112 | last LedgerState 113 | retryOpts []retry.Option 114 | } 115 | 116 | // WithRetryOptions appends given retry options 117 | func (c *client) WithRetryOptions(opts ...retry.Option) Client { 118 | c.retryOpts = append(c.retryOpts, opts...) 119 | return c 120 | } 121 | 122 | // LastResponseLedgerState returns last recorded response ledger state 123 | func (c *client) LastResponseLedgerState() LedgerState { 124 | c.mux.RLock() 125 | defer c.mux.RUnlock() 126 | return c.last 127 | } 128 | 129 | // UpdateLastResponseLedgerState updates LastResponseLedgerState 130 | func (c *client) UpdateLastResponseLedgerState(state LedgerState) error { 131 | c.mux.Lock() 132 | defer c.mux.Unlock() 133 | var last = c.last 134 | if last.Version == state.Version && last.TimestampUsec == state.TimestampUsec { 135 | return nil 136 | } 137 | if last.Version > state.Version || last.TimestampUsec > state.TimestampUsec { 138 | return &StaleResponseError{Client: last, Server: state} 139 | } 140 | 141 | c.last = state 142 | return nil 143 | } 144 | 145 | // WaitForTransaction3 waits for given `SignedTransaction` hex string 146 | func (c *client) WaitForTransaction3(signedTxnHex string, timeout time.Duration) (*Transaction, error) { 147 | bytes, err := hex.DecodeString(signedTxnHex) 148 | if err != nil { 149 | return nil, err 150 | } 151 | txn, err := diemtypes.BcsDeserializeSignedTransaction(bytes) 152 | if err != nil { 153 | return nil, fmt.Errorf("Deserialize given hex string as SignedTransaction BCS failed: %v", err.Error()) 154 | } 155 | return c.WaitForTransaction2(&txn, timeout) 156 | } 157 | 158 | // WaitForTransaction2 waits for given `SignedTransaction` 159 | func (c *client) WaitForTransaction2(txn *diemtypes.SignedTransaction, timeout time.Duration) (*Transaction, error) { 160 | return c.WaitForTransaction( 161 | txn.RawTxn.Sender, 162 | txn.RawTxn.SequenceNumber, 163 | txn.TransactionHash(), 164 | txn.RawTxn.ExpirationTimestampSecs, 165 | timeout, 166 | ) 167 | } 168 | 169 | // WaitForTransaction waits for given (address, sequence number, hash) transaction. 170 | func (c *client) WaitForTransaction(address diemtypes.AccountAddress, seq uint64, hash string, expirationTimeSec uint64, timeout time.Duration) (*Transaction, error) { 171 | step := time.Millisecond * 500 172 | start := time.Now() 173 | for { 174 | if time.Since(start) > timeout { 175 | break 176 | } 177 | txn, err := c.GetAccountTransaction(address, seq, true) 178 | if _, ok := err.(*StaleResponseError); ok { 179 | continue 180 | } 181 | if err != nil { 182 | return nil, err 183 | } 184 | if txn != nil { 185 | if txn.Hash != hash { 186 | return nil, &InvalidTransactionError{ 187 | Transaction: *txn, 188 | Msg: fmt.Sprintf( 189 | "transaction hash does not match, given %#v, but got %#v", 190 | hash, txn.Hash), 191 | } 192 | } 193 | if txn.VmStatus.Type != VmStatusExecuted { 194 | return nil, &InvalidTransactionError{ 195 | Transaction: *txn, 196 | Msg: fmt.Sprintf( 197 | "transaction execution failed: %v", txn.VmStatus), 198 | } 199 | 200 | } 201 | return txn, nil 202 | } 203 | if expirationTimeSec*1_000_000 <= c.LastResponseLedgerState().TimestampUsec { 204 | return nil, errors.New("transaction expired") 205 | } 206 | time.Sleep(step) 207 | } 208 | return nil, fmt.Errorf("transaction not found within timeout period: %v", timeout) 209 | } 210 | 211 | // GetCurrencies calls to "get_currencies" method 212 | func (c *client) GetCurrencies() ([]*CurrencyInfo, error) { 213 | var ret []*CurrencyInfo 214 | ok, err := c.call(GetCurrencies, &ret) 215 | if !ok { 216 | return nil, err 217 | } 218 | 219 | return ret, nil 220 | } 221 | 222 | func (c *client) GetMetadata() (*Metadata, error) { 223 | var ret Metadata 224 | ok, err := c.call(GetMetadata, &ret) 225 | if !ok { 226 | return nil, err 227 | } 228 | 229 | return &ret, nil 230 | } 231 | 232 | func (c *client) GetMetadataByVersion(version uint64) (*Metadata, error) { 233 | var ret Metadata 234 | ok, err := c.call(GetMetadata, &ret, version) 235 | if !ok { 236 | return nil, err 237 | } 238 | 239 | return &ret, nil 240 | } 241 | 242 | func (c *client) GetAccount(address diemtypes.AccountAddress) (*Account, error) { 243 | var ret Account 244 | ok, err := c.call(GetAccount, &ret, address.Hex()) 245 | if !ok { 246 | return nil, err 247 | } 248 | 249 | return &ret, nil 250 | } 251 | 252 | func (c *client) GetAccountTransaction(address diemtypes.AccountAddress, sequenceNum uint64, includeEvent bool) (*Transaction, error) { 253 | var ret Transaction 254 | ok, err := c.call(GetAccountTransaction, &ret, address.Hex(), sequenceNum, includeEvent) 255 | if !ok { 256 | return nil, err 257 | } 258 | return &ret, nil 259 | } 260 | 261 | func (c *client) GetAccountTransactions(address diemtypes.AccountAddress, start uint64, limit uint64, includeEvent bool) ([]*Transaction, error) { 262 | var ret []*Transaction 263 | _, err := c.call(GetAccountTransactions, &ret, address.Hex(), start, limit, includeEvent) 264 | if err != nil { 265 | return nil, err 266 | } 267 | return ret, nil 268 | } 269 | 270 | func (c *client) GetTransactions(startVersion uint64, limit uint64, includeEvent bool) ([]*Transaction, error) { 271 | var ret []*Transaction 272 | ok, err := c.call(GetTransactions, &ret, startVersion, limit, includeEvent) 273 | if !ok { 274 | return nil, err 275 | } 276 | return ret, nil 277 | } 278 | 279 | func (c *client) GetEvents(key string, start uint64, limit uint64) ([]*Event, error) { 280 | var ret []*Event 281 | ok, err := c.call(GetEvents, &ret, key, start, limit) 282 | if !ok { 283 | return nil, err 284 | } 285 | return ret, nil 286 | } 287 | 288 | // Submit hex-encoded signed transaction bytes to mempool. 289 | // This function ignores StaleResponseError and does not retry on any errors. 290 | func (c *client) Submit(data string) error { 291 | ok, err := c.callWithoutRetry(Submit, nil, data) 292 | if !ok { 293 | if _, ok := err.(*StaleResponseError); ok { 294 | return nil 295 | } 296 | return err 297 | } 298 | return nil 299 | } 300 | 301 | func (c *client) SubmitTransaction(txn *diemtypes.SignedTransaction) error { 302 | return c.Submit(diemtypes.ToHex(txn)) 303 | } 304 | 305 | func (c *client) call(method jsonrpc.Method, ret interface{}, params ...jsonrpc.Param) (ok bool, err error) { 306 | err = retry.Do( 307 | func() error { 308 | ok, err = c.callWithoutRetry(method, ret, params...) 309 | return err 310 | }, 311 | c.retryOpts..., 312 | ) 313 | return ok, err 314 | } 315 | 316 | func (c *client) callWithoutRetry(method jsonrpc.Method, ret interface{}, params ...jsonrpc.Param) (bool, error) { 317 | req := jsonrpc.NewRequest(method, params...) 318 | resps, err := c.rpc.Call(req) 319 | if err != nil { 320 | return false, err 321 | } 322 | resp := resps[req.ID] 323 | 324 | if err = c.validateChainID(byte(resp.DiemChainID)); err != nil { 325 | return false, err 326 | } 327 | err = c.UpdateLastResponseLedgerState(LedgerState{ 328 | TimestampUsec: resp.DiemLedgerTimestampusec, 329 | Version: resp.DiemLedgerVersion, 330 | }) 331 | if err != nil { 332 | return false, err 333 | } 334 | 335 | if resp.Error != nil { 336 | return false, resp.Error 337 | } 338 | return resp.UnmarshalResult(ret) 339 | } 340 | 341 | func (c *client) validateChainID(chainID byte) error { 342 | if c.chainID != chainID { 343 | return fmt.Errorf("chain id mismatch error: expected server response chain id == %d, but got %d", c.chainID, chainID) 344 | } 345 | return nil 346 | } 347 | -------------------------------------------------------------------------------- /diemclient/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclient_test 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "testing" 10 | "time" 11 | 12 | "github.com/avast/retry-go" 13 | "github.com/diem/client-sdk-go/diemclient" 14 | "github.com/diem/client-sdk-go/diemkeys" 15 | "github.com/diem/client-sdk-go/jsonrpc" 16 | "github.com/diem/client-sdk-go/jsonrpc/jsonrpctest" 17 | "github.com/diem/client-sdk-go/testnet" 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func TestWaitForTransaction(t *testing.T) { 23 | cases := []struct { 24 | name string 25 | response jsonrpc.Response 26 | call func(t *testing.T, client diemclient.Client) 27 | }{ 28 | { 29 | name: "wait for transaction: success", 30 | response: jsonrpc.Response{ 31 | Result: toPtr(json.RawMessage(`{ 32 | "events": [], 33 | "gas_used": 175, 34 | "hash": "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 35 | "transaction": { 36 | "chain_id": 2, 37 | "expiration_timestamp_secs": 100000000000, 38 | "sequence_number": 0, 39 | "signature": "a181a036ba68fcd25a7ba9f3895caf720af7aee4bf86c4d798050a1101e75f71ccd891158c8fa0bf349bbb66fb0ba50b29b6fb29822dc04071aff831735e6402", 40 | "type": "user" 41 | }, 42 | "version": 106548, 43 | "vm_status": { "type": "executed" } 44 | }`)), 45 | }, 46 | call: func(t *testing.T, client diemclient.Client) { 47 | account := diemkeys.MustGenKeys() 48 | ret, err := client.WaitForTransaction( 49 | account.AccountAddress(), 50 | 0, 51 | "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 52 | uint64(time.Now().Add(2*time.Second).Unix()), 53 | time.Second*1, 54 | ) 55 | require.NoError(t, err) 56 | assert.NotNil(t, ret) 57 | }, 58 | }, 59 | { 60 | name: "wait for transaction: timeout when server response stale version", 61 | response: jsonrpc.Response{ 62 | DiemLedgerVersion: 10, 63 | DiemLedgerTimestampusec: 1597722856123456, 64 | Result: toPtr(json.RawMessage(`{ 65 | "events": [], 66 | "gas_used": 175, 67 | "hash": "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 68 | "transaction": { 69 | "chain_id": 2, 70 | "expiration_timestamp_secs": 100000000000, 71 | "sequence_number": 0, 72 | "signature": "a181a036ba68fcd25a7ba9f3895caf720af7aee4bf86c4d798050a1101e75f71ccd891158c8fa0bf349bbb66fb0ba50b29b6fb29822dc04071aff831735e6402", 73 | "type": "user" 74 | }, 75 | "version": 106548, 76 | "vm_status": { "type": "executed" } 77 | }`)), 78 | }, 79 | call: func(t *testing.T, client diemclient.Client) { 80 | client.UpdateLastResponseLedgerState(diemclient.LedgerState{ 81 | Version: 11, 82 | TimestampUsec: 1597722856123457, 83 | }) 84 | account := diemkeys.MustGenKeys() 85 | ret, err := client.WaitForTransaction( 86 | account.AccountAddress(), 87 | 0, 88 | "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 89 | uint64(time.Now().Add(2*time.Second).Unix()), 90 | time.Second*1, 91 | ) 92 | require.EqualError(t, err, "transaction not found within timeout period: 1s") 93 | assert.Nil(t, ret) 94 | }, 95 | }, 96 | { 97 | name: "wait for transaction: timeout", 98 | response: jsonrpc.Response{}, 99 | call: func(t *testing.T, client diemclient.Client) { 100 | account := diemkeys.MustGenKeys() 101 | ret, err := client.WaitForTransaction( 102 | account.AccountAddress(), 103 | 0, 104 | "invalid-hash", 105 | uint64(time.Now().Add(2*time.Second).Unix()), 106 | time.Second*1, 107 | ) 108 | require.EqualError(t, err, "transaction not found within timeout period: 1s") 109 | assert.Nil(t, ret) 110 | }, 111 | }, 112 | { 113 | name: "wait for transaction: hash mismatch", 114 | response: jsonrpc.Response{ 115 | Result: toPtr(json.RawMessage(`{ 116 | "events": [], 117 | "gas_used": 175, 118 | "hash": "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 119 | "transaction": { 120 | "chain_id": 2, 121 | "expiration_timestamp_secs": 100000000000, 122 | "sequence_number": 0, 123 | "signature": "a181a036ba68fcd25a7ba9f3895caf720af7aee4bf86c4d798050a1101e75f71ccd891158c8fa0bf349bbb66fb0ba50b29b6fb29822dc04071aff831735e6402", 124 | "type": "user" 125 | }, 126 | "version": 106548, 127 | "vm_status": { "type": "executed" } 128 | }`)), 129 | }, 130 | call: func(t *testing.T, client diemclient.Client) { 131 | account := diemkeys.MustGenKeys() 132 | ret, err := client.WaitForTransaction( 133 | account.AccountAddress(), 134 | 0, 135 | "mismatched hash", 136 | uint64(time.Now().Add(time.Second).Unix()), 137 | time.Second*5, 138 | ) 139 | assert.EqualError(t, err, "transaction hash does not match, given \"mismatched hash\", but got \"0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e\"") 140 | assert.Nil(t, ret) 141 | }, 142 | }, 143 | { 144 | name: "wait for transaction: execution failed", 145 | response: jsonrpc.Response{ 146 | Result: toPtr(json.RawMessage(`{ 147 | "events": [], 148 | "gas_used": 175, 149 | "hash": "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 150 | "transaction": { 151 | "chain_id": 2, 152 | "expiration_timestamp_secs": 100000000000, 153 | "sequence_number": 0, 154 | "signature": "a181a036ba68fcd25a7ba9f3895caf720af7aee4bf86c4d798050a1101e75f71ccd891158c8fa0bf349bbb66fb0ba50b29b6fb29822dc04071aff831735e6402", 155 | "type": "user" 156 | }, 157 | "version": 106548, 158 | "vm_status": { "type": "move_abort", "abort_code": 5, "location": "00000000000000000000000000000001::DiemAccount"} 159 | }`)), 160 | }, 161 | call: func(t *testing.T, client diemclient.Client) { 162 | account := diemkeys.MustGenKeys() 163 | ret, err := client.WaitForTransaction( 164 | account.AccountAddress(), 165 | 0, 166 | "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 167 | uint64(time.Now().Add(time.Second).Unix()), 168 | time.Second*5, 169 | ) 170 | assert.Error(t, err) 171 | assert.Contains(t, err.Error(), "transaction execution failed") 172 | assert.Nil(t, ret) 173 | }, 174 | }, 175 | { 176 | name: "wait for transaction: expired", 177 | response: jsonrpc.Response{ 178 | Result: nil, 179 | DiemLedgerTimestampusec: 1597722856123456, 180 | }, 181 | call: func(t *testing.T, client diemclient.Client) { 182 | account := diemkeys.MustGenKeys() 183 | ret, err := client.WaitForTransaction( 184 | account.AccountAddress(), 185 | 0, 186 | "0fa27a781a9086e80a870851ea4f1b14090fb8b5bd9933e27447ab806443e08e", 187 | uint64(1597722856), 188 | time.Second*5, 189 | ) 190 | assert.EqualError(t, err, "transaction expired") 191 | assert.Nil(t, ret) 192 | }, 193 | }, 194 | { 195 | name: "wait for transaction: if onchain time is exactly same with expiration time", 196 | response: jsonrpc.Response{ 197 | Result: nil, 198 | DiemLedgerTimestampusec: 1597722856000000, 199 | }, 200 | call: func(t *testing.T, client diemclient.Client) { 201 | account := diemkeys.MustGenKeys() 202 | ret, err := client.WaitForTransaction( 203 | account.AccountAddress(), 204 | 0, 205 | "a181a036ba68fcd25a7ba9f3895caf720af7aee4bf86c4d798050a1101e75f71ccd891158c8fa0bf349bbb66fb0ba50b29b6fb29822dc04071aff831735e6402", 206 | uint64(1597722856), 207 | time.Second*5, 208 | ) 209 | assert.EqualError(t, err, "transaction expired") 210 | assert.Nil(t, ret) 211 | }, 212 | }, 213 | { 214 | name: "wait for transaction3 invalid hex string", 215 | response: jsonrpc.Response{}, 216 | call: func(t *testing.T, client diemclient.Client) { 217 | ret, err := client.WaitForTransaction3("invalid", time.Second) 218 | require.EqualError(t, err, "encoding/hex: invalid byte: U+0069 'i'") 219 | assert.Nil(t, ret) 220 | }, 221 | }, 222 | { 223 | name: "wait for transaction3: not a signed transaction bcs", 224 | response: jsonrpc.Response{}, 225 | call: func(t *testing.T, client diemclient.Client) { 226 | account := diemkeys.MustGenKeys() 227 | ret, err := client.WaitForTransaction3( 228 | account.AccountAddress().Hex(), 229 | time.Second) 230 | require.EqualError(t, err, "Deserialize given hex string as SignedTransaction BCS failed: EOF") 231 | assert.Nil(t, ret) 232 | }, 233 | }, 234 | } 235 | 236 | for _, tc := range cases { 237 | t.Run(tc.name, func(t *testing.T) { 238 | client := diemclient.NewWithJsonRpcClient(testnet.ChainID, &jsonrpctest.Stub{ 239 | Responses: map[jsonrpc.RequestID]jsonrpc.Response{ 240 | 1: tc.response, 241 | }, 242 | }).WithRetryOptions(retry.Attempts(1)) 243 | tc.call(t, client) 244 | }) 245 | } 246 | } 247 | 248 | func TestHandleStaleResponse(t *testing.T) { 249 | cases := []struct { 250 | name string 251 | response jsonrpc.Response 252 | call func(t *testing.T, client diemclient.Client) 253 | }{ 254 | { 255 | name: "return error if server response version is older", 256 | response: jsonrpc.Response{ 257 | DiemLedgerVersion: 9, 258 | DiemLedgerTimestampusec: 1597722856123456, 259 | Result: toPtr(json.RawMessage(`{ 260 | "timestamp": 1597722856123456, 261 | "version": 9, 262 | "chain_id": 2 263 | }`)), 264 | }, 265 | call: func(t *testing.T, client diemclient.Client) { 266 | client.UpdateLastResponseLedgerState(diemclient.LedgerState{ 267 | Version: 10, 268 | TimestampUsec: 1597722856123477, 269 | }) 270 | ret, err := client.GetMetadata() 271 | assert.EqualError(t, err, "stale response error: expected server response ledger {1597722856123456 9} >= {1597722856123477 10}") 272 | assert.Nil(t, ret) 273 | }, 274 | }, 275 | { 276 | name: "return error if server response timestamp is older", 277 | response: jsonrpc.Response{ 278 | DiemLedgerVersion: 10, 279 | DiemLedgerTimestampusec: 1597722856123456, 280 | Result: toPtr(json.RawMessage(`{ 281 | "timestamp": 1597722856123456, 282 | "version": 10, 283 | "chain_id": 2 284 | }`)), 285 | }, 286 | call: func(t *testing.T, client diemclient.Client) { 287 | client.UpdateLastResponseLedgerState(diemclient.LedgerState{ 288 | Version: 10, 289 | TimestampUsec: 1597722856123477, 290 | }) 291 | ret, err := client.GetMetadata() 292 | assert.EqualError(t, err, "stale response error: expected server response ledger {1597722856123456 10} >= {1597722856123477 10}") 293 | assert.Nil(t, ret) 294 | }, 295 | }, 296 | { 297 | name: "update last response state if server response version & timestamp is new", 298 | response: jsonrpc.Response{ 299 | DiemLedgerVersion: 11, 300 | DiemLedgerTimestampusec: 1597722856123488, 301 | Result: toPtr(json.RawMessage(`{ 302 | "timestamp": 1597722856123488, 303 | "version": 11, 304 | "chain_id": 2 305 | }`)), 306 | }, 307 | call: func(t *testing.T, client diemclient.Client) { 308 | client.UpdateLastResponseLedgerState(diemclient.LedgerState{ 309 | Version: 10, 310 | TimestampUsec: 1597722856123477, 311 | }) 312 | _, err := client.GetMetadata() 313 | assert.NoError(t, err) 314 | last := client.LastResponseLedgerState() 315 | assert.Equal(t, uint64(11), last.Version) 316 | assert.Equal(t, uint64(1597722856123488), last.TimestampUsec) 317 | }, 318 | }, 319 | } 320 | 321 | for _, tc := range cases { 322 | t.Run(tc.name, func(t *testing.T) { 323 | client := diemclient.NewWithJsonRpcClient(testnet.ChainID, &jsonrpctest.Stub{ 324 | Responses: map[jsonrpc.RequestID]jsonrpc.Response{ 325 | 1: tc.response, 326 | }, 327 | }).WithRetryOptions(retry.Attempts(1)) 328 | tc.call(t, client) 329 | }) 330 | } 331 | } 332 | 333 | func TestValidateChainID(t *testing.T) { 334 | cases := []struct { 335 | name string 336 | response jsonrpc.Response 337 | call func(t *testing.T, client diemclient.Client) 338 | }{ 339 | { 340 | name: "return error if server response chain id mismatched", 341 | response: jsonrpc.Response{ 342 | DiemChainID: 9, 343 | Result: toPtr(json.RawMessage(`{ 344 | "timestamp": 1597722856123456, 345 | "version": 9, 346 | "chain_id": 9 347 | }`)), 348 | }, 349 | call: func(t *testing.T, client diemclient.Client) { 350 | ret, err := client.GetMetadata() 351 | assert.EqualError(t, err, fmt.Sprintf("chain id mismatch error: expected server response chain id == %d, but got 9", testnet.ChainID)) 352 | assert.Nil(t, ret) 353 | }, 354 | }, 355 | } 356 | 357 | for _, tc := range cases { 358 | t.Run(tc.name, func(t *testing.T) { 359 | client := diemclient.NewWithJsonRpcClient(testnet.ChainID, &jsonrpctest.Stub{ 360 | Responses: map[jsonrpc.RequestID]jsonrpc.Response{ 361 | 1: tc.response, 362 | }, 363 | }).WithRetryOptions(retry.Attempts(1)) 364 | tc.call(t, client) 365 | }) 366 | } 367 | } 368 | 369 | func toPtr(msg json.RawMessage) *json.RawMessage { 370 | return &msg 371 | } 372 | -------------------------------------------------------------------------------- /diemclient/diemclienttest/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides builders for creating JSON-RPC response result in test. Should only be used in test code. 5 | package diemclienttest 6 | -------------------------------------------------------------------------------- /diemclient/diemclienttest/event_builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclienttest 5 | 6 | import "github.com/diem/client-sdk-go/diemclient" 7 | 8 | type eventBuilderPart func(*diemclient.Event) 9 | type EventBuilder struct { 10 | parts []eventBuilderPart 11 | } 12 | 13 | func (b EventBuilder) Build() *diemclient.Event { 14 | t := new(diemclient.Event) 15 | for _, part := range b.parts { 16 | part(t) 17 | } 18 | return t 19 | } 20 | 21 | func (b EventBuilder) Type(t string) *EventBuilder { 22 | return b.append(func(e *diemclient.Event) { 23 | if e.Data == nil { 24 | e.Data = new(diemclient.EventData) 25 | } 26 | e.Data.Type = t 27 | }) 28 | } 29 | 30 | func (b EventBuilder) SequenceNumber(n uint64) *EventBuilder { 31 | return b.append(func(e *diemclient.Event) { 32 | e.SequenceNumber = n 33 | }) 34 | } 35 | 36 | func (b EventBuilder) Receiver(receiver string) *EventBuilder { 37 | return b.append(func(e *diemclient.Event) { 38 | if e.Data == nil { 39 | e.Data = new(diemclient.EventData) 40 | } 41 | e.Data.Receiver = receiver 42 | }) 43 | } 44 | 45 | func (b EventBuilder) Metadata(metadata string) *EventBuilder { 46 | return b.append(func(e *diemclient.Event) { 47 | if e.Data == nil { 48 | e.Data = new(diemclient.EventData) 49 | } 50 | e.Data.Metadata = metadata 51 | }) 52 | } 53 | 54 | func (b *EventBuilder) append(parts ...eventBuilderPart) *EventBuilder { 55 | newParts := make([]eventBuilderPart, len(b.parts)+len(parts)) 56 | copy(newParts, b.parts) 57 | copy(newParts[len(b.parts):], parts) 58 | b.parts = newParts 59 | return b 60 | } 61 | -------------------------------------------------------------------------------- /diemclient/diemclienttest/transaction_builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclienttest 5 | 6 | import "github.com/diem/client-sdk-go/diemclient" 7 | 8 | type transactionBuilderPart func(*diemclient.Transaction) 9 | type TransactionBuilder struct { 10 | parts []transactionBuilderPart 11 | } 12 | 13 | func (b *TransactionBuilder) Build() *diemclient.Transaction { 14 | t := new(diemclient.Transaction) 15 | for _, part := range b.parts { 16 | part(t) 17 | } 18 | return t 19 | } 20 | 21 | func (b TransactionBuilder) Events(events ...*EventBuilder) *TransactionBuilder { 22 | return b.append(func(t *diemclient.Transaction) { 23 | for _, event := range events { 24 | t.Events = append(t.Events, event.Build()) 25 | } 26 | }) 27 | } 28 | 29 | func (b *TransactionBuilder) append(parts ...transactionBuilderPart) *TransactionBuilder { 30 | newParts := make([]transactionBuilderPart, len(b.parts)+len(parts)) 31 | copy(newParts, b.parts) 32 | copy(newParts[len(b.parts):], parts) 33 | b.parts = newParts 34 | return b 35 | } 36 | -------------------------------------------------------------------------------- /diemclient/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides Diem JSON-RPC API client, see https://github.com/diem/diem/blob/master/json-rpc/json-rpc-spec.md 5 | // for more details. 6 | package diemclient 7 | -------------------------------------------------------------------------------- /diemclient/testnet_integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclient_test 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/diem/client-sdk-go/diemclient" 11 | "github.com/diem/client-sdk-go/diemsigner" 12 | "github.com/diem/client-sdk-go/diemtypes" 13 | "github.com/diem/client-sdk-go/jsonrpc" 14 | "github.com/diem/client-sdk-go/stdlib" 15 | "github.com/diem/client-sdk-go/testnet" 16 | 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestClient(t *testing.T) { 22 | cases := []struct { 23 | name string 24 | call func(t *testing.T, client diemclient.Client) 25 | }{ 26 | { 27 | name: "get currencies", 28 | call: func(t *testing.T, client diemclient.Client) { 29 | ret, err := client.GetCurrencies() 30 | require.Nil(t, err) 31 | assert.NotEmpty(t, ret) 32 | assert.True(t, len(ret) > 0) 33 | }, 34 | }, 35 | { 36 | name: "get currencies error", 37 | call: func(t *testing.T, client diemclient.Client) { 38 | client = diemclient.New(testnet.ChainID, "invalid") 39 | ret, err := client.GetCurrencies() 40 | require.Error(t, err) 41 | assert.Nil(t, ret) 42 | }, 43 | }, 44 | { 45 | name: "get metadata", 46 | call: func(t *testing.T, client diemclient.Client) { 47 | ret, err := client.GetMetadata() 48 | require.Nil(t, err) 49 | assert.NotNil(t, ret) 50 | }, 51 | }, 52 | { 53 | name: "get metadata error", 54 | call: func(t *testing.T, client diemclient.Client) { 55 | client = diemclient.New(testnet.ChainID, "invalid") 56 | ret, err := client.GetMetadata() 57 | require.Error(t, err) 58 | assert.Nil(t, ret) 59 | }, 60 | }, 61 | { 62 | name: "get metadata by version", 63 | call: func(t *testing.T, client diemclient.Client) { 64 | ret, err := client.GetMetadataByVersion(1) 65 | require.Nil(t, err) 66 | assert.NotNil(t, ret) 67 | }, 68 | }, 69 | { 70 | name: "get metadata by version error", 71 | call: func(t *testing.T, client diemclient.Client) { 72 | client = diemclient.New(testnet.ChainID, "invalid") 73 | ret, err := client.GetMetadataByVersion(1) 74 | require.Error(t, err) 75 | assert.Nil(t, ret) 76 | }, 77 | }, 78 | { 79 | name: "get account", 80 | call: func(t *testing.T, client diemclient.Client) { 81 | ret, err := client.GetAccount( 82 | diemtypes.MustMakeAccountAddress("0000000000000000000000000A550C18")) 83 | require.Nil(t, err) 84 | assert.NotNil(t, ret) 85 | }, 86 | }, 87 | { 88 | name: "get account not found", 89 | call: func(t *testing.T, client diemclient.Client) { 90 | ret, err := client.GetAccount( 91 | diemtypes.MustMakeAccountAddress("10000000010000000000000010000C18")) 92 | require.Nil(t, err) 93 | assert.Nil(t, ret) 94 | }, 95 | }, 96 | { 97 | name: "get account transaction", 98 | call: func(t *testing.T, client diemclient.Client) { 99 | ret, err := client.GetAccountTransaction( 100 | diemtypes.MustMakeAccountAddress("000000000000000000000000000000DD"), 101 | 0, true) 102 | require.Nil(t, err) 103 | assert.NotNil(t, ret) 104 | }, 105 | }, 106 | { 107 | name: "get account transaction not found", 108 | call: func(t *testing.T, client diemclient.Client) { 109 | ret, err := client.GetAccountTransaction( 110 | diemtypes.MustMakeAccountAddress("10000000010000000000000010000C18"), 111 | 10000000, true) 112 | require.Nil(t, err) 113 | assert.Nil(t, ret) 114 | }, 115 | }, 116 | { 117 | name: "get account transactions", 118 | call: func(t *testing.T, client diemclient.Client) { 119 | ret, err := client.GetAccountTransactions( 120 | diemtypes.MustMakeAccountAddress("000000000000000000000000000000DD"), 121 | 0, 10, true) 122 | require.Nil(t, err) 123 | assert.NotEmpty(t, ret) 124 | }, 125 | }, 126 | { 127 | name: "get account transactions error", 128 | call: func(t *testing.T, client diemclient.Client) { 129 | ret, err := client.GetAccountTransactions( 130 | diemtypes.MustMakeAccountAddress("1668f6be25668c1a17cd8caf6b8d2f2f"), 131 | 0, 10000000000, true) 132 | require.Error(t, err) 133 | assert.Empty(t, ret) 134 | }, 135 | }, 136 | { 137 | name: "get transactions", 138 | call: func(t *testing.T, client diemclient.Client) { 139 | ret, err := client.GetTransactions(0, 10, true) 140 | require.Nil(t, err) 141 | assert.NotEmpty(t, ret) 142 | assert.Len(t, ret, 10) 143 | }, 144 | }, 145 | { 146 | name: "get transactions error", 147 | call: func(t *testing.T, client diemclient.Client) { 148 | ret, err := client.GetTransactions(0, 10000000, true) 149 | require.Error(t, err) 150 | assert.Empty(t, ret) 151 | }, 152 | }, 153 | { 154 | name: "get events", 155 | call: func(t *testing.T, client diemclient.Client) { 156 | account, err := client.GetAccount( 157 | diemtypes.MustMakeAccountAddress("000000000000000000000000000000DD")) 158 | require.NoError(t, err) 159 | 160 | ret, err := client.GetEvents(account.SentEventsKey, 2, 5) 161 | require.Nil(t, err) 162 | assert.NotEmpty(t, ret) 163 | assert.Len(t, ret, 5) 164 | }, 165 | }, 166 | { 167 | name: "get events error", 168 | call: func(t *testing.T, client diemclient.Client) { 169 | ret, err := client.GetEvents( 170 | "00000000000000001668f6be25668c1a17cd8caf6b8d2f2K", 2, 15) 171 | require.Error(t, err) 172 | assert.Empty(t, ret) 173 | }, 174 | }, 175 | { 176 | name: "submit data", 177 | call: func(t *testing.T, client diemclient.Client) { 178 | err := client.Submit("1668f6be25668c1a17cd8caf6b8d2f25") 179 | require.Error(t, err) 180 | jrpcErr, ok := err.(*jsonrpc.ResponseError) 181 | require.True(t, ok) 182 | require.Equal(t, "Invalid params for method 'submit'", jrpcErr.Message) 183 | }, 184 | }, 185 | { 186 | name: "submit: transaction hex-encoded bytes", 187 | call: func(t *testing.T, client diemclient.Client) { 188 | var currencyCode = "XUS" 189 | var sequenceNum uint64 = 0 190 | var amount uint64 = 10 191 | account1 := testnet.GenAccount() 192 | account2 := testnet.GenAccount() 193 | script := stdlib.EncodePeerToPeerWithMetadataScript( 194 | diemtypes.Currency(currencyCode), 195 | account2.AccountAddress(), 196 | amount, nil, nil) 197 | 198 | txn := diemsigner.Sign( 199 | account1, 200 | account1.AccountAddress(), 201 | sequenceNum, 202 | script, 203 | 10000, 0, currencyCode, 204 | uint64(time.Now().Add(time.Second*30).Unix()), 205 | testnet.ChainID, 206 | ) 207 | err := client.Submit(diemtypes.ToHex(txn)) 208 | require.NoError(t, err) 209 | 210 | ret, err := client.WaitForTransaction3( 211 | diemtypes.ToHex(txn), time.Second*5) 212 | require.NoError(t, err) 213 | assert.NotNil(t, ret) 214 | }, 215 | }, 216 | { 217 | name: "submit transaction: with multi-signatures", 218 | call: func(t *testing.T, client diemclient.Client) { 219 | var currencyCode = "XUS" 220 | var sequenceNum uint64 = 0 221 | var amount uint64 = 10 222 | account1 := testnet.GenMultiSigAccount() 223 | address1 := account1.AccountAddress() 224 | 225 | account2 := testnet.GenAccount() 226 | script := stdlib.EncodePeerToPeerWithMetadataScript( 227 | diemtypes.Currency(currencyCode), 228 | account2.AccountAddress(), 229 | amount, nil, nil) 230 | 231 | txn := diemsigner.Sign( 232 | account1, 233 | address1, 234 | sequenceNum, 235 | script, 236 | 10000, 0, currencyCode, 237 | uint64(time.Now().Add(time.Second*30).Unix()), 238 | testnet.ChainID, 239 | ) 240 | err := client.SubmitTransaction(txn) 241 | require.NoError(t, err) 242 | ret, err := client.WaitForTransaction2(txn, time.Second*5) 243 | require.NoError(t, err) 244 | assert.NotNil(t, ret) 245 | }, 246 | }, 247 | { 248 | name: "submit: ignores stale response", 249 | call: func(t *testing.T, client diemclient.Client) { 250 | var currencyCode = "XUS" 251 | var sequenceNum uint64 = 0 252 | var amount uint64 = 10 253 | account1 := testnet.GenAccount() 254 | account2 := testnet.GenAccount() 255 | script := stdlib.EncodePeerToPeerWithMetadataScript( 256 | diemtypes.Currency(currencyCode), 257 | account2.AccountAddress(), 258 | amount, nil, nil) 259 | 260 | txn := diemsigner.Sign( 261 | account1, 262 | account1.AccountAddress(), 263 | sequenceNum, 264 | script, 265 | 10000, 0, currencyCode, 266 | uint64(time.Now().Add(time.Second*30).Unix()), 267 | testnet.ChainID, 268 | ) 269 | state := client.LastResponseLedgerState() 270 | client.UpdateLastResponseLedgerState(diemclient.LedgerState{ 271 | TimestampUsec: state.TimestampUsec * 2, 272 | Version: state.Version + 1000000, 273 | }) 274 | err := client.Submit(diemtypes.ToHex(txn)) 275 | require.NoError(t, err) 276 | 277 | // use a fresh client to check transaction is submitted and executed successfully 278 | fresh_client := diemclient.New(testnet.ChainID, testnet.URL) 279 | fresh_client.UpdateLastResponseLedgerState(state) 280 | ret, err := fresh_client.WaitForTransaction3( 281 | diemtypes.ToHex(txn), time.Second*5) 282 | require.NoError(t, err) 283 | assert.NotNil(t, ret) 284 | }, 285 | }, 286 | } 287 | 288 | for _, tc := range cases { 289 | t.Run(tc.name, func(t *testing.T) { 290 | client := diemclient.New(testnet.ChainID, testnet.URL) 291 | tc.call(t, client) 292 | }) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /diemclient/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemclient 5 | 6 | import ( 7 | "github.com/diem/client-sdk-go/diemjsonrpctypes" 8 | ) 9 | 10 | // Amount represents currency amount 11 | type Amount = diemjsonrpctypes.Amount 12 | 13 | // CurrencyInfo is get_currencies response 14 | type CurrencyInfo = diemjsonrpctypes.CurrencyInfo 15 | 16 | // AccountRole represents role specific data for account 17 | type AccountRole = diemjsonrpctypes.AccountRole 18 | 19 | // Account is get_account method response 20 | type Account = diemjsonrpctypes.Account 21 | 22 | // EventData is event type specific data 23 | type EventData = diemjsonrpctypes.EventData 24 | 25 | // Event data 26 | type Event = diemjsonrpctypes.Event 27 | 28 | // Metadata is get_metadata method response 29 | type Metadata = diemjsonrpctypes.Metadata 30 | 31 | // Script represents decoded transaction script arguments 32 | type Script = diemjsonrpctypes.Script 33 | 34 | // TransactionData include specific type transaction details 35 | type TransactionData = diemjsonrpctypes.TransactionData 36 | 37 | // VmStatus represents transaction execution result and error info 38 | type VmStatus = diemjsonrpctypes.VMStatus 39 | 40 | // Transaction represents executed / failed transaction 41 | type Transaction = diemjsonrpctypes.Transaction 42 | 43 | // StateProof is get_state_proof response 44 | type StateProof = diemjsonrpctypes.StateProof 45 | 46 | // AccountStateProof represents account state blob proof 47 | type AccountStateProof = diemjsonrpctypes.AccountStateProof 48 | 49 | // AccountStateWithProof is get_account_state_with_proof response 50 | type AccountStateWithProof = diemjsonrpctypes.AccountStateWithProof 51 | -------------------------------------------------------------------------------- /diemid/account.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // This file implemenets Diem Account Identifier proposal 5 | // https://github.com/diem/lip/blob/master/lips/lip-5.md 6 | 7 | package diemid 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/diem/client-sdk-go/diemid/bech32" 13 | "github.com/diem/client-sdk-go/diemtypes" 14 | ) 15 | 16 | const ( 17 | // MainnetPrefix is mainnet account identifier prefix 18 | MainnetPrefix NetworkPrefix = "dm" 19 | // TestnetPrefix is testnet account identifier prefix 20 | TestnetPrefix NetworkPrefix = "tdm" 21 | PreMainnetPrefix NetworkPrefix = "pdm" 22 | DryRunMainnetPrefix NetworkPrefix = "ddm" 23 | 24 | // version 1 25 | V1 byte = 1 26 | 27 | // account address length 28 | AccountAddressLength = diemtypes.AccountAddressLength 29 | SubAddressLength = diemtypes.SubAddressLength 30 | ) 31 | 32 | // NetworkPrefix is account identifier prefix type 33 | type NetworkPrefix string 34 | 35 | // Account captures all parts of account identifier 36 | type Account struct { 37 | Prefix NetworkPrefix 38 | Version byte 39 | AccountAddress diemtypes.AccountAddress 40 | SubAddress diemtypes.SubAddress 41 | } 42 | 43 | // NewAccount create new Account with version set to v1. 44 | // Set subAddress == nil for no sub-address case. 45 | func NewAccount(prefix NetworkPrefix, accountAddress diemtypes.AccountAddress, subAddress diemtypes.SubAddress) *Account { 46 | return &Account{ 47 | Prefix: prefix, 48 | Version: V1, 49 | AccountAddress: accountAddress, 50 | SubAddress: subAddress, 51 | } 52 | } 53 | 54 | // EncodeAccount creates account v1 encode string 55 | // Set subAddress == nil for no sub-address case. 56 | func EncodeAccount(prefix NetworkPrefix, accountAddress diemtypes.AccountAddress, subAddress diemtypes.SubAddress) (string, error) { 57 | return NewAccount(prefix, accountAddress, subAddress).Encode() 58 | } 59 | 60 | // DecodeToAccount decode given encoded account identifier string to `Account`. 61 | // Given NetworkPrefix is used to validate account identifier network prefix, and returns error 62 | // if the network prefix mismatched. 63 | func DecodeToAccount(prefix NetworkPrefix, encodedAccountIdentifier string) (*Account, error) { 64 | version, data, err := bech32.SegwitAddrDecode(string(prefix), encodedAccountIdentifier) 65 | if err != nil { 66 | return nil, err 67 | } 68 | if len(data) != AccountAddressLength+SubAddressLength { 69 | return nil, errors.New("invalid account identifier, account address and sub-address length does not match") 70 | } 71 | 72 | address, _ := diemtypes.MakeAccountAddressFromBytes( 73 | ints2bytes(data[:AccountAddressLength])) 74 | subAddress, _ := diemtypes.MakeSubAddressFromBytes( 75 | ints2bytes(data[AccountAddressLength:])) 76 | return &Account{ 77 | Prefix: prefix, 78 | Version: byte(version), 79 | AccountAddress: address, 80 | SubAddress: subAddress, 81 | }, nil 82 | } 83 | 84 | // Encode encodes Account into SegwitAddr string 85 | func (ai *Account) Encode() (string, error) { 86 | if len(ai.SubAddress) != SubAddressLength { 87 | return "", errors.New("invalid sub address") 88 | } 89 | data := make([]byte, 0, AccountAddressLength+SubAddressLength) 90 | data = append(data, ai.AccountAddress[:]...) 91 | data = append(data, ai.SubAddress[:]...) 92 | 93 | return bech32.SegwitAddrEncode(string(ai.Prefix), int(ai.Version), bytes2ints(data)) 94 | } 95 | 96 | func bytes2ints(bs []byte) []int { 97 | ret := make([]int, len(bs)) 98 | for i, b := range bs { 99 | ret[i] = int(b) 100 | } 101 | return ret 102 | } 103 | 104 | func ints2bytes(is []int) []byte { 105 | ret := make([]byte, len(is)) 106 | for i, b := range is { 107 | ret[i] = byte(b) 108 | } 109 | return ret 110 | } 111 | -------------------------------------------------------------------------------- /diemid/account_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemid_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemid" 10 | "github.com/diem/client-sdk-go/diemid/bech32" 11 | "github.com/diem/client-sdk-go/diemtypes" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestEncodeDecodeAccountIdentifier(t *testing.T) { 17 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 18 | subAddress, _ := diemtypes.MakeSubAddress("cf64428bdeb62af2") 19 | 20 | ret, err := diemid.EncodeAccount(diemid.MainnetPrefix, address, subAddress) 21 | require.NoError(t, err) 22 | assert.Equal(t, "dm1p7ujcndcl7nudzwt8fglhx6wxn08kgs5tm6mz4us2vfufk", string(ret)) 23 | 24 | id, err := diemid.DecodeToAccount(diemid.MainnetPrefix, ret) 25 | require.NoError(t, err) 26 | assert.Equal(t, "f72589b71ff4f8d139674a3f7369c69b", id.AccountAddress.Hex()) 27 | assert.Equal(t, "cf64428bdeb62af2", id.SubAddress.Hex()) 28 | assert.Equal(t, byte(1), id.Version) 29 | assert.Equal(t, diemid.MainnetPrefix, id.Prefix) 30 | } 31 | 32 | func TestEncodeDecodeAccountIdentifierWithoutSubAddress(t *testing.T) { 33 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 34 | 35 | ret, err := diemid.EncodeAccount(diemid.MainnetPrefix, address, diemtypes.EmptySubAddress) 36 | require.NoError(t, err) 37 | assert.Equal(t, "dm1p7ujcndcl7nudzwt8fglhx6wxnvqqqqqqqqqqqqqd8p9cq", string(ret)) 38 | 39 | id, err := diemid.DecodeToAccount(diemid.MainnetPrefix, ret) 40 | require.NoError(t, err) 41 | assert.Equal(t, "f72589b71ff4f8d139674a3f7369c69b", id.AccountAddress.Hex()) 42 | assert.Equal(t, "0000000000000000", id.SubAddress.Hex()) 43 | assert.Equal(t, byte(1), id.Version) 44 | assert.Equal(t, diemid.MainnetPrefix, id.Prefix) 45 | } 46 | 47 | func TestDecodeInvalidAccountIdentifierString(t *testing.T) { 48 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 49 | subAddress, _ := diemtypes.MakeSubAddress("cf64428bdeb62af2") 50 | t.Run("invalid checksum", func(t *testing.T) { 51 | ret, err := diemid.EncodeAccount(diemid.MainnetPrefix, address, subAddress) 52 | require.NoError(t, err) 53 | id, err := diemid.DecodeToAccount(diemid.MainnetPrefix, ret[:len(ret)-1]) 54 | require.Error(t, err) 55 | assert.Nil(t, id) 56 | assert.Contains(t, err.Error(), "invalid checksum") 57 | }) 58 | t.Run("invalid account address length", func(t *testing.T) { 59 | data := make([]int, diemtypes.AccountAddressLength) 60 | for i, b := range address { 61 | data[i] = int(b) 62 | } 63 | 64 | encoded, err := bech32.SegwitAddrEncode(string(diemid.MainnetPrefix), 1, data) 65 | require.NoError(t, err) 66 | 67 | id, err := diemid.DecodeToAccount(diemid.MainnetPrefix, encoded) 68 | require.Error(t, err) 69 | assert.Nil(t, id) 70 | assert.Contains(t, err.Error(), "invalid account identifier") 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /diemid/bech32/bech32.go: -------------------------------------------------------------------------------- 1 | // Package bech32 reference implementation for Bech32 and segwit addresses. 2 | // Copyright (c) 2017 Takatoshi Nakagawa 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | package bech32 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "strings" 27 | ) 28 | 29 | var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 30 | 31 | var generator = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} 32 | 33 | func polymod(values []int) int { 34 | chk := 1 35 | for _, v := range values { 36 | top := chk >> 25 37 | chk = (chk&0x1ffffff)<<5 ^ v 38 | for i := 0; i < 5; i++ { 39 | if (top>>uint(i))&1 == 1 { 40 | chk ^= generator[i] 41 | } 42 | } 43 | } 44 | return chk 45 | } 46 | 47 | func hrpExpand(hrp string) []int { 48 | ret := []int{} 49 | for _, c := range hrp { 50 | ret = append(ret, int(c>>5)) 51 | } 52 | ret = append(ret, 0) 53 | for _, c := range hrp { 54 | ret = append(ret, int(c&31)) 55 | } 56 | return ret 57 | } 58 | 59 | func verifyChecksum(hrp string, data []int) bool { 60 | return polymod(append(hrpExpand(hrp), data...)) == 1 61 | } 62 | 63 | func createChecksum(hrp string, data []int) []int { 64 | values := append(append(hrpExpand(hrp), data...), []int{0, 0, 0, 0, 0, 0}...) 65 | mod := polymod(values) ^ 1 66 | ret := make([]int, 6) 67 | for p := 0; p < len(ret); p++ { 68 | ret[p] = (mod >> uint(5*(5-p))) & 31 69 | } 70 | return ret 71 | } 72 | 73 | // Encode encodes hrp(human-readable part) and data(32bit data array), returns Bech32 / or error 74 | // if hrp is uppercase, return uppercase Bech32 75 | func Encode(hrp string, data []int) (string, error) { 76 | if (len(hrp) + len(data) + 7) > 90 { 77 | return "", fmt.Errorf("too long : hrp length=%d, data length=%d", len(hrp), len(data)) 78 | } 79 | if len(hrp) < 1 { 80 | return "", fmt.Errorf("invalid hrp : hrp=%v", hrp) 81 | } 82 | for p, c := range hrp { 83 | if c < 33 || c > 126 { 84 | return "", fmt.Errorf("invalid character human-readable part : hrp[%d]=%d", p, c) 85 | } 86 | } 87 | if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp { 88 | return "", fmt.Errorf("mix case : hrp=%v", hrp) 89 | } 90 | lower := strings.ToLower(hrp) == hrp 91 | hrp = strings.ToLower(hrp) 92 | combined := append(data, createChecksum(hrp, data)...) 93 | var ret bytes.Buffer 94 | ret.WriteString(hrp) 95 | ret.WriteString("1") 96 | for idx, p := range combined { 97 | if p < 0 || p >= len(charset) { 98 | return "", fmt.Errorf("invalid data : data[%d]=%d", idx, p) 99 | } 100 | ret.WriteByte(charset[p]) 101 | } 102 | if lower { 103 | return ret.String(), nil 104 | } 105 | return strings.ToUpper(ret.String()), nil 106 | } 107 | 108 | // Decode decodes bechString(Bech32) returns hrp(human-readable part) and data(32bit data array) / or error 109 | func Decode(bechString string) (string, []int, error) { 110 | if len(bechString) > 90 { 111 | return "", nil, fmt.Errorf("too long : len=%d", len(bechString)) 112 | } 113 | if strings.ToLower(bechString) != bechString && strings.ToUpper(bechString) != bechString { 114 | return "", nil, fmt.Errorf("mixed case") 115 | } 116 | bechString = strings.ToLower(bechString) 117 | pos := strings.LastIndex(bechString, "1") 118 | if pos < 1 || pos+7 > len(bechString) { 119 | return "", nil, fmt.Errorf("separator '1' at invalid position : pos=%d , len=%d", pos, len(bechString)) 120 | } 121 | hrp := bechString[0:pos] 122 | for p, c := range hrp { 123 | if c < 33 || c > 126 { 124 | return "", nil, fmt.Errorf("invalid character human-readable part : bechString[%d]=%d", p, c) 125 | } 126 | } 127 | data := []int{} 128 | for p := pos + 1; p < len(bechString); p++ { 129 | d := strings.Index(charset, fmt.Sprintf("%c", bechString[p])) 130 | if d == -1 { 131 | return "", nil, fmt.Errorf("invalid character data part : bechString[%d]=%d", p, bechString[p]) 132 | } 133 | data = append(data, d) 134 | } 135 | if !verifyChecksum(hrp, data) { 136 | return "", nil, fmt.Errorf("invalid checksum") 137 | } 138 | return hrp, data[:len(data)-6], nil 139 | } 140 | 141 | func convertbits(data []int, frombits, tobits uint, pad bool) ([]int, error) { 142 | acc := 0 143 | bits := uint(0) 144 | ret := []int{} 145 | maxv := (1 << tobits) - 1 146 | for idx, value := range data { 147 | if value < 0 || (value>>frombits) != 0 { 148 | return nil, fmt.Errorf("invalid data range : data[%d]=%d (frombits=%d)", idx, value, frombits) 149 | } 150 | acc = (acc << frombits) | value 151 | bits += frombits 152 | for bits >= tobits { 153 | bits -= tobits 154 | ret = append(ret, (acc>>bits)&maxv) 155 | } 156 | } 157 | if pad { 158 | if bits > 0 { 159 | ret = append(ret, (acc<<(tobits-bits))&maxv) 160 | } 161 | } else if bits >= frombits { 162 | return nil, fmt.Errorf("illegal zero padding") 163 | } else if ((acc << (tobits - bits)) & maxv) != 0 { 164 | return nil, fmt.Errorf("non-zero padding") 165 | } 166 | return ret, nil 167 | } 168 | 169 | // SegwitAddrDecode decodes hrp(human-readable part) Segwit Address(string), returns version(int) and data(bytes array) / or error 170 | func SegwitAddrDecode(hrp, addr string) (int, []int, error) { 171 | dechrp, data, err := Decode(addr) 172 | if err != nil { 173 | return -1, nil, err 174 | } 175 | if dechrp != hrp { 176 | return -1, nil, fmt.Errorf("invalid human-readable part : %s != %s", hrp, dechrp) 177 | } 178 | if len(data) < 1 { 179 | return -1, nil, fmt.Errorf("invalid decode data length : %d", len(data)) 180 | } 181 | if data[0] > 16 { 182 | return -1, nil, fmt.Errorf("invalid witness version : %d", data[0]) 183 | } 184 | res, err := convertbits(data[1:], 5, 8, false) 185 | if err != nil { 186 | return -1, nil, err 187 | } 188 | if len(res) < 2 || len(res) > 40 { 189 | return -1, nil, fmt.Errorf("invalid convertbits length : %d", len(res)) 190 | } 191 | if data[0] == 0 && len(res) != 20 && len(res) != 32 { 192 | return -1, nil, fmt.Errorf("invalid program length for witness version 0 (per BIP141) : %d", len(res)) 193 | } 194 | return data[0], res, nil 195 | } 196 | 197 | // SegwitAddrEncode encodes hrp(human-readable part) , version(int) and data(bytes array), returns Segwit Address / or error 198 | func SegwitAddrEncode(hrp string, version int, program []int) (string, error) { 199 | if version < 0 || version > 16 { 200 | return "", fmt.Errorf("invalid witness version : %d", version) 201 | } 202 | if len(program) < 2 || len(program) > 40 { 203 | return "", fmt.Errorf("invalid program length : %d", len(program)) 204 | } 205 | if version == 0 && len(program) != 20 && len(program) != 32 { 206 | return "", fmt.Errorf("invalid program length for witness version 0 (per BIP141) : %d", len(program)) 207 | } 208 | data, err := convertbits(program, 8, 5, true) 209 | if err != nil { 210 | return "", err 211 | } 212 | ret, err := Encode(hrp, append([]int{version}, data...)) 213 | if err != nil { 214 | return "", err 215 | } 216 | return ret, nil 217 | } 218 | -------------------------------------------------------------------------------- /diemid/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides utility functions for Diem Intent Identifier and Account Identifier 5 | // (https://github.com/diem/lip/blob/master/lips/lip-5.md) 6 | package diemid 7 | -------------------------------------------------------------------------------- /diemid/intent.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // This file implemenets Diem Intent Identifier proposal 5 | // https://github.com/diem/lip/blob/master/lips/lip-5.md 6 | 7 | package diemid 8 | 9 | import ( 10 | "fmt" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | const ( 16 | DiemScheme = "diem" 17 | CurrencyParamName = "c" 18 | AmountParamName = "am" 19 | ) 20 | 21 | // Params for Intent 22 | type Params struct { 23 | Currency string 24 | Amount *uint64 25 | } 26 | 27 | // Intent captures all parts of intent identifier 28 | type Intent struct { 29 | Account Account 30 | Params Params 31 | } 32 | 33 | // DecodeToIntent decode given intent string to `Intent`. 34 | // Given `networkPrefix` is used to validate intent account identifier network prefix. 35 | func DecodeToIntent(networkPrefix NetworkPrefix, intent string) (*Intent, error) { 36 | u, err := url.ParseRequestURI(intent) 37 | if err != nil { 38 | return nil, fmt.Errorf("invalid intent identifier: %s", err.Error()) 39 | } 40 | if u.Scheme != DiemScheme { 41 | return nil, fmt.Errorf("invalid intent scheme: %s", u.Scheme) 42 | } 43 | account, err := DecodeToAccount(networkPrefix, u.Host) 44 | if err != nil { 45 | return nil, fmt.Errorf("invalid account identifier: %s", err.Error()) 46 | } 47 | return &Intent{ 48 | Account: *account, 49 | Params: Params{ 50 | Currency: u.Query().Get(CurrencyParamName), 51 | Amount: toIntPtr(u.Query().Get(AmountParamName)), 52 | }, 53 | }, nil 54 | } 55 | 56 | func (i *Intent) Encode() (string, error) { 57 | encoded, err := i.Account.Encode() 58 | if err != nil { 59 | return "", fmt.Errorf("encode account identifier failed: %s", err.Error()) 60 | } 61 | u := url.URL{Scheme: DiemScheme, Host: encoded} 62 | q := u.Query() 63 | if i.Params.Currency != "" { 64 | q.Add(CurrencyParamName, i.Params.Currency) 65 | } 66 | if i.Params.Amount != nil { 67 | q.Add(AmountParamName, strconv.FormatUint(*i.Params.Amount, 10)) 68 | } 69 | u.RawQuery = q.Encode() 70 | return u.String(), nil 71 | } 72 | 73 | func toIntPtr(str string) *uint64 { 74 | ret, err := strconv.ParseUint(str, 10, 64) 75 | if err != nil { 76 | return nil 77 | } 78 | return &ret 79 | } 80 | -------------------------------------------------------------------------------- /diemid/intent_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemid_test 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/diem/client-sdk-go/diemid" 11 | "github.com/diem/client-sdk-go/diemtypes" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestEncodeDecodeIntent(t *testing.T) { 17 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 18 | subAddress, _ := diemtypes.MakeSubAddress("cf64428bdeb62af2") 19 | account := diemid.NewAccount(diemid.MainnetPrefix, address, subAddress) 20 | accountEncode, _ := account.Encode() 21 | 22 | t.Run("without params", func(t *testing.T) { 23 | intent := diemid.Intent{Account: *account} 24 | intentEncode, err := intent.Encode() 25 | require.NoError(t, err) 26 | assert.Equal(t, fmt.Sprintf("diem://%s", accountEncode), intentEncode) 27 | 28 | ret, err := diemid.DecodeToIntent(diemid.MainnetPrefix, intentEncode) 29 | require.NoError(t, err) 30 | require.NotNil(t, ret) 31 | assert.Equal(t, intent, *ret) 32 | }) 33 | 34 | t.Run("with params", func(t *testing.T) { 35 | amount := uint64(123) 36 | intent := diemid.Intent{ 37 | Account: *account, 38 | Params: diemid.Params{ 39 | Currency: "XUS", 40 | Amount: &amount, 41 | }, 42 | } 43 | intentEncode, err := intent.Encode() 44 | require.NoError(t, err) 45 | assert.Equal(t, fmt.Sprintf("diem://%s?am=123&c=XUS", accountEncode), intentEncode) 46 | 47 | ret, err := diemid.DecodeToIntent(diemid.MainnetPrefix, intentEncode) 48 | require.NoError(t, err) 49 | require.NotNil(t, ret) 50 | assert.Equal(t, intent, *ret) 51 | }) 52 | } 53 | 54 | func TestDecodeIntentErrors(t *testing.T) { 55 | t.Run("invalid url", func(t *testing.T) { 56 | ret, err := diemid.DecodeToIntent(diemid.MainnetPrefix, "s/s/###...") 57 | require.Error(t, err) 58 | require.Nil(t, ret) 59 | assert.Contains(t, err.Error(), "invalid intent identifier") 60 | }) 61 | t.Run("invalid scheme", func(t *testing.T) { 62 | ret, err := diemid.DecodeToIntent(diemid.MainnetPrefix, "http://account") 63 | require.Error(t, err) 64 | require.Nil(t, ret) 65 | assert.Contains(t, err.Error(), "invalid intent scheme") 66 | }) 67 | t.Run("invalid account identifier", func(t *testing.T) { 68 | ret, err := diemid.DecodeToIntent(diemid.MainnetPrefix, "diem://accountid") 69 | require.Error(t, err) 70 | require.Nil(t, ret) 71 | assert.Contains(t, err.Error(), "invalid account identifier") 72 | }) 73 | } 74 | 75 | func TestEncodeIntentErrors(t *testing.T) { 76 | t.Run("invalid account identifier", func(t *testing.T) { 77 | intent := diemid.Intent{Account: diemid.Account{}} 78 | ret, err := intent.Encode() 79 | require.Error(t, err) 80 | require.Empty(t, ret) 81 | assert.Contains(t, err.Error(), "encode account identifier failed") 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /diemjsonrpctypes/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides Diem JSON-RPC API response data types. 5 | // Generated from schema defined by protobuf. 6 | package diemjsonrpctypes 7 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-transaction-with-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-transaction-with-events", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account_transactions", 7 | "params": [ 8 | "876ff1d441cb0b352f438fcfbce8608f", 9 | 1, 10 | 3, 11 | true 12 | ] 13 | }, 14 | "response": { 15 | "id": 1, 16 | "jsonrpc": "2.0", 17 | "diem_chain_id": 4, 18 | "diem_ledger_timestampusec": 1600152974894366, 19 | "diem_ledger_version": 45, 20 | "result": { 21 | "bytes": "00876ff1d441cb0b352f438fcfbce8608f010000000000000001e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e1342501c0c62d00000000000400040040420f00000000000000000000000000034c4252a765605f00000000040020f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e54079358f2a818adcbfb7e9db3e9a560007881acfe6b62c29f1eedcc76b534024a574fcb95bf4c438da33eba8052b565c8d7e3f507225d2998849d6613f64fa6005", 22 | "events": [ 23 | { 24 | "data": { 25 | "amount": { 26 | "amount": 3000000, 27 | "currency": "LBR" 28 | }, 29 | "metadata": "metadata", 30 | "receiver": "762cbea8b99911d49707d2b901e13425", 31 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 32 | "type": "sentpayment" 33 | }, 34 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 35 | "sequence_number": 1, 36 | "transaction_version": 27 37 | }, 38 | { 39 | "data": { 40 | "amount": { 41 | "amount": 3000000, 42 | "currency": "LBR" 43 | }, 44 | "metadata": "metadata", 45 | "receiver": "762cbea8b99911d49707d2b901e13425", 46 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 47 | "type": "receivedpayment" 48 | }, 49 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 50 | "sequence_number": 1, 51 | "transaction_version": 27 52 | } 53 | ], 54 | "gas_used": 476, 55 | "hash": "0895ced78e9f10c1351f248b9bda40cc9c8e6d4dbba823e08fe1cfacd582f171", 56 | "transaction": { 57 | "chain_id": 4, 58 | "expiration_timestamp_secs": 1600152999, 59 | "gas_currency": "LBR", 60 | "gas_unit_price": 10, 61 | "max_gas_amount": 1000000, 62 | "public_key": "f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e5", 63 | "script": { 64 | "amount": 3000000, 65 | "currency": "LBR", 66 | "metadata": "metadata", 67 | "metadata_signature": "metadata_signature", 68 | "receiver": "762cbea8b99911d49707d2b901e13425", 69 | "type": "peer_to_peer_transaction" 70 | }, 71 | "script_bytes": "e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e1342501c0c62d000000000004000400", 72 | "script_hash": "61749d43d8f10940be6944df85ddf13f0f8fb830269c601f481cc5ee3de731c8", 73 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 74 | "sequence_number": 1, 75 | "signature": "79358f2a818adcbfb7e9db3e9a560007881acfe6b62c29f1eedcc76b534024a574fcb95bf4c438da33eba8052b565c8d7e3f507225d2998849d6613f64fa6005", 76 | "signature_scheme": "Scheme::Ed25519", 77 | "type": "user" 78 | }, 79 | "version": 27, 80 | "vm_status": { 81 | "type": "executed" 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-transactions-with-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-transactions-with-events", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account_transactions", 7 | "params": [ 8 | "876ff1d441cb0b352f438fcfbce8608f", 9 | 1, 10 | 3, 11 | true 12 | ] 13 | }, 14 | "response": { 15 | "id": 1, 16 | "jsonrpc": "2.0", 17 | "diem_chain_id": 4, 18 | "diem_ledger_timestampusec": 1600152974894366, 19 | "diem_ledger_version": 45, 20 | "result": [ 21 | { 22 | "bytes": "00876ff1d441cb0b352f438fcfbce8608f010000000000000001e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e1342501c0c62d00000000000400040040420f00000000000000000000000000034c4252a765605f00000000040020f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e54079358f2a818adcbfb7e9db3e9a560007881acfe6b62c29f1eedcc76b534024a574fcb95bf4c438da33eba8052b565c8d7e3f507225d2998849d6613f64fa6005", 23 | "events": [ 24 | { 25 | "data": { 26 | "amount": { 27 | "amount": 3000000, 28 | "currency": "XDX" 29 | }, 30 | "metadata": "metadata", 31 | "receiver": "762cbea8b99911d49707d2b901e13425", 32 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 33 | "type": "sentpayment" 34 | }, 35 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 36 | "sequence_number": 1, 37 | "transaction_version": 27 38 | }, 39 | { 40 | "data": { 41 | "amount": { 42 | "amount": 3000000, 43 | "currency": "XDX" 44 | }, 45 | "metadata": "metadata", 46 | "receiver": "762cbea8b99911d49707d2b901e13425", 47 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 48 | "type": "receivedpayment" 49 | }, 50 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 51 | "sequence_number": 1, 52 | "transaction_version": 27 53 | } 54 | ], 55 | "gas_used": 476, 56 | "hash": "0895ced78e9f10c1351f248b9bda40cc9c8e6d4dbba823e08fe1cfacd582f171", 57 | "transaction": { 58 | "chain_id": 4, 59 | "expiration_timestamp_secs": 1600152999, 60 | "gas_currency": "XDX", 61 | "gas_unit_price": 10, 62 | "max_gas_amount": 1000000, 63 | "public_key": "f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e5", 64 | "script": { 65 | "amount": 3000000, 66 | "currency": "XDX", 67 | "metadata": "metadata", 68 | "metadata_signature": "metadata_signature", 69 | "receiver": "762cbea8b99911d49707d2b901e13425", 70 | "type": "peer_to_peer_transaction" 71 | }, 72 | "script_bytes": "e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e1342501c0c62d000000000004000400", 73 | "script_hash": "61749d43d8f10940be6944df85ddf13f0f8fb830269c601f481cc5ee3de731c8", 74 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 75 | "sequence_number": 1, 76 | "signature": "79358f2a818adcbfb7e9db3e9a560007881acfe6b62c29f1eedcc76b534024a574fcb95bf4c438da33eba8052b565c8d7e3f507225d2998849d6613f64fa6005", 77 | "signature_scheme": "Scheme::Ed25519", 78 | "type": "user" 79 | }, 80 | "version": 27, 81 | "vm_status": { 82 | "type": "executed" 83 | } 84 | }, 85 | { 86 | "bytes": "00876ff1d441cb0b352f438fcfbce8608f020000000000000001e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e134250100093d00000000000400040040420f00000000000000000000000000034c4252a765605f00000000040020f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e540e71960042a94378962331af53f472de5086aff7889e74c097a7655db3b007da09a7c9cc94d499dc609174338ae2ba261f1e502c567ff94eef8305b7595d5a904", 87 | "events": [ 88 | { 89 | "data": { 90 | "amount": { 91 | "amount": 4000000, 92 | "currency": "XDX" 93 | }, 94 | "metadata": "metadata", 95 | "receiver": "762cbea8b99911d49707d2b901e13425", 96 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 97 | "type": "sentpayment" 98 | }, 99 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 100 | "sequence_number": 2, 101 | "transaction_version": 33 102 | }, 103 | { 104 | "data": { 105 | "amount": { 106 | "amount": 4000000, 107 | "currency": "XDX" 108 | }, 109 | "metadata": "metadata", 110 | "receiver": "762cbea8b99911d49707d2b901e13425", 111 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 112 | "type": "receivedpayment" 113 | }, 114 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 115 | "sequence_number": 2, 116 | "transaction_version": 33 117 | } 118 | ], 119 | "gas_used": 476, 120 | "hash": "ed70ff4634cd26f6a269ae7f9723b4aac476fe7951a5d5dd42980991d763badb", 121 | "transaction": { 122 | "chain_id": 4, 123 | "expiration_timestamp_secs": 1600152999, 124 | "gas_currency": "XDX", 125 | "gas_unit_price": 10, 126 | "max_gas_amount": 1000000, 127 | "public_key": "f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e5", 128 | "script": { 129 | "amount": 4000000, 130 | "currency": "XDX", 131 | "metadata": "metadata", 132 | "metadata_signature": "metadata_signature", 133 | "receiver": "762cbea8b99911d49707d2b901e13425", 134 | "type": "peer_to_peer_transaction" 135 | }, 136 | "script_bytes": "e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e134250100093d000000000004000400", 137 | "script_hash": "61749d43d8f10940be6944df85ddf13f0f8fb830269c601f481cc5ee3de731c8", 138 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 139 | "sequence_number": 2, 140 | "signature": "e71960042a94378962331af53f472de5086aff7889e74c097a7655db3b007da09a7c9cc94d499dc609174338ae2ba261f1e502c567ff94eef8305b7595d5a904", 141 | "signature_scheme": "Scheme::Ed25519", 142 | "type": "user" 143 | }, 144 | "version": 33, 145 | "vm_status": { 146 | "type": "executed" 147 | } 148 | }, 149 | { 150 | "bytes": "00876ff1d441cb0b352f438fcfbce8608f030000000000000001e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e134250100093d00000000000400040040420f00000000000000000000000000034c4252a765605f00000000040020f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e5405c6ccf9b6c0406a3fc3208a24399dfa058fcf82a54197a612ebde25e1b02a120d820a5900a50ffaa58c4fcd68ea4a356d5851515d7e36738fda937361dd2b20f", 151 | "events": [ 152 | { 153 | "data": { 154 | "amount": { 155 | "amount": 4000000, 156 | "currency": "XDX" 157 | }, 158 | "metadata": "metadata", 159 | "receiver": "762cbea8b99911d49707d2b901e13425", 160 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 161 | "type": "sentpayment" 162 | }, 163 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 164 | "sequence_number": 3, 165 | "transaction_version": 38 166 | }, 167 | { 168 | "data": { 169 | "amount": { 170 | "amount": 4000000, 171 | "currency": "XDX" 172 | }, 173 | "metadata": "metadata", 174 | "receiver": "762cbea8b99911d49707d2b901e13425", 175 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 176 | "type": "receivedpayment" 177 | }, 178 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 179 | "sequence_number": 3, 180 | "transaction_version": 38 181 | } 182 | ], 183 | "gas_used": 476, 184 | "hash": "738d639cae17ae47454bd03688d6199fcde45cc0c24619a6cb7472f2123df446", 185 | "transaction": { 186 | "chain_id": 4, 187 | "expiration_timestamp_secs": 1600152999, 188 | "gas_currency": "XDX", 189 | "gas_unit_price": 10, 190 | "max_gas_amount": 1000000, 191 | "public_key": "f4d854d7719c8ca6b454df278aa513c3e1f17ad6a38579ff0baa2bd50f5dd0e5", 192 | "script": { 193 | "amount": 4000000, 194 | "currency": "XDX", 195 | "metadata": "metadata", 196 | "metadata_signature": "metadata_signature", 197 | "receiver": "762cbea8b99911d49707d2b901e13425", 198 | "type": "peer_to_peer_transaction" 199 | }, 200 | "script_bytes": "e101a11ceb0b010000000701000202020403061004160205181d0735610896011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000c4c696272614163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b05110202010700000000000000000000000000000001034c4252034c4252000403762cbea8b99911d49707d2b901e134250100093d000000000004000400", 201 | "script_hash": "61749d43d8f10940be6944df85ddf13f0f8fb830269c601f481cc5ee3de731c8", 202 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 203 | "sequence_number": 3, 204 | "signature": "5c6ccf9b6c0406a3fc3208a24399dfa058fcf82a54197a612ebde25e1b02a120d820a5900a50ffaa58c4fcd68ea4a356d5851515d7e36738fda937361dd2b20f", 205 | "signature_scheme": "Scheme::Ed25519", 206 | "type": "user" 207 | }, 208 | "version": 38, 209 | "vm_status": { 210 | "type": "executed" 211 | } 212 | } 213 | ] 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-with-child-vasp-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-with-child-vasp-role", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account", 7 | "params": [ 8 | "762cbea8b99911d49707d2b901e13425" 9 | ] 10 | }, 11 | "response": { 12 | "id": 1, 13 | "jsonrpc": "2.0", 14 | "diem_chain_id": 4, 15 | "diem_ledger_timestampusec": 1600152974894366, 16 | "diem_ledger_version": 45, 17 | "result": { 18 | "address": "762cbea8b99911d49707d2b901e13425", 19 | "authentication_key": "5d378ee8381f5b39f84d4c3cd1618b17762cbea8b99911d49707d2b901e13425", 20 | "balances": [ 21 | { 22 | "amount": 8000000, 23 | "currency": "LBR" 24 | } 25 | ], 26 | "delegated_key_rotation_capability": true, 27 | "delegated_withdrawal_capability": true, 28 | "is_frozen": true, 29 | "received_events_key": "0000000000000000762cbea8b99911d49707d2b901e13425", 30 | "role": { 31 | "parent_vasp_address": "876ff1d441cb0b352f438fcfbce8608f", 32 | "type": "child_vasp" 33 | }, 34 | "sent_events_key": "0100000000000000762cbea8b99911d49707d2b901e13425", 35 | "sequence_number": 1 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-with-designated-dealer-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-with-designated-dealer-role", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account", 7 | "params": [ 8 | "000000000000000000000000000000dd" 9 | ] 10 | }, 11 | "response": { 12 | "id": 1, 13 | "jsonrpc": "2.0", 14 | "diem_chain_id": 4, 15 | "diem_ledger_timestampusec": 1600152974894366, 16 | "diem_ledger_version": 45, 17 | "result": { 18 | "address": "000000000000000000000000000000dd", 19 | "authentication_key": "536beec888b6a19d480c5f2375280432a7fd7dc99e4daed8699d1706de22eec4", 20 | "balances": [ 21 | { 22 | "amount": 4611686018427387903, 23 | "currency": "XUS" 24 | }, 25 | { 26 | "amount": 9223372035854775807, 27 | "currency": "XDX" 28 | } 29 | ], 30 | "delegated_key_rotation_capability": true, 31 | "delegated_withdrawal_capability": true, 32 | "is_frozen": true, 33 | "received_events_key": "0300000000000000000000000000000000000000000000dd", 34 | "role": { 35 | "base_url": "base url", 36 | "base_url_rotation_events_key": "0200000000000000000000000000000000000000000000dd", 37 | "compliance_key": "compliance key", 38 | "compliance_key_rotation_events_key": "0100000000000000000000000000000000000000000000dd", 39 | "expiration_time": 18446744073709551615, 40 | "human_name": "moneybags", 41 | "preburn_balances": [ 42 | { 43 | "amount": 10, 44 | "currency": "XUS" 45 | } 46 | ], 47 | "received_mint_events_key": "0000000000000000000000000000000000000000000000dd", 48 | "type": "designated_dealer" 49 | }, 50 | "sent_events_key": "0400000000000000000000000000000000000000000000dd", 51 | "sequence_number": 1 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-with-parent-vasp-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-with-parent-vasp-role", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account", 7 | "params": [ 8 | "876ff1d441cb0b352f438fcfbce8608f" 9 | ] 10 | }, 11 | "response": { 12 | "id": 1, 13 | "jsonrpc": "2.0", 14 | "diem_chain_id": 4, 15 | "diem_ledger_timestampusec": 1600152974894366, 16 | "diem_ledger_version": 45, 17 | "result": { 18 | "address": "876ff1d441cb0b352f438fcfbce8608f", 19 | "authentication_key": "1c24113dea38cf242a9949cc6605b46d876ff1d441cb0b352f438fcfbce8608f", 20 | "balances": [ 21 | { 22 | "amount": 992000000, 23 | "currency": "LBR" 24 | } 25 | ], 26 | "delegated_key_rotation_capability": true, 27 | "delegated_withdrawal_capability": true, 28 | "is_frozen": true, 29 | "received_events_key": "0200000000000000876ff1d441cb0b352f438fcfbce8608f", 30 | "role": { 31 | "base_url": "base url", 32 | "base_url_rotation_events_key": "0100000000000000876ff1d441cb0b352f438fcfbce8608f", 33 | "compliance_key": "compliance_key", 34 | "compliance_key_rotation_events_key": "0000000000000000876ff1d441cb0b352f438fcfbce8608f", 35 | "expiration_time": 18446744073709551615, 36 | "human_name": "Novi", 37 | "num_children": 1, 38 | "type": "parent_vasp" 39 | }, 40 | "sent_events_key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 41 | "sequence_number": 4 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-account-with-unknown-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-account-with-unknown-role", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_account", 7 | "params": [ 8 | "0000000000000000000000000a550c18" 9 | ] 10 | }, 11 | "response": { 12 | "id": 1, 13 | "jsonrpc": "2.0", 14 | "diem_chain_id": 4, 15 | "diem_ledger_timestampusec": 1600152974894366, 16 | "diem_ledger_version": 45, 17 | "result": { 18 | "address": "0000000000000000000000000a550c18", 19 | "authentication_key": "536beec888b6a19d480c5f2375280432a7fd7dc99e4daed8699d1706de22eec4", 20 | "balances": [ 21 | { 22 | "amount": 992000000, 23 | "currency": "XDX" 24 | } 25 | ], 26 | "delegated_key_rotation_capability": true, 27 | "delegated_withdrawal_capability": true, 28 | "is_frozen": true, 29 | "received_events_key": "12000000000000000000000000000000000000000a550c18", 30 | "role": { 31 | "type": "unknown" 32 | }, 33 | "sent_events_key": "13000000000000000000000000000000000000000a550c18", 34 | "sequence_number": 1 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-currencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-currencies", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_currencies", 7 | "params": [] 8 | }, 9 | "response": { 10 | "id": 1, 11 | "jsonrpc": "2.0", 12 | "diem_chain_id": 4, 13 | "diem_ledger_timestampusec": 1600152974894366, 14 | "diem_ledger_version": 45, 15 | "result": [ 16 | { 17 | "burn_events_key": "02000000000000000000000000000000000000000a550c18", 18 | "cancel_burn_events_key": "04000000000000000000000000000000000000000a550c18", 19 | "code": "XUS", 20 | "exchange_rate_update_events_key": "05000000000000000000000000000000000000000a550c18", 21 | "fractional_part": 100, 22 | "mint_events_key": "01000000000000000000000000000000000000000a550c18", 23 | "preburn_events_key": "03000000000000000000000000000000000000000a550c18", 24 | "scaling_factor": 1000000, 25 | "to_xdx_exchange_rate": 0.5 26 | }, 27 | { 28 | "burn_events_key": "0c000000000000000000000000000000000000000a550c18", 29 | "cancel_burn_events_key": "0e000000000000000000000000000000000000000a550c18", 30 | "code": "XDX", 31 | "exchange_rate_update_events_key": "0f000000000000000000000000000000000000000a550c18", 32 | "fractional_part": 1000, 33 | "mint_events_key": "0b000000000000000000000000000000000000000a550c18", 34 | "preburn_events_key": "0d000000000000000000000000000000000000000a550c18", 35 | "scaling_factor": 1000000, 36 | "to_xdx_exchange_rate": 1.2 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-latest-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-latest-metadata", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_metadata", 7 | "params": [] 8 | }, 9 | "response": { 10 | "id": 1, 11 | "jsonrpc": "2.0", 12 | "diem_chain_id": 4, 13 | "diem_ledger_timestampusec": 1600152974894366, 14 | "diem_ledger_version": 45, 15 | "result": { 16 | "chain_id": 4, 17 | "timestamp": 1600152974894366, 18 | "version": 45 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-received-payment-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-received-payment-events", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_events", 7 | "params": [ 8 | "0000000000000000762cbea8b99911d49707d2b901e13425", 9 | 1, 10 | 3 11 | ] 12 | }, 13 | "response": { 14 | "id": 1, 15 | "jsonrpc": "2.0", 16 | "diem_chain_id": 4, 17 | "diem_ledger_timestampusec": 1600152975026838, 18 | "diem_ledger_version": 46, 19 | "result": [ 20 | { 21 | "data": { 22 | "amount": { 23 | "amount": 3000000, 24 | "currency": "LBR" 25 | }, 26 | "metadata": "metadata", 27 | "receiver": "762cbea8b99911d49707d2b901e13425", 28 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 29 | "type": "receivedpayment" 30 | }, 31 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 32 | "sequence_number": 1, 33 | "transaction_version": 27 34 | }, 35 | { 36 | "data": { 37 | "amount": { 38 | "amount": 4000000, 39 | "currency": "LBR" 40 | }, 41 | "metadata": "metadata", 42 | "receiver": "762cbea8b99911d49707d2b901e13425", 43 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 44 | "type": "receivedpayment" 45 | }, 46 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 47 | "sequence_number": 2, 48 | "transaction_version": 33 49 | }, 50 | { 51 | "data": { 52 | "amount": { 53 | "amount": 4000000, 54 | "currency": "LBR" 55 | }, 56 | "metadata": "metadata", 57 | "receiver": "762cbea8b99911d49707d2b901e13425", 58 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 59 | "type": "receivedpayment" 60 | }, 61 | "key": "0000000000000000762cbea8b99911d49707d2b901e13425", 62 | "sequence_number": 3, 63 | "transaction_version": 38 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /diemjsonrpctypes/testdata/get-sent-payment-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-sent-payment-events", 3 | "request": { 4 | "id": 1, 5 | "jsonrpc": "2.0", 6 | "method": "get_events", 7 | "params": [ 8 | "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 9 | 1, 10 | 10 11 | ] 12 | }, 13 | "response": { 14 | "id": 1, 15 | "jsonrpc": "2.0", 16 | "diem_chain_id": 4, 17 | "diem_ledger_timestampusec": 1600152975026838, 18 | "diem_ledger_version": 46, 19 | "result": [ 20 | { 21 | "data": { 22 | "amount": { 23 | "amount": 3000000, 24 | "currency": "XDX" 25 | }, 26 | "metadata": "metadata", 27 | "receiver": "762cbea8b99911d49707d2b901e13425", 28 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 29 | "type": "sentpayment" 30 | }, 31 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 32 | "sequence_number": 1, 33 | "transaction_version": 27 34 | }, 35 | { 36 | "data": { 37 | "amount": { 38 | "amount": 4000000, 39 | "currency": "XDX" 40 | }, 41 | "metadata": "metadata", 42 | "receiver": "762cbea8b99911d49707d2b901e13425", 43 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 44 | "type": "sentpayment" 45 | }, 46 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 47 | "sequence_number": 2, 48 | "transaction_version": 33 49 | }, 50 | { 51 | "data": { 52 | "amount": { 53 | "amount": 4000000, 54 | "currency": "XDX" 55 | }, 56 | "metadata": "metadata", 57 | "receiver": "762cbea8b99911d49707d2b901e13425", 58 | "sender": "876ff1d441cb0b352f438fcfbce8608f", 59 | "type": "sentpayment" 60 | }, 61 | "key": "0300000000000000876ff1d441cb0b352f438fcfbce8608f", 62 | "sequence_number": 3, 63 | "transaction_version": 38 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /diemjsonrpctypes/types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemjsonrpctypes_test 5 | 6 | import ( 7 | "encoding/json" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | 12 | "github.com/diem/client-sdk-go/diemjsonrpctypes" 13 | "github.com/diem/client-sdk-go/jsonrpc" 14 | "github.com/nsf/jsondiff" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestParseServerResponseJson(t *testing.T) { 20 | cases := []struct { 21 | name string 22 | unmarshal func(*testing.T, *jsonrpc.Response) interface{} 23 | }{ 24 | { 25 | name: "get-account-transactions-with-events.json", 26 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 27 | var result []*diemjsonrpctypes.Transaction 28 | ok, err := resp.UnmarshalResult(&result) 29 | assert.True(t, ok) 30 | require.NoError(t, err) 31 | return &result 32 | }, 33 | }, 34 | { 35 | name: "get-account-with-child-vasp-role.json", 36 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 37 | var result diemjsonrpctypes.Account 38 | ok, err := resp.UnmarshalResult(&result) 39 | assert.True(t, ok) 40 | require.NoError(t, err) 41 | return &result 42 | }, 43 | }, 44 | { 45 | name: "get-account-with-designated-dealer-role.json", 46 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 47 | var result diemjsonrpctypes.Account 48 | ok, err := resp.UnmarshalResult(&result) 49 | assert.True(t, ok) 50 | require.NoError(t, err) 51 | return &result 52 | }, 53 | }, 54 | { 55 | name: "get-account-with-parent-vasp-role.json", 56 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 57 | var result diemjsonrpctypes.Account 58 | ok, err := resp.UnmarshalResult(&result) 59 | assert.True(t, ok) 60 | require.NoError(t, err) 61 | return &result 62 | }, 63 | }, 64 | { 65 | name: "get-account-with-unknown-role.json", 66 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 67 | var result diemjsonrpctypes.Account 68 | ok, err := resp.UnmarshalResult(&result) 69 | assert.True(t, ok) 70 | require.NoError(t, err) 71 | return &result 72 | }, 73 | }, 74 | { 75 | name: "get-currencies.json", 76 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 77 | var result []*diemjsonrpctypes.CurrencyInfo 78 | ok, err := resp.UnmarshalResult(&result) 79 | assert.True(t, ok) 80 | require.NoError(t, err) 81 | return &result 82 | }, 83 | }, 84 | { 85 | name: "get-latest-metadata.json", 86 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 87 | var result diemjsonrpctypes.Metadata 88 | ok, err := resp.UnmarshalResult(&result) 89 | assert.True(t, ok) 90 | require.NoError(t, err) 91 | return &result 92 | }, 93 | }, 94 | { 95 | name: "get-received-payment-events.json", 96 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 97 | var result []*diemjsonrpctypes.Event 98 | ok, err := resp.UnmarshalResult(&result) 99 | assert.True(t, ok) 100 | require.NoError(t, err) 101 | return &result 102 | }, 103 | }, 104 | { 105 | name: "get-sent-payment-events.json", 106 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 107 | var result []*diemjsonrpctypes.Event 108 | ok, err := resp.UnmarshalResult(&result) 109 | assert.True(t, ok) 110 | require.NoError(t, err) 111 | return &result 112 | }, 113 | }, 114 | { 115 | name: "get-transactions-with-events.json", 116 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 117 | var result []*diemjsonrpctypes.Transaction 118 | ok, err := resp.UnmarshalResult(&result) 119 | assert.True(t, ok) 120 | require.NoError(t, err) 121 | return &result 122 | }, 123 | }, 124 | { 125 | name: "get-account-transaction-with-events.json", 126 | unmarshal: func(t *testing.T, resp *jsonrpc.Response) interface{} { 127 | var result diemjsonrpctypes.Transaction 128 | ok, err := resp.UnmarshalResult(&result) 129 | assert.True(t, ok) 130 | require.NoError(t, err) 131 | return &result 132 | }, 133 | }, 134 | } 135 | wd, _ := os.Getwd() 136 | for _, tc := range cases { 137 | t.Run(tc.name, func(t *testing.T) { 138 | bytes, err := ioutil.ReadFile(wd + "/testdata/" + tc.name) 139 | var data Data 140 | err = json.Unmarshal(bytes, &data) 141 | require.NoError(t, err) 142 | require.NotNil(t, data.Response) 143 | 144 | result := tc.unmarshal(t, data.Response) 145 | 146 | resultBytes, err := json.Marshal(&result) 147 | require.NoError(t, err) 148 | expectedBytes, err := json.Marshal(data.Response.Result) 149 | require.NoError(t, err) 150 | opt := jsondiff.DefaultConsoleOptions() 151 | diff, desc := jsondiff.Compare(expectedBytes, resultBytes, &opt) 152 | assert.Equal(t, jsondiff.FullMatch, diff, desc) 153 | }) 154 | } 155 | } 156 | 157 | type Data struct { 158 | Name string `json:"name"` 159 | Request *jsonrpc.Request `json:"request"` 160 | Response *jsonrpc.Response `json:"response"` 161 | } 162 | -------------------------------------------------------------------------------- /diemkeys/auth_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys 5 | 6 | import ( 7 | "encoding/hex" 8 | 9 | "github.com/diem/client-sdk-go/diemtypes" 10 | "golang.org/x/crypto/sha3" 11 | ) 12 | 13 | type KeyScheme byte 14 | 15 | const ( 16 | Ed25519Key KeyScheme = 0 17 | MultiEd25519Key KeyScheme = 1 18 | ) 19 | 20 | // AuthKey is Diem account authentication key 21 | type AuthKey []byte 22 | 23 | // NewAuthKeyFromString creates AuthKey from given hex-encoded key string. 24 | // Returns error if given string is not hex encoded. 25 | func NewAuthKeyFromString(key string) (AuthKey, error) { 26 | bytes, err := hex.DecodeString(key) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return AuthKey(bytes), nil 31 | } 32 | 33 | // MustNewAuthKeyFromString parses given key or panic 34 | func MustNewAuthKeyFromString(key string) AuthKey { 35 | ret, err := NewAuthKeyFromString(key) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return ret 40 | } 41 | 42 | // NewAuthKey creates AuthKey PublicKey 43 | func NewAuthKey(publicKey PublicKey) AuthKey { 44 | hash := sha3.New256() 45 | hash.Write(publicKey.Bytes()) 46 | if publicKey.IsMulti() { 47 | hash.Write([]byte{byte(MultiEd25519Key)}) 48 | } else { 49 | hash.Write([]byte{byte(Ed25519Key)}) 50 | } 51 | return AuthKey(hash.Sum(nil)) 52 | } 53 | 54 | // Hex returns hex encoded string for the AuthKey 55 | func (k AuthKey) Hex() string { 56 | return hex.EncodeToString(k) 57 | } 58 | 59 | // Prefix returns AuthKey's first 16 bytes 60 | func (k AuthKey) Prefix() []uint8 { 61 | return k[:diemtypes.AccountAddressLength] 62 | } 63 | 64 | // AccountAddress return account address from auth key 65 | func (k AuthKey) AccountAddress() diemtypes.AccountAddress { 66 | ret, _ := diemtypes.MakeAccountAddressFromBytes( 67 | k[len(k)-diemtypes.AccountAddressLength:]) 68 | return ret 69 | } 70 | -------------------------------------------------------------------------------- /diemkeys/auth_key_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys_test 5 | 6 | import ( 7 | "crypto/ed25519" 8 | "encoding/hex" 9 | "testing" 10 | 11 | "github.com/diem/client-sdk-go/diemkeys" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestAuthKey(t *testing.T) { 16 | key := diemkeys.MustNewAuthKeyFromString( 17 | "459c77a38803bd53f3adee52703810e3a74fd7c46952c497e75afb0a7932586d") 18 | t.Run("hex", func(t *testing.T) { 19 | assert.Equal(t, "459c77a38803bd53f3adee52703810e3a74fd7c46952c497e75afb0a7932586d", key.Hex()) 20 | }) 21 | t.Run("prefix", func(t *testing.T) { 22 | assert.Equal(t, "459c77a38803bd53f3adee52703810e3", hex.EncodeToString(key.Prefix())) 23 | }) 24 | } 25 | 26 | func TestNewAuthKey(t *testing.T) { 27 | keyHex := "447fc3be296803c2303951c7816624c7566730a5cc6860a4a1bd3c04731569f5" 28 | publicKey, _ := diemkeys.NewEd25519PublicKeyFromString(keyHex) 29 | authKey := diemkeys.NewAuthKey(publicKey) 30 | assert.Equal(t, 31 | "459c77a38803bd53f3adee52703810e3a74fd7c46952c497e75afb0a7932586d", 32 | authKey.Hex()) 33 | assert.Equal(t, 34 | "a74fd7c46952c497e75afb0a7932586d", 35 | authKey.AccountAddress().Hex()) 36 | assert.Equal(t, 37 | "459c77a38803bd53f3adee52703810e3", 38 | hex.EncodeToString(authKey.Prefix())) 39 | } 40 | 41 | func TestNewAuthKeyFromMultiSigPublicKey(t *testing.T) { 42 | keys := []string{ 43 | "20fdbac9b10b7587bba7b5bc163bce69e796d71e4ed44c10fcb4488689f7a144", 44 | "75e4174dd58822548086f17b037cecb0ee86516b7d13400a80c856b4bdaf7fe1", 45 | "631c1541f3a4bf44d4d897061564aa8495d766f6191a3ff61562003f184b8c65", 46 | } 47 | 48 | publicKeys := make([]ed25519.PublicKey, len(keys)) 49 | for i, k := range keys { 50 | bytes, _ := hex.DecodeString(k) 51 | publicKeys[i] = ed25519.PublicKey(bytes) 52 | } 53 | 54 | pk := diemkeys.NewMultiEd25519PublicKey(publicKeys, byte(2)) 55 | authKey := diemkeys.NewAuthKey(pk) 56 | assert.Equal(t, 57 | "4b09784ca88af28c16b6e8cf24c36c45c8b5290aa97c1d392679f636790fa5de", 58 | authKey.Hex()) 59 | assert.Equal(t, 60 | "c8b5290aa97c1d392679f636790fa5de", 61 | authKey.AccountAddress().Hex()) 62 | assert.Equal(t, 63 | "4b09784ca88af28c16b6e8cf24c36c45", 64 | hex.EncodeToString(authKey.Prefix())) 65 | } 66 | 67 | func TestMustNewAuthKeyFromStringPanic(t *testing.T) { 68 | defer func() { 69 | if r := recover(); r != nil { 70 | return 71 | } 72 | assert.Fail(t, "should panic, but not") 73 | }() 74 | diemkeys.MustNewAuthKeyFromString("invalid") 75 | } 76 | -------------------------------------------------------------------------------- /diemkeys/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides interface for abstracting private key and public key implementations, 5 | // utility functions for authentication key. 6 | package diemkeys 7 | -------------------------------------------------------------------------------- /diemkeys/ed25519.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys 5 | 6 | import ( 7 | "crypto/ed25519" 8 | "encoding/hex" 9 | ) 10 | 11 | // Ed25519PublicKey implements `PublicKey` interface for ed25519 public key 12 | type Ed25519PublicKey struct { 13 | pk ed25519.PublicKey 14 | } 15 | 16 | // Ed25519PrivateKey implements `PrivateKey` interface for ed25519 private key 17 | type Ed25519PrivateKey struct { 18 | pk ed25519.PrivateKey 19 | } 20 | 21 | // NewEd25519PublicKey creates `Ed25519PublicKey` 22 | func NewEd25519PublicKey(key ed25519.PublicKey) *Ed25519PublicKey { 23 | return &Ed25519PublicKey{key} 24 | } 25 | 26 | // NewEd25519PrivateKey creates `Ed25519PrivateKey` 27 | func NewEd25519PrivateKey(key ed25519.PrivateKey) *Ed25519PrivateKey { 28 | return &Ed25519PrivateKey{key} 29 | } 30 | 31 | // NewEd25519PublicKeyFromString creates `*Ed25519PublicKey` from given hex-encoded 32 | // `ed25519.PublicKey` string 33 | func NewEd25519PublicKeyFromString(key string) (*Ed25519PublicKey, error) { 34 | bytes, err := hex.DecodeString(key) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &Ed25519PublicKey{bytes}, nil 39 | } 40 | 41 | // NewEd25519PrivateKeyFromString creates `*Ed25519PrivateKey` from given hex-encoded 42 | // `ed25519.PrivateKey` string 43 | func NewEd25519PrivateKeyFromString(key string) (*Ed25519PrivateKey, error) { 44 | bytes, err := hex.DecodeString(key) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return NewEd25519PrivateKey(ed25519.PrivateKey(bytes)), nil 49 | } 50 | 51 | // IsMulti returns false 52 | func (k *Ed25519PublicKey) IsMulti() bool { 53 | return false 54 | } 55 | 56 | // Hex returns hex string of the public key 57 | func (k *Ed25519PublicKey) Hex() string { 58 | return hex.EncodeToString(k.pk) 59 | } 60 | 61 | // Bytes returns public key bytes 62 | func (k *Ed25519PublicKey) Bytes() []byte { 63 | return []byte(k.pk) 64 | } 65 | 66 | // Sign signs given message bytes by private key 67 | func (k *Ed25519PrivateKey) Sign(msg []byte) []byte { 68 | return ed25519.Sign(k.pk, msg) 69 | } 70 | 71 | // Hex returns hex string of private key, used for testing 72 | func (k *Ed25519PrivateKey) Hex() string { 73 | return hex.EncodeToString(k.pk) 74 | } 75 | -------------------------------------------------------------------------------- /diemkeys/ed25519_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys_test 5 | 6 | import ( 7 | "encoding/hex" 8 | "testing" 9 | 10 | "github.com/diem/client-sdk-go/diemkeys" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestEd25519PublicKey(t *testing.T) { 16 | keyHex := "447fc3be296803c2303951c7816624c7566730a5cc6860a4a1bd3c04731569f5" 17 | publicKey, _ := diemkeys.NewEd25519PublicKeyFromString(keyHex) 18 | t.Run("Hex", func(t *testing.T) { 19 | assert.Equal(t, keyHex, publicKey.Hex()) 20 | }) 21 | t.Run("IsMulti", func(t *testing.T) { 22 | assert.False(t, publicKey.IsMulti()) 23 | }) 24 | t.Run("Bytes", func(t *testing.T) { 25 | bytes, _ := hex.DecodeString(keyHex) 26 | assert.Equal(t, bytes, publicKey.Bytes()) 27 | }) 28 | } 29 | 30 | func TestNewEd25519PublicKeyFromStringError(t *testing.T) { 31 | _, err := diemkeys.NewEd25519PublicKeyFromString("invalid") 32 | assert.Error(t, err) 33 | } 34 | 35 | func TestEd25519PrivateKey(t *testing.T) { 36 | keyHex := "b38318e91089220c144854881c48b88975c25d6395ac3aeeb21a287bcfa1ebe9fc4ea02dc1e42b332ac221d716ece959d5b1fc86c156fa4a5d8b77b3886c3c63" 37 | key, err := diemkeys.NewEd25519PrivateKeyFromString(keyHex) 38 | require.NoError(t, err) 39 | 40 | t.Run("sign", func(t *testing.T) { 41 | expectedSig := "46a8d7cb7ba2fe5703b18d72cbbd6f3e19d3d05793a5870b5d22cac191ad757286c9222ed82a21ff3d2ef02bd2f08380607417e21403da44318ecb39a12f2904" 42 | assert.Equal(t, expectedSig, hex.EncodeToString(key.Sign([]byte("test")))) 43 | }) 44 | t.Run("hex", func(t *testing.T) { 45 | assert.Equal(t, keyHex, key.Hex()) 46 | }) 47 | } 48 | 49 | func TestNewEd25519PrivateKeyFromStringError(t *testing.T) { 50 | _, err := diemkeys.NewEd25519PrivateKeyFromString("invalid") 51 | assert.Error(t, err) 52 | } 53 | -------------------------------------------------------------------------------- /diemkeys/keys.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys 5 | 6 | import ( 7 | "crypto/ed25519" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/diem/client-sdk-go/diemtypes" 12 | ) 13 | 14 | // PublicKey is Diem account public key 15 | type PublicKey interface { 16 | IsMulti() bool 17 | Hex() string 18 | Bytes() []byte 19 | } 20 | 21 | // PrivateKey is Diem account private key 22 | type PrivateKey interface { 23 | Sign(msg []byte) []byte 24 | } 25 | 26 | // Keys holds Diem local account keys 27 | type Keys struct { 28 | PublicKey PublicKey 29 | PrivateKey PrivateKey 30 | } 31 | 32 | // AccountAddress return account address from auth key 33 | func (k *Keys) AccountAddress() diemtypes.AccountAddress { 34 | return k.AuthKey().AccountAddress() 35 | } 36 | 37 | func (k *Keys) AuthKey() AuthKey { 38 | return NewAuthKey(k.PublicKey) 39 | } 40 | 41 | // NewKeysFromPublicAndPrivateKeys creates new `Keys` from given public key and private key 42 | func NewKeysFromPublicAndPrivateKeys(publicKey PublicKey, privateKey PrivateKey) *Keys { 43 | return &Keys{ 44 | PublicKey: publicKey, 45 | PrivateKey: privateKey, 46 | } 47 | } 48 | 49 | // MustGenKeys generates local account keys, panics if got error 50 | func MustGenKeys() *Keys { 51 | publicKey, privateKey, err := ed25519.GenerateKey(nil) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return NewKeysFromPublicAndPrivateKeys( 56 | NewEd25519PublicKey(publicKey), NewEd25519PrivateKey(privateKey)) 57 | } 58 | 59 | // MustGenMultiSigKeys generates `*Keys`, mostly for testing purpose. 60 | // It panics if got error while generating key 61 | func MustGenMultiSigKeys() *Keys { 62 | rand.Seed(time.Now().UnixNano()) 63 | numOfKeys := 1 + rand.Intn(MaxNumOfKeys) 64 | publicKeys := make([]ed25519.PublicKey, numOfKeys) 65 | privateKeys := make([]ed25519.PrivateKey, numOfKeys) 66 | var err error 67 | for i := 0; i < numOfKeys; i++ { 68 | publicKeys[i], privateKeys[i], err = ed25519.GenerateKey(nil) 69 | if err != nil { 70 | panic(err) 71 | } 72 | } 73 | threshold := 1 + rand.Intn(numOfKeys) 74 | return NewKeysFromPublicAndPrivateKeys( 75 | NewMultiEd25519PublicKey(publicKeys, byte(threshold)), 76 | NewMultiEd25519PrivateKey(privateKeys, byte(threshold)), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /diemkeys/keys_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMustGenKeys(t *testing.T) { 14 | keys := diemkeys.MustGenKeys() 15 | assert.NotEmpty(t, keys.PublicKey) 16 | assert.NotEmpty(t, keys.PrivateKey) 17 | assert.NotEmpty(t, keys.AuthKey()) 18 | assert.NotEmpty(t, keys.AccountAddress().Hex()) 19 | } 20 | 21 | func TestMustGenMultiSigKeys(t *testing.T) { 22 | keys := diemkeys.MustGenMultiSigKeys() 23 | assert.NotEmpty(t, keys.PublicKey) 24 | assert.NotEmpty(t, keys.PrivateKey) 25 | assert.NotEmpty(t, keys.AuthKey()) 26 | assert.NotEmpty(t, keys.AccountAddress().Hex()) 27 | 28 | for i := 0; i < 1000; i++ { 29 | keys2 := diemkeys.MustGenMultiSigKeys() 30 | assert.NotEqual(t, keys.PrivateKey, keys2.PrivateKey) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /diemkeys/multi_ed25519.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys 5 | 6 | import ( 7 | "crypto/ed25519" 8 | "encoding/hex" 9 | ) 10 | 11 | const ( 12 | // BitmapNumOfBytes defines length of bitmap appended to multi-signed signature 13 | BitmapNumOfBytes = 4 14 | 15 | // MaxNumOfKeys defines max number of keys a multi sig keys can have 16 | MaxNumOfKeys = 32 17 | ) 18 | 19 | // MultiEd25519PublicKey implements `PublicKey` interface with multi ed25519 sig support 20 | type MultiEd25519PublicKey struct { 21 | keys []ed25519.PublicKey 22 | threshold byte 23 | } 24 | 25 | // MultiEd25519PrivateKey implements `PrivateKey` interface with multi ed25519 sig support 26 | type MultiEd25519PrivateKey struct { 27 | keys []ed25519.PrivateKey 28 | threshold byte 29 | } 30 | 31 | // NewMultiEd25519PublicKey creates new `MultiEd25519PublicKey` as `PublicKey` 32 | // with given keys and threshold 33 | func NewMultiEd25519PublicKey(keys []ed25519.PublicKey, threshold byte) PublicKey { 34 | validate(len(keys), threshold) 35 | return &MultiEd25519PublicKey{keys, threshold} 36 | } 37 | 38 | // NewMultiEd25519PrivateKey creates new `MultiEd25519PrivateKey` as `PrivateKey` 39 | // with given keys and threshold 40 | func NewMultiEd25519PrivateKey(keys []ed25519.PrivateKey, threshold byte) PrivateKey { 41 | validate(len(keys), threshold) 42 | return &MultiEd25519PrivateKey{keys, threshold} 43 | } 44 | 45 | func validate(keysLen int, threshold byte) { 46 | if keysLen == 0 { 47 | panic("should at least have 1 key") 48 | } 49 | if int(threshold) > keysLen { 50 | panic("threshold should be less or equal to len(keys)") 51 | } 52 | if keysLen > MaxNumOfKeys { 53 | panic("len(keys) is more than max num of keys") 54 | } 55 | } 56 | 57 | // IsMulti returns true 58 | func (k *MultiEd25519PublicKey) IsMulti() bool { 59 | return true 60 | } 61 | 62 | // Hex implements `PublicKey` interface returns hex-encoded string of public keys' bytes 63 | func (k *MultiEd25519PublicKey) Hex() string { 64 | return hex.EncodeToString(k.Bytes()) 65 | } 66 | 67 | // Bytes returns bytes representation of Diem MultiEd25519 public key 68 | func (k *MultiEd25519PublicKey) Bytes() []byte { 69 | var ret []byte 70 | for _, key := range k.keys { 71 | ret = append(ret, key...) 72 | } 73 | return append(ret, k.threshold) 74 | } 75 | 76 | // Sign implements `PrivateKey` interface, signs arbitrary message bytes and return it's signature. 77 | func (k *MultiEd25519PrivateKey) Sign(msg []byte) []byte { 78 | var bitmap [BitmapNumOfBytes]byte 79 | var ret []byte 80 | for i, key := range k.keys[:k.threshold] { 81 | bitmapSetBit(&bitmap, byte(i)) 82 | ret = append(ret, ed25519.Sign(key, msg)...) 83 | } 84 | return append(ret, bitmap[:]...) 85 | } 86 | 87 | func bitmapSetBit(input *[BitmapNumOfBytes]byte, index byte) { 88 | bucket := index / 8 89 | // It's always invoked with index < 32, thus there is no need to check range. 90 | pos := index - (bucket * 8) 91 | input[bucket] |= uint8(128 >> pos) 92 | } 93 | -------------------------------------------------------------------------------- /diemkeys/multi_ed25519_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemkeys_test 5 | 6 | import ( 7 | "crypto/ed25519" 8 | "encoding/hex" 9 | "testing" 10 | 11 | "github.com/diem/client-sdk-go/diemkeys" 12 | "github.com/novifinancial/serde-reflection/serde-generate/runtime/golang/bcs" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestMultiEd25519PublicKey(t *testing.T) { 17 | keys := []string{ 18 | "20fdbac9b10b7587bba7b5bc163bce69e796d71e4ed44c10fcb4488689f7a144", 19 | "75e4174dd58822548086f17b037cecb0ee86516b7d13400a80c856b4bdaf7fe1", 20 | "631c1541f3a4bf44d4d897061564aa8495d766f6191a3ff61562003f184b8c65", 21 | } 22 | 23 | publicKeys := make([]ed25519.PublicKey, len(keys)) 24 | for i, k := range keys { 25 | bytes, _ := hex.DecodeString(k) 26 | publicKeys[i] = ed25519.PublicKey(bytes) 27 | } 28 | 29 | pk := diemkeys.NewMultiEd25519PublicKey(publicKeys, byte(2)) 30 | 31 | t.Run("hex", func(t *testing.T) { 32 | expectedBytes := "20fdbac9b10b7587bba7b5bc163bce69e796d71e4ed44c10fcb4488689f7a14475e4174dd58822548086f17b037cecb0ee86516b7d13400a80c856b4bdaf7fe1631c1541f3a4bf44d4d897061564aa8495d766f6191a3ff61562003f184b8c6502" 33 | assert.Equal(t, expectedBytes, pk.Hex()) 34 | }) 35 | 36 | t.Run("is multi", func(t *testing.T) { 37 | assert.True(t, pk.IsMulti()) 38 | }) 39 | 40 | } 41 | 42 | func TestMultiEd25519PrivateKey(t *testing.T) { 43 | keys := []string{ 44 | "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7", 45 | "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586", 46 | "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed", 47 | } 48 | privateKeys := make([]ed25519.PrivateKey, len(keys)) 49 | for i, k := range keys { 50 | bytes, _ := hex.DecodeString(k) 51 | privateKeys[i] = ed25519.PrivateKey(ed25519.NewKeyFromSeed(bytes)) 52 | } 53 | 54 | pk := diemkeys.NewMultiEd25519PrivateKey(privateKeys, byte(2)) 55 | 56 | t.Run("sign", func(t *testing.T) { 57 | multiSig := pk.Sign([]byte("test")) 58 | 59 | expectedSig := "8401951ea9303fe7c0245a2a4c159b3f641e4623e15091a6e0557eb26144cac9ebe3ab4338b9bc7f7d54e78e9c50f3de10bf43199956f5ed0fbcd3c54a081c43b407e86b66d43e1c70a69e819247c9df579dca7d6927569a89a1863f74af7d8ebe07947425ddf6d0155b8a193c8e859a8b3f7f85191b4c613718d40d0fd3e09ca400c0000000" 60 | assert.Equal(t, expectedSig, bcsBytes(multiSig)) 61 | }) 62 | } 63 | 64 | func TestNewMultiEd25519PrivateKeyErrors(t *testing.T) { 65 | t.Run("empty keys", func(t *testing.T) { 66 | defer func() { 67 | if r := recover(); r != nil { 68 | return 69 | } 70 | assert.Fail(t, "should panic, but not") 71 | }() 72 | diemkeys.NewMultiEd25519PrivateKey(nil, 0) 73 | }) 74 | t.Run("threshold > len(keys)", func(t *testing.T) { 75 | _, privateKey, _ := ed25519.GenerateKey(nil) 76 | 77 | defer func() { 78 | if r := recover(); r != nil { 79 | return 80 | } 81 | assert.Fail(t, "should panic, but not") 82 | }() 83 | diemkeys.NewMultiEd25519PrivateKey([]ed25519.PrivateKey{privateKey}, 2) 84 | }) 85 | t.Run("len(keys) > max num of keys", func(t *testing.T) { 86 | defer func() { 87 | if r := recover(); r != nil { 88 | return 89 | } 90 | assert.Fail(t, "should panic, but not") 91 | }() 92 | keys := make([]ed25519.PrivateKey, diemkeys.MaxNumOfKeys+1) 93 | diemkeys.NewMultiEd25519PrivateKey(keys, 2) 94 | }) 95 | } 96 | 97 | func TestNewMultiEd25519PublicKeyErrors(t *testing.T) { 98 | t.Run("empty keys", func(t *testing.T) { 99 | defer func() { 100 | if r := recover(); r != nil { 101 | return 102 | } 103 | assert.Fail(t, "should panic, but not") 104 | }() 105 | diemkeys.NewMultiEd25519PublicKey(nil, 0) 106 | }) 107 | t.Run("threshold > len(keys)", func(t *testing.T) { 108 | publicKey, _, _ := ed25519.GenerateKey(nil) 109 | 110 | defer func() { 111 | if r := recover(); r != nil { 112 | return 113 | } 114 | assert.Fail(t, "should panic, but not") 115 | }() 116 | diemkeys.NewMultiEd25519PublicKey([]ed25519.PublicKey{publicKey}, 2) 117 | }) 118 | t.Run("len(keys) > max num of keys", func(t *testing.T) { 119 | defer func() { 120 | if r := recover(); r != nil { 121 | return 122 | } 123 | assert.Fail(t, "should panic, but not") 124 | }() 125 | keys := make([]ed25519.PublicKey, diemkeys.MaxNumOfKeys+1) 126 | diemkeys.NewMultiEd25519PublicKey(keys, 2) 127 | }) 128 | } 129 | 130 | func bcsBytes(bytes []byte) string { 131 | s := bcs.NewSerializer() 132 | s.SerializeBytes(bytes) 133 | return hex.EncodeToString(s.GetBytes()) 134 | } 135 | -------------------------------------------------------------------------------- /diemsigner/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides signing transaction logic. 5 | package diemsigner 6 | -------------------------------------------------------------------------------- /diemsigner/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemsigner 5 | 6 | import ( 7 | "github.com/diem/client-sdk-go/diemkeys" 8 | "github.com/diem/client-sdk-go/diemtypes" 9 | ) 10 | 11 | // Sign transaction with `diemtypes.Script` 12 | func Sign( 13 | keys *diemkeys.Keys, 14 | accountAddress diemtypes.AccountAddress, 15 | sequenceNum uint64, script diemtypes.Script, 16 | maxGasAmmount uint64, gasUnitPrice uint64, gasCurrencyCode string, 17 | expirationTimeSec uint64, 18 | chainID byte, 19 | ) *diemtypes.SignedTransaction { 20 | return SignTxn( 21 | keys, 22 | accountAddress, 23 | sequenceNum, 24 | &diemtypes.TransactionPayload__Script{script}, 25 | maxGasAmmount, gasUnitPrice, gasCurrencyCode, 26 | expirationTimeSec, 27 | chainID) 28 | } 29 | 30 | // Sign transaction with `diemtypes.TransactionPayload` 31 | func SignTxn( 32 | keys *diemkeys.Keys, 33 | accountAddress diemtypes.AccountAddress, 34 | sequenceNum uint64, payload diemtypes.TransactionPayload, 35 | maxGasAmmount uint64, gasUnitPrice uint64, gasCurrencyCode string, 36 | expirationTimeSec uint64, 37 | chainID byte, 38 | ) *diemtypes.SignedTransaction { 39 | rawTxn, signingMsg := NewRawTransactionAndSigningMsg( 40 | accountAddress, 41 | sequenceNum, payload, 42 | maxGasAmmount, gasUnitPrice, gasCurrencyCode, 43 | expirationTimeSec, 44 | chainID) 45 | 46 | signature := keys.PrivateKey.Sign(signingMsg) 47 | return NewSignedTransaction(keys.PublicKey, rawTxn, signature) 48 | } 49 | 50 | // NewRawTransactionAndSigningMsg creates raw transaction and signing message 51 | func NewRawTransactionAndSigningMsg( 52 | accountAddress diemtypes.AccountAddress, 53 | sequenceNum uint64, payload diemtypes.TransactionPayload, 54 | maxGasAmmount uint64, gasUnitPrice uint64, gasCurrencyCode string, 55 | expirationTimeSec uint64, 56 | chainID byte, 57 | ) (*diemtypes.RawTransaction, []byte) { 58 | rawTxn := diemtypes.RawTransaction{ 59 | Sender: accountAddress, 60 | SequenceNumber: sequenceNum, 61 | Payload: payload, 62 | MaxGasAmount: maxGasAmmount, 63 | GasUnitPrice: gasUnitPrice, 64 | GasCurrencyCode: gasCurrencyCode, 65 | ExpirationTimestampSecs: expirationTimeSec, 66 | ChainId: diemtypes.ChainId(chainID), 67 | } 68 | 69 | signingMsg := append(diemtypes.HashPrefix("RawTransaction"), diemtypes.ToBCS(&rawTxn)...) 70 | return &rawTxn, signingMsg 71 | } 72 | 73 | // NewSignedTransaction creates new `SignedTransaction` 74 | func NewSignedTransaction(publicKey diemkeys.PublicKey, rawTxn *diemtypes.RawTransaction, signature []byte) *diemtypes.SignedTransaction { 75 | var auth diemtypes.TransactionAuthenticator 76 | if publicKey.IsMulti() { 77 | auth = &diemtypes.TransactionAuthenticator__MultiEd25519{ 78 | PublicKey: diemtypes.MultiEd25519PublicKey(publicKey.Bytes()), 79 | Signature: diemtypes.MultiEd25519Signature(signature), 80 | } 81 | } else { 82 | auth = &diemtypes.TransactionAuthenticator__Ed25519{ 83 | PublicKey: diemtypes.Ed25519PublicKey(publicKey.Bytes()), 84 | Signature: diemtypes.Ed25519Signature(signature), 85 | } 86 | } 87 | return &diemtypes.SignedTransaction{ 88 | RawTxn: *rawTxn, 89 | Authenticator: auth, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /diemsigner/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemsigner_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/diem/client-sdk-go/diemsigner" 11 | "github.com/diem/client-sdk-go/diemtypes" 12 | "github.com/diem/client-sdk-go/stdlib" 13 | "github.com/diem/client-sdk-go/testnet" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestSign(t *testing.T) { 18 | var maxGasAmount uint64 = 1000000 19 | var gasUnitPrice uint64 = 0 20 | var seq uint64 = 42 21 | var expiration uint64 = 1593189628 22 | var amount uint64 = 100 23 | var currencyCode = "XDX" 24 | 25 | sender := newKeysFromHexKeys("fc4ea02dc1e42b332ac221d716ece959d5b1fc86c156fa4a5d8b77b3886c3c63", "b38318e91089220c144854881c48b88975c25d6395ac3aeeb21a287bcfa1ebe9fc4ea02dc1e42b332ac221d716ece959d5b1fc86c156fa4a5d8b77b3886c3c63") 26 | receiver := newKeysFromHexKeys( 27 | "a761194c93feb3983e6fffb0af9ccc02bc91fe21e1a9c38b24e03dabc40105ed", 28 | "6762610fdb4bc8acee054bf11870277c63386d64a22ae67a90936e74cb6c4ccba761194c93feb3983e6fffb0af9ccc02bc91fe21e1a9c38b24e03dabc40105ed", 29 | ) 30 | 31 | script := stdlib.EncodePeerToPeerWithMetadataScript( 32 | diemtypes.Currency(currencyCode), 33 | receiver.AccountAddress(), 34 | amount, []byte{}, []byte{}) 35 | 36 | txn := diemsigner.Sign( 37 | sender, 38 | sender.AccountAddress(), 39 | seq, 40 | script, 41 | maxGasAmount, gasUnitPrice, currencyCode, 42 | expiration, 43 | testnet.ChainID, 44 | ) 45 | expected := "e6866fc23780715681be9febd4f771f72a0000000000000001e001a11ceb0b010000000701000202020403061004160205181d0735600895011000000001010000020001000003020301010004010300010501060c0108000506080005030a020a020005060c05030a020a020109000b4469656d4163636f756e741257697468647261774361706162696c6974791b657874726163745f77697468647261775f6361706162696c697479087061795f66726f6d1b726573746f72655f77697468647261775f6361706162696c69747900000000000000000000000000000001010104010c0b0011000c050e050a010a020b030b0438000b051102020107000000000000000000000000000000010358445803584458000403b4b71dbdfaa82e63855337e615889c970164000000000000000400040040420f0000000000000000000000000003584458fc24f65e00000000020020fc4ea02dc1e42b332ac221d716ece959d5b1fc86c156fa4a5d8b77b3886c3c6340833bb10a6b7a45c327426d0f6f20fe140f8641840d7a20cd22ed711ebca0daa4fe9d8d557d1836517435abc21e5d2e423b5d4e331e3f74aafd2c8eeaccbe470e" 46 | assert.Equal(t, expected, diemtypes.ToHex(txn)) 47 | } 48 | 49 | func newKeysFromHexKeys(publicKeyHex string, privateKeyHex string) *diemkeys.Keys { 50 | publicKey, err := diemkeys.NewEd25519PublicKeyFromString(publicKeyHex) 51 | if err != nil { 52 | panic(err) 53 | } 54 | privateKey, err := diemkeys.NewEd25519PrivateKeyFromString(privateKeyHex) 55 | if err != nil { 56 | panic(err) 57 | } 58 | return diemkeys.NewKeysFromPublicAndPrivateKeys(publicKey, privateKey) 59 | } 60 | -------------------------------------------------------------------------------- /diemtypes/address.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes 5 | 6 | import ( 7 | "encoding/hex" 8 | "fmt" 9 | ) 10 | 11 | // AccountAddressLength account address bytes length 12 | const AccountAddressLength = 16 13 | 14 | // MakeAccountAddress creates account address from given hex string, 15 | // it returns error if given string is not hex-encoded or decoded bytes length 16 | // does not meet requirement (16 bytes). 17 | func MakeAccountAddress(address string) (AccountAddress, error) { 18 | bytes, err := hex.DecodeString(address) 19 | if err != nil { 20 | return AccountAddress{}, err 21 | } 22 | return MakeAccountAddressFromBytes(bytes) 23 | } 24 | 25 | // MakeAccountAddressFromBytes creates account address from given bytes, it returns 26 | // error if given bytes length does not meet requirement (16 bytes). 27 | func MakeAccountAddressFromBytes(bytes []byte) (AccountAddress, error) { 28 | var ret AccountAddress 29 | if len(bytes) != AccountAddressLength { 30 | return ret, fmt.Errorf( 31 | "invalid account address bytes length: %v", len(bytes)) 32 | } 33 | copy(ret[:], bytes) 34 | return ret, nil 35 | } 36 | 37 | // MustMakeAccountAddress panics if parse given string address failed 38 | func MustMakeAccountAddress(address string) AccountAddress { 39 | ret, err := MakeAccountAddress(address) 40 | if err != nil { 41 | panic(err) 42 | } 43 | return ret 44 | } 45 | 46 | // Hex returns hex-encoded string of the address 47 | func (a AccountAddress) Hex() string { 48 | return hex.EncodeToString(a[:]) 49 | } 50 | -------------------------------------------------------------------------------- /diemtypes/address_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/diem/client-sdk-go/diemtypes" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestAccountAddress(t *testing.T) { 16 | t.Run("MakeAccountAddress", func(t *testing.T) { 17 | keys := diemkeys.MustGenKeys() 18 | address := keys.AccountAddress().Hex() 19 | accountAddress, err := diemtypes.MakeAccountAddress(address) 20 | require.NoError(t, err) 21 | assert.Equal(t, address, accountAddress.Hex()) 22 | }) 23 | 24 | t.Run("MakeAccountAddress: invalid hex-encoded string", func(t *testing.T) { 25 | _, err := diemtypes.MakeAccountAddress("xx") 26 | assert.EqualError(t, err, "encoding/hex: invalid byte: U+0078 'x'") 27 | }) 28 | 29 | t.Run("MakeAccountAddress: invalid bytes length", func(t *testing.T) { 30 | _, err := diemtypes.MakeAccountAddress("22") 31 | assert.EqualError(t, err, "invalid account address bytes length: 1") 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /diemtypes/currency_type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes 5 | 6 | var codeAddress = [16]uint8{ 7 | 0, 0, 0, 0, 0, 0, 0, 0, 8 | 0, 0, 0, 0, 0, 0, 0, 1, 9 | } 10 | 11 | // Currency converts given currency code string into Move TypeTag that is required by 12 | // move script argument. 13 | func Currency(code string) TypeTag { 14 | return &TypeTag__Struct{ 15 | Value: StructTag{ 16 | Address: codeAddress, 17 | Module: Identifier(code), 18 | Name: Identifier(code), 19 | TypeParams: []TypeTag{}, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /diemtypes/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides Diem on-chain data types, utility functions for converting types. 5 | package diemtypes 6 | -------------------------------------------------------------------------------- /diemtypes/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes 5 | 6 | import ( 7 | "encoding/hex" 8 | 9 | "golang.org/x/crypto/sha3" 10 | ) 11 | 12 | // HashPrefix returns Diem hashing prefix by given type name 13 | func HashPrefix(name string) []byte { 14 | return Hash([]byte("DIEM::"), []byte(name)) 15 | } 16 | 17 | // Hash returns sha3 256 hash bytes for given bytes 18 | func Hash(prefix []byte, bytes []byte) []byte { 19 | sha256 := sha3.New256() 20 | sha256.Write(prefix) 21 | sha256.Write(bytes) 22 | return sha256.Sum(nil) 23 | } 24 | 25 | // TransactionHash returns hex-encoded hash string of the 26 | // transaction that `SignedTransaction` may executed. 27 | func (t *SignedTransaction) TransactionHash() string { 28 | return hex.EncodeToString(Hash( 29 | HashPrefix("Transaction"), 30 | ToBCS(&Transaction__UserTransaction{*t}), 31 | )) 32 | } 33 | -------------------------------------------------------------------------------- /diemtypes/lcs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes 5 | 6 | import ( 7 | "encoding/hex" 8 | "fmt" 9 | ) 10 | 11 | // BCSable interface for `ToBCS` 12 | type BCSable interface { 13 | BcsSerialize() ([]byte, error) 14 | } 15 | 16 | // ToBCS serialize given `BCSable` into BCS bytes. 17 | // It panics if bcs serialization failed. 18 | func ToBCS(t BCSable) []byte { 19 | ret, err := t.BcsSerialize() 20 | if err != nil { 21 | panic(fmt.Sprintf("bcs serialize failed: %v", err.Error())) 22 | } 23 | return ret 24 | } 25 | 26 | // ToHex serialize given `BCSable` into BCS bytes and then return hex-encoded string 27 | // It panics if bcs serialization failed. 28 | func ToHex(t BCSable) string { 29 | return hex.EncodeToString(ToBCS(t)) 30 | } 31 | -------------------------------------------------------------------------------- /diemtypes/lcs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes_test 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/diem/client-sdk-go/diemtypes" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestToBCS(t *testing.T) { 15 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 16 | bytes := diemtypes.ToBCS(&address) 17 | assert.NotEmpty(t, bytes) 18 | } 19 | 20 | func TestToBCSPanicsForSerializationError(t *testing.T) { 21 | defer func() { 22 | r := recover() 23 | assert.NotNil(t, r) 24 | }() 25 | diemtypes.ToBCS(new(bcsError)) 26 | } 27 | 28 | type bcsError struct { 29 | } 30 | 31 | func (l *bcsError) BcsSerialize() ([]byte, error) { 32 | return nil, errors.New("unexpected") 33 | } 34 | -------------------------------------------------------------------------------- /diemtypes/subaddress.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/hex" 9 | "fmt" 10 | ) 11 | 12 | const ( 13 | // SubAddressLength is valid sub-address length 14 | SubAddressLength = 8 15 | ) 16 | 17 | // SubAddress represents sub-address bytes 18 | type SubAddress [SubAddressLength]uint8 19 | 20 | // EmptySubAddress represents empty sub-address, used for creating account identifier without sub-address 21 | var EmptySubAddress SubAddress 22 | 23 | // MakeSubAddress creates SubAddress from given hex-encoded bytes string 24 | // SubAddress should be 8 bytes. 25 | func MakeSubAddress(str string) (SubAddress, error) { 26 | bytes, err := hex.DecodeString(str) 27 | if err != nil { 28 | return EmptySubAddress, err 29 | } 30 | return MakeSubAddressFromBytes(bytes) 31 | } 32 | 33 | // MakeSubAddressFromBytes from given bytes 34 | func MakeSubAddressFromBytes(bytes []byte) (SubAddress, error) { 35 | if len(bytes) != SubAddressLength { 36 | return EmptySubAddress, fmt.Errorf( 37 | "SubAddress should be %v uint8, but given %v", 38 | SubAddressLength, 39 | len(bytes), 40 | ) 41 | } 42 | var ret SubAddress 43 | copy(ret[:], bytes) 44 | return ret, nil 45 | } 46 | 47 | // GenSubAddress generates a random subaddress. 48 | func GenSubAddress() (SubAddress, error) { 49 | bytes := make([]byte, SubAddressLength) 50 | _, err := rand.Read(bytes) 51 | if err != nil { 52 | return EmptySubAddress, err 53 | } 54 | return MakeSubAddressFromBytes(bytes) 55 | } 56 | 57 | // MustGenSubAddress calls `GenSubAddress` and panics if got error 58 | func MustGenSubAddress() SubAddress { 59 | ret, err := GenSubAddress() 60 | if err != nil { 61 | panic(err) 62 | } 63 | return ret 64 | } 65 | 66 | // Hex returns hex-encoded address string 67 | func (a SubAddress) Hex() string { 68 | return hex.EncodeToString(a[:]) 69 | } 70 | -------------------------------------------------------------------------------- /diemtypes/subaddress_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package diemtypes_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemtypes" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestGenSubAddress(t *testing.T) { 15 | address, err := diemtypes.GenSubAddress() 16 | assert.NoError(t, err) 17 | assert.Len(t, address, 8) 18 | for i := 0; i < 10000; i++ { 19 | require.NotEqual(t, address, diemtypes.MustGenSubAddress()) 20 | } 21 | } 22 | 23 | func TestNewSubAddressErrorsForInvalidSubAddress(t *testing.T) { 24 | t.Run("invalid hex-encoded string", func(t *testing.T) { 25 | _, err := diemtypes.MakeSubAddress("invalid") 26 | assert.Error(t, err) 27 | }) 28 | t.Run("invalid bytes length: too long", func(t *testing.T) { 29 | _, err := diemtypes.MakeSubAddress("f72589b71ff4f8d139674a3f7369c69b") 30 | assert.Error(t, err) 31 | }) 32 | 33 | t.Run("invalid bytes length: too short", func(t *testing.T) { 34 | _, err := diemtypes.MakeSubAddress("f72589b") 35 | assert.Error(t, err) 36 | }) 37 | } 38 | 39 | func TestMakeSubAddress(t *testing.T) { 40 | address, _ := diemtypes.GenSubAddress() 41 | newSubAddress, err := diemtypes.MakeSubAddress(address.Hex()) 42 | require.NoError(t, err) 43 | assert.EqualValues(t, address, newSubAddress) 44 | } 45 | -------------------------------------------------------------------------------- /examples/address-encoder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/diem/client-sdk-go/diemid" 12 | "github.com/diem/client-sdk-go/diemkeys" 13 | "github.com/diem/client-sdk-go/diemtypes" 14 | ) 15 | 16 | // networkToPrefix converts from a human friendly format to a prefix usable by the bech32 address format 17 | func networkToPrefix(network string) (diemid.NetworkPrefix, error) { 18 | switch network { 19 | case "mainnet": 20 | return diemid.MainnetPrefix, nil 21 | case "premainnet": 22 | return diemid.PreMainnetPrefix, nil 23 | case "testnet": 24 | return diemid.TestnetPrefix, nil 25 | default: 26 | return diemid.NetworkPrefix(""), fmt.Errorf("Invalid network=%s supplied, no network prefix") 27 | } 28 | } 29 | 30 | // encode converts a onchainAddress + subAddress or publickey to a bech32 address format 31 | // if you provide both, it will pick the onchainAddress and ignore publickey 32 | func encode(networkPrefix diemid.NetworkPrefix, onchainAddress, publicKey string, subAddressNum uint64) (string, error) { 33 | var ( 34 | accountAddress diemtypes.AccountAddress 35 | subAddress diemtypes.SubAddress 36 | err error 37 | ) 38 | 39 | if onchainAddress == "" && publicKey == "" { 40 | return "", errors.New("Need at least onchain address or publickey to encode") 41 | } 42 | 43 | if onchainAddress != "" { 44 | accountAddress, err = diemtypes.MakeAccountAddress(onchainAddress) 45 | if err != nil { 46 | return "", fmt.Errorf("failed to make account address for %s: %w", onchainAddress, err) 47 | } 48 | } 49 | 50 | if publicKey != "" { 51 | pubKey, err := diemkeys.NewEd25519PublicKeyFromString(publicKey) 52 | if err != nil { 53 | return "", fmt.Errorf("failed to parse public key %w", err) 54 | } 55 | authKey := diemkeys.NewAuthKey(pubKey) 56 | accountAddress = authKey.AccountAddress() 57 | } 58 | 59 | if subAddressNum == 0 { 60 | subAddress = diemtypes.EmptySubAddress 61 | } else { 62 | subAddressBuf := make([]byte, 8) 63 | // Write the buffer as 00000000000000 64 | binary.BigEndian.PutUint64(subAddressBuf, subAddressNum) 65 | subAddress, err = diemtypes.MakeSubAddressFromBytes(subAddressBuf) 66 | if err != nil { 67 | return "", fmt.Errorf("Error making subaddress from bytes %q: %w", subAddressBuf, err) 68 | } 69 | } 70 | 71 | return diemid.EncodeAccount(networkPrefix, accountAddress, subAddress) 72 | } 73 | 74 | // decode decodes the given bech32 encoded network public address and returns the hex account address and subAddress 75 | func decode(networkPrefix diemid.NetworkPrefix, encodedAddress string) (string, string, error) { 76 | account, err := diemid.DecodeToAccount(networkPrefix, encodedAddress) 77 | if err != nil { 78 | return "", "", fmt.Errorf("Failed to decode to account: %w", err) 79 | } 80 | return account.AccountAddress.Hex(), account.SubAddress.Hex(), nil 81 | } 82 | 83 | func main() { 84 | var network = flag.String("network", "testnet", "Network to encode or decode addresses") 85 | var encodedAddress = flag.String("encoded-address", "", "Encoded address to use") 86 | var onChainAddress = flag.String("onchain-address", "", "Onchain address to use") 87 | var publicKey = flag.String("publickey", "", "Public key in hex format to use for generating address") 88 | var subAddress = flag.Uint64("subaddress", 0, "SubAddress to use, this will be left-padded with 0 to make 8 bytes to create a bech32 address") 89 | var task = flag.String("task", "", "Default task to do - encode or decode") 90 | flag.Parse() 91 | 92 | prefix, err := networkToPrefix(*network) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | switch *task { 98 | case "encode": 99 | address, err := encode(prefix, *onChainAddress, *publicKey, *subAddress) 100 | if err != nil { 101 | panic(err) 102 | } 103 | fmt.Println(address) 104 | case "decode": 105 | onChainAddress, subAddress, err := decode(prefix, *encodedAddress) 106 | if err != nil { 107 | panic(err) 108 | } 109 | subAddressNum, err := strconv.ParseUint(subAddress, 16, 64) 110 | if err != nil { 111 | panic(err) 112 | } 113 | fmt.Printf("OnchainAddress: %s\nsubAddress(hex): %s\nsubAddress(int): %d\n", onChainAddress, subAddress, subAddressNum) 114 | default: 115 | fmt.Printf("Unknown task %s\n", task) 116 | os.Exit(1) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/create-child-vasp-account/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/diem/client-sdk-go/examples/exampleutils" 11 | "github.com/diem/client-sdk-go/stdlib" 12 | "github.com/diem/client-sdk-go/testnet" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | func main() { 17 | parentVASP := testnet.GenAccount() 18 | parentVASPAddress := parentVASP.AccountAddress() 19 | account, err := exampleutils.Client.GetAccount(parentVASPAddress) 20 | if err != nil { 21 | panic(err) 22 | } 23 | print("Parent VASP account", account) 24 | 25 | childVASPAccount := diemkeys.MustGenKeys() 26 | childVASPAddress := childVASPAccount.AccountAddress() 27 | childAuthKey := childVASPAccount.AuthKey() 28 | 29 | exampleutils.SubmitAndWait( 30 | "create child vasp account transaction", 31 | parentVASP, 32 | stdlib.EncodeCreateChildVaspAccountScript( 33 | testnet.XUS, 34 | childVASPAddress, 35 | childAuthKey.Prefix(), 36 | false, 37 | uint64(1000), 38 | ), 39 | ) 40 | 41 | child, err := exampleutils.Client.GetAccount(childVASPAddress) 42 | if err != nil { 43 | panic(err) 44 | } 45 | print("Child VASP account", child) 46 | } 47 | 48 | func print(title string, obj interface{}) { 49 | fmt.Printf("====== %v ======\n", title) 50 | yaml, _ := yaml.Marshal(obj) 51 | fmt.Println(string(yaml)) 52 | } 53 | -------------------------------------------------------------------------------- /examples/exampleutils/account_balances.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package exampleutils 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/diem/client-sdk-go/diemclient" 10 | "github.com/diem/client-sdk-go/diemkeys" 11 | ) 12 | 13 | // PrintAccountsBalances prints sender & receiver's account balances 14 | func PrintAccountsBalances(title string, sender, receiver *diemkeys.Keys) { 15 | fmt.Printf("\n> %v\n", title) 16 | PrintAccountBalances("sender", sender) 17 | PrintAccountBalances("receiver", receiver) 18 | } 19 | 20 | // PrintAccountBalances prints given account balances 21 | func PrintAccountBalances(name string, account *diemkeys.Keys) { 22 | RetryGetAccount: 23 | ret, err := Client.GetAccount(account.AccountAddress()) 24 | if _, ok := err.(*diemclient.StaleResponseError); ok { 25 | // retry to hit another server if got stale response 26 | goto RetryGetAccount 27 | } 28 | fmt.Println(name) 29 | for _, b := range ret.Balances { 30 | fmt.Printf(" - %v: %v\n", b.Currency, b.Amount) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/exampleutils/submit_and_wait.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package exampleutils 5 | 6 | import ( 7 | "fmt" 8 | "time" 9 | 10 | "github.com/diem/client-sdk-go/diemclient" 11 | "github.com/diem/client-sdk-go/diemkeys" 12 | "github.com/diem/client-sdk-go/diemsigner" 13 | "github.com/diem/client-sdk-go/diemtypes" 14 | "github.com/diem/client-sdk-go/testnet" 15 | ) 16 | 17 | // Client should be singleton instance for your application. 18 | // It is initialized with connection pool, see it's document 19 | // for how to configure. 20 | var Client = diemclient.New(testnet.ChainID, testnet.URL) 21 | 22 | // SubmitAndWait creates transaction for given script, then submit and wait for 23 | // the transaction executed. 24 | // Title is passed in for output with transaction version. 25 | // To keep logic simple: 26 | // - this function simply panic when got unexpected error. 27 | // - assume sender account has "XUS" currency and use it as gas currency 28 | // - always use 0 gasUnitPrice 29 | // This function returns back executed transaction version. 30 | func SubmitAndWait(title string, sender *diemkeys.Keys, script diemtypes.Script) uint64 { 31 | fmt.Println(title) 32 | address := sender.AccountAddress() 33 | Retry: 34 | account, err := Client.GetAccount(address) 35 | if err != nil { 36 | if _, ok := err.(*diemclient.StaleResponseError); ok { 37 | // retry to hit another server if got stale response 38 | goto Retry 39 | } 40 | panic(err) 41 | } 42 | sequenceNum := account.SequenceNumber 43 | // it is recommended to set short expiration time for peer to peer transaction, 44 | // as Diem blockchain transaction execution is fast. 45 | expirationDuration := 30 * time.Second 46 | expiration := uint64(time.Now().Add(expirationDuration).Unix()) 47 | txn := diemsigner.Sign( 48 | sender, 49 | address, 50 | sequenceNum, 51 | script, 52 | 1_000_000, 0, "XUS", 53 | expiration, 54 | testnet.ChainID, 55 | ) 56 | err = Client.SubmitTransaction(txn) 57 | if err != nil { 58 | if _, ok := err.(*diemclient.StaleResponseError); !ok { 59 | panic(err) 60 | } else { 61 | // ignore *diemclient.StaleResponseError as we know 62 | // submit probably succeed even hit a stale server 63 | } 64 | } 65 | transaction, err := Client.WaitForTransaction2(txn, expirationDuration) 66 | if err != nil { 67 | // WaitForTransaction retried for *diemclient.StaleResponseError 68 | // already, hence here we panic if got error (including timeout error) 69 | panic(err) 70 | } 71 | fmt.Printf("=> version: %v, status: %v\n", 72 | transaction.Version, transaction.VmStatus.Type) 73 | return transaction.Version 74 | } 75 | -------------------------------------------------------------------------------- /examples/intent-identifier/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/diem/client-sdk-go/diemid" 10 | "github.com/diem/client-sdk-go/diemkeys" 11 | "github.com/diem/client-sdk-go/diemtypes" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | func main() { 16 | merchant := diemkeys.MustGenKeys() 17 | address := merchant.AccountAddress() 18 | currency := "XUS" 19 | amount := uint64(5000) 20 | account := diemid.NewAccount( 21 | diemid.TestnetPrefix, address, diemtypes.EmptySubAddress) 22 | intent := diemid.Intent{ 23 | Account: *account, 24 | Params: diemid.Params{ 25 | Currency: currency, 26 | Amount: &amount, 27 | }, 28 | } 29 | encodedIntent, err := intent.Encode() 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Printf("==== encoded intent identifier ====\n%v\n", encodedIntent) 35 | 36 | decodedIntent, err := diemid.DecodeToIntent(diemid.TestnetPrefix, encodedIntent) 37 | if err != nil { 38 | panic(err) 39 | } 40 | fmt.Println("\n\n==== decoded intent ====") 41 | yaml, _ := yaml.Marshal(decodedIntent) 42 | fmt.Println(string(yaml)) 43 | } 44 | -------------------------------------------------------------------------------- /examples/p2p-transfers/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "crypto/ed25519" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/diem/client-sdk-go/diemtypes" 11 | "github.com/diem/client-sdk-go/examples/exampleutils" 12 | "github.com/diem/client-sdk-go/stdlib" 13 | "github.com/diem/client-sdk-go/testnet" 14 | "github.com/diem/client-sdk-go/txnmetadata" 15 | ) 16 | 17 | const currency = "XUS" 18 | 19 | func main() { 20 | nonCustodialAccount := testnet.GenAccount() 21 | nonCustodialAccount2 := testnet.GenAccount() 22 | 23 | custodialAccountParentVasp, custodialAccountChildVasp, custodialAccountSubAddress := createCustodialAccount() 24 | amount := uint64(10000) 25 | 26 | // Non custodial to non custodial has no requirement on metadata 27 | exampleutils.PrintAccountsBalances("before transfer", nonCustodialAccount, nonCustodialAccount2) 28 | exampleutils.SubmitAndWait( 29 | "non custodial account to non custodial account transaction", 30 | nonCustodialAccount, 31 | stdlib.EncodePeerToPeerWithMetadataScript( 32 | diemtypes.Currency(currency), 33 | nonCustodialAccount2.AccountAddress(), 34 | amount, 35 | nil, 36 | nil, 37 | ), 38 | ) 39 | exampleutils.PrintAccountsBalances("after transfer", nonCustodialAccount, nonCustodialAccount2) 40 | 41 | // Non custodial account to custodial account requires target custodial account subaddress, 42 | // hence we need construct a general metadata includes to_subaddress 43 | exampleutils.PrintAccountsBalances("before transfer", nonCustodialAccount, custodialAccountChildVasp) 44 | exampleutils.SubmitAndWait( 45 | "non custodial account to custodial account transaction", 46 | nonCustodialAccount, 47 | stdlib.EncodePeerToPeerWithMetadataScript( 48 | diemtypes.Currency(currency), 49 | custodialAccountChildVasp.AccountAddress(), 50 | amount, 51 | txnmetadata.NewGeneralMetadataToSubAddress(custodialAccountSubAddress), 52 | nil, // no metadata signature for GeneralMetadata 53 | ), 54 | ) 55 | exampleutils.PrintAccountsBalances("after transfer", nonCustodialAccount, custodialAccountChildVasp) 56 | 57 | // Custodial account to non-custodial account requires sender's custodial account subaddress, 58 | // hence we need construct a general metadata includes from_subaddress 59 | exampleutils.PrintAccountsBalances("before transfer", custodialAccountChildVasp, nonCustodialAccount) 60 | exampleutils.SubmitAndWait( 61 | "custodial account to non custodial account transaction", 62 | custodialAccountChildVasp, 63 | stdlib.EncodePeerToPeerWithMetadataScript( 64 | diemtypes.Currency(currency), 65 | nonCustodialAccount.AccountAddress(), 66 | amount, 67 | txnmetadata.NewGeneralMetadataFromSubAddress(custodialAccountSubAddress), 68 | nil, // no metadata signature for GeneralMetadata 69 | ), 70 | ) 71 | exampleutils.PrintAccountsBalances("after transfer", custodialAccountChildVasp, nonCustodialAccount) 72 | 73 | // Custodial account to custodial account transaction has 2 cases 74 | 75 | // setup sender custodial account 76 | _, senderCustodialAccountChildVasp, senderCustodialAccountSubAddress := createCustodialAccount() 77 | 78 | // Case 1: For transactions under the travel rule threshold, transaction metadata inclusive of both to_subaddress and from_subaddress should be composed. 79 | exampleutils.PrintAccountsBalances("before transfer", 80 | senderCustodialAccountChildVasp, custodialAccountChildVasp) 81 | exampleutils.SubmitAndWait( 82 | "custodial account to custodial account transaction under travel rule threshold", 83 | senderCustodialAccountChildVasp, 84 | stdlib.EncodePeerToPeerWithMetadataScript( 85 | diemtypes.Currency(currency), 86 | custodialAccountChildVasp.AccountAddress(), 87 | amount, 88 | txnmetadata.NewGeneralMetadataWithFromToSubAddresses( 89 | senderCustodialAccountSubAddress, 90 | custodialAccountSubAddress, 91 | ), 92 | nil, // no metadata signature for GeneralMetadata 93 | ), 94 | ) 95 | exampleutils.PrintAccountsBalances("after transfer", 96 | senderCustodialAccountChildVasp, custodialAccountChildVasp) 97 | 98 | // Case 2: For transactions over the travel rule limit, custodial to custodial transactions must exchange travel rule compliance data off-chain 99 | 100 | // setup receiver compliance public & private keys 101 | compliancePublicKey, compliancePrivateKey, _ := ed25519.GenerateKey(nil) 102 | exampleutils.SubmitAndWait( 103 | "setup parent vasp compliance key, testnet defaults it to fake key.", 104 | custodialAccountParentVasp, 105 | stdlib.EncodeRotateDualAttestationInfoScript( 106 | []byte("http://helloworld.com"), 107 | []byte(compliancePublicKey), 108 | ), 109 | ) 110 | 111 | // sender & receiver communicate by off chain APIs 112 | offChainReferenceId := "32323abc" 113 | 114 | // metadata and signature message 115 | metadata, sigMsg := txnmetadata.NewTravelRuleMetadata( 116 | offChainReferenceId, 117 | senderCustodialAccountChildVasp.AccountAddress(), 118 | amount, 119 | ) 120 | 121 | // receiver_signature is passed to the sender via the off-chain APIs as per 122 | // https://github.com/diem/lip/blob/master/lips/lip-1.mdx#recipient-signature 123 | recipientSignature := ed25519.Sign(compliancePrivateKey, sigMsg) 124 | 125 | exampleutils.PrintAccountsBalances("before transfer", 126 | senderCustodialAccountChildVasp, custodialAccountChildVasp) 127 | exampleutils.SubmitAndWait( 128 | "custodial account to custodial account transaction", 129 | senderCustodialAccountChildVasp, 130 | stdlib.EncodePeerToPeerWithMetadataScript( 131 | diemtypes.Currency(currency), 132 | custodialAccountChildVasp.AccountAddress(), //receiverAccountAddress, 133 | amount, 134 | metadata, 135 | recipientSignature, 136 | ), 137 | ) 138 | exampleutils.PrintAccountsBalances("after transfer", 139 | senderCustodialAccountChildVasp, custodialAccountChildVasp) 140 | } 141 | 142 | func createCustodialAccount() (*diemkeys.Keys, *diemkeys.Keys, diemtypes.SubAddress) { 143 | parentVASP := testnet.GenAccount() 144 | childVASPAccount := diemkeys.MustGenKeys() 145 | exampleutils.SubmitAndWait( 146 | "create child vasp for custodial account", 147 | parentVASP, 148 | stdlib.EncodeCreateChildVaspAccountScript( 149 | testnet.XUS, 150 | childVASPAccount.AccountAddress(), 151 | childVASPAccount.AuthKey().Prefix(), 152 | false, 153 | uint64(100000), 154 | ), 155 | ) 156 | custodialAccountSubAddress := diemtypes.MustGenSubAddress() 157 | return parentVASP, childVASPAccount, custodialAccountSubAddress 158 | } 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/diem/client-sdk-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/avast/retry-go v3.0.0+incompatible 7 | github.com/golang/protobuf v1.4.2 8 | github.com/novifinancial/serde-reflection/serde-generate/runtime/golang v0.0.0-20201214184956-1fd02a932898 9 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce 10 | github.com/stretchr/testify v1.6.1 11 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de 12 | golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d // indirect 13 | google.golang.org/protobuf v1.25.0 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 4 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 10 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 11 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 12 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 13 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 16 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 17 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 18 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 19 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 20 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 21 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 22 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 23 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 28 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/novifinancial/serde-reflection/serde-generate/runtime/golang v0.0.0-20201214184956-1fd02a932898 h1:lUlHSmy98ZGYJfy+zlSnKSguVfcS86ZrUGVKTLmTRx0= 30 | github.com/novifinancial/serde-reflection/serde-generate/runtime/golang v0.0.0-20201214184956-1fd02a932898/go.mod h1:NrRYJCFtaewjIRr4B9V2AyWsAEMW0Zqdjs8Bm+bACbM= 31 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= 32 | github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 38 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 41 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 42 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 43 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 44 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 45 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 46 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 48 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 49 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 52 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 57 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo= 59 | golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 62 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 63 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 64 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 65 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 66 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 67 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 68 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 69 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 70 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 71 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 72 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 73 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 74 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 75 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 76 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 77 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 78 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 79 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 80 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 81 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 82 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 83 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 84 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 88 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 90 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 91 | -------------------------------------------------------------------------------- /jsonrpc/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jsonrpc 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // Client is interface of the JSON-RPC client 18 | type Client interface { 19 | // Call with requests. When given multiple requests 20 | Call(...*Request) (map[RequestID]*Response, error) 21 | } 22 | 23 | // NewClient creates a new JSON-RPC Client. 24 | // Creates http.Transport with 3 max idle connections and 30 seconds idle timeout, and 30 seconds connection timeout 25 | // NewClientWithHTTPClient can be used to override the connection timeout 26 | // NewClientWithTransport can be used to override the underlying transport 27 | func NewClient(url string) Client { 28 | return NewClientWithHTTPClient(url, &http.Client{ 29 | Transport: &http.Transport{ 30 | MaxIdleConns: 3, 31 | IdleConnTimeout: 30 * time.Second, 32 | }, 33 | Timeout: 30 * time.Second, 34 | }) 35 | } 36 | 37 | // NewClientWithTransport creates a new JSON-RPC Client with given URL and 38 | // `*http.Transport` 39 | func NewClientWithTransport(url string, t *http.Transport) Client { 40 | return NewClientWithHTTPClient(url, &http.Client{Transport: t}) 41 | } 42 | 43 | // NewClientWithHTTPClient creates a new JSON-RPC Client with given URL and `*http.Client` 44 | func NewClientWithHTTPClient(url string, httpClient *http.Client) Client { 45 | return &client{url: url, http: httpClient} 46 | } 47 | 48 | type client struct { 49 | url string 50 | http *http.Client 51 | } 52 | 53 | // Call implements Client interface 54 | func (c *client) Call(requests ...*Request) (map[RequestID]*Response, error) { 55 | switch len(requests) { 56 | case 0: 57 | return nil, errors.New("no requests") 58 | case 1: 59 | request := requests[0] 60 | reqBody, err := json.Marshal(request) 61 | if err != nil { 62 | return nil, newError(SerializeRequestJsonError, err) 63 | } 64 | var resp Response 65 | if err = c.httpPost(reqBody, &resp); err != nil { 66 | return nil, err 67 | } 68 | return valid(requests, &resp) 69 | default: 70 | reqBody, err := json.Marshal(requests) 71 | if err != nil { 72 | return nil, newError(SerializeRequestJsonError, err) 73 | } 74 | var resps []*Response 75 | if err = c.httpPost(reqBody, &resps); err != nil { 76 | return nil, err 77 | } 78 | return valid(requests, resps...) 79 | } 80 | } 81 | 82 | func (c *client) httpPost(body []byte, ret interface{}) error { 83 | resp, err := c.http.Post(c.url, "application/json", bytes.NewBuffer(body)) 84 | if err != nil { 85 | return newError(HttpCallError, err) 86 | } 87 | 88 | defer resp.Body.Close() 89 | body, err = ioutil.ReadAll(resp.Body) 90 | if err != nil { 91 | return newError(ReadHttpResponseBodyError, err) 92 | } 93 | 94 | if resp.StatusCode != 200 { 95 | return newError(HttpCallError, fmt.Errorf( 96 | "Failed https call: %d, %s", resp.StatusCode, string(body))) 97 | } 98 | 99 | if err = json.Unmarshal(body, ret); err != nil { 100 | return newError(ParseResponseJsonError, err) 101 | } 102 | return nil 103 | } 104 | 105 | func valid(requests []*Request, resps ...*Response) (map[RequestID]*Response, error) { 106 | ret := make(map[RequestID]*Response) 107 | for _, resp := range resps { 108 | if err := resp.Validate(); err != nil { 109 | return nil, err 110 | } 111 | if resp.ID != nil { 112 | ret[*resp.ID] = resp 113 | } 114 | } 115 | var missing []string 116 | for _, req := range requests { 117 | if _, ok := ret[req.ID]; !ok { 118 | missing = append(missing, req.ToString()) 119 | } 120 | } 121 | if len(missing) > 0 { 122 | return ret, newError(InvalidJsonRpcResponseError, fmt.Errorf( 123 | "missing responses for requests: \n%s", strings.Join(missing, "\n"))) 124 | } 125 | return ret, nil 126 | } 127 | -------------------------------------------------------------------------------- /jsonrpc/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jsonrpc_test 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "strconv" 13 | "testing" 14 | 15 | "github.com/diem/client-sdk-go/jsonrpc" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | type expectation func(*testing.T, *jsonrpc.Response, *jsonrpc.Error) 21 | 22 | func TestCall(t *testing.T) { 23 | cases := []struct { 24 | name string 25 | method jsonrpc.Method 26 | params []jsonrpc.Param 27 | url string 28 | serve string 29 | expect expectation 30 | }{ 31 | { 32 | name: "success", 33 | method: "get_code", 34 | serve: `{"jsonrpc": "2.0", "result": {"code": 1, "msg": "hello"}, "id": 1}`, 35 | expect: response_result(), 36 | }, 37 | { 38 | name: "response result == null", 39 | method: "get_code", 40 | serve: `{"jsonrpc": "2.0", "result": null, "id": 1}`, 41 | expect: func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 42 | assert.Nil(t, err) 43 | require.NotNil(t, resp) 44 | assert.Nil(t, resp.Error) 45 | assert.Nil(t, resp.Result) 46 | 47 | var ret result 48 | ok, unmarshalErr := resp.UnmarshalResult(&ret) 49 | assert.False(t, ok) 50 | assert.NoError(t, unmarshalErr) 51 | assert.Equal(t, result{}, ret) 52 | }, 53 | }, 54 | { 55 | name: "success with params", 56 | method: "get_code", 57 | params: []jsonrpc.Param{"hello", 1}, 58 | serve: `{"jsonrpc": "2.0", "result": {"code": 1, "msg": "hello"}, "id": 1}`, 59 | expect: response_result(), 60 | }, 61 | { 62 | name: "success with result and diem extension fields", 63 | method: "get_code", 64 | params: []jsonrpc.Param{"hello", 1}, 65 | serve: `{ 66 | "jsonrpc": "2.0", 67 | "result": {"code": 1, "msg": "hello"}, 68 | "diem_chain_id": 2, 69 | "diem_ledger_timestampusec": 3, 70 | "diem_ledger_version": 4, 71 | "id": 1 72 | }`, 73 | expect: list(response_result(), diem_extension()), 74 | }, 75 | { 76 | name: "success with error and diem extension fields", 77 | method: "get_code", 78 | params: []jsonrpc.Param{"hello", 1}, 79 | serve: `{ 80 | "jsonrpc": "2.0", 81 | "error": {"code": 32000, "message": "hello world", "data": {"foo": "bar"}}, 82 | "diem_chain_id": 2, 83 | "diem_ledger_timestampusec": 3, 84 | "diem_ledger_version": 4, 85 | "id": 1 86 | }`, 87 | expect: list( 88 | response_error(32000, "hello world", map[string]interface{}{"foo": "bar"}), 89 | diem_extension(), 90 | ), 91 | }, 92 | { 93 | name: "invalid json response", 94 | method: "get_code", 95 | serve: `{ ... }`, 96 | expect: expectError(jsonrpc.ParseResponseJsonError), 97 | }, 98 | { 99 | name: "invalid jsonrpc response: jsonrpc version is not 2.0", 100 | method: "get_code", 101 | serve: `{}`, 102 | expect: expectError(jsonrpc.InvalidJsonRpcResponseError), 103 | }, 104 | { 105 | name: "invalid jsonrpc response: invalid result json", 106 | method: "get_code", 107 | serve: `{"jsonrpc": "2.0", "result": { ... }, "id": 1}`, 108 | expect: expectError(jsonrpc.ParseResponseJsonError), 109 | }, 110 | { 111 | name: "jsonrpc response type mismatch", 112 | method: "get_another_code", 113 | serve: `{"jsonrpc": "2.0", "result": {"code": "hello", "msg": "hello"}, "id": 1}`, 114 | expect: func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 115 | assert.Nil(t, err) 116 | require.NotNil(t, resp) 117 | assert.Nil(t, resp.Error) 118 | assert.NotNil(t, resp.Result) 119 | 120 | ok, unmarshalErr := resp.UnmarshalResult(new(result)) 121 | assert.False(t, ok) 122 | assert.Error(t, unmarshalErr) 123 | assert.Equal(t, jsonrpc.ParseResponseResultJsonError, 124 | unmarshalErr.(*jsonrpc.Error).ErrorType) 125 | }, 126 | }, 127 | { 128 | name: "http call error", 129 | method: "get_code", 130 | url: "invalid", 131 | expect: expectError(jsonrpc.HttpCallError), 132 | }, 133 | { 134 | name: "serialize request error", 135 | method: "get_code", 136 | params: []jsonrpc.Param{func() {}}, 137 | expect: expectError(jsonrpc.SerializeRequestJsonError), 138 | }, 139 | } 140 | for _, tc := range cases { 141 | t.Run(tc.name, func(t *testing.T) { 142 | request := jsonrpc.NewRequest(tc.method, tc.params...) 143 | server := serve(t, tc.serve, request) 144 | defer server.Close() 145 | if tc.url == "" { 146 | tc.url = server.URL 147 | } 148 | client := jsonrpc.NewClient(tc.url) 149 | resp, err := client.Call(request) 150 | jerr, _ := err.(*jsonrpc.Error) 151 | require.True(t, resp == nil || len(resp) == 1) 152 | tc.expect(t, resp[1], jerr) 153 | }) 154 | } 155 | } 156 | 157 | func TestBatchRequests(t *testing.T) { 158 | cases := []struct { 159 | name string 160 | requests []*jsonrpc.Request 161 | serve string 162 | expect func(*testing.T, map[jsonrpc.RequestID]*jsonrpc.Response, error) 163 | }{ 164 | { 165 | name: "success", 166 | requests: []*jsonrpc.Request{ 167 | jsonrpc.NewRequestWithID(1, "get_code"), 168 | jsonrpc.NewRequestWithID(2, "get_code"), 169 | }, 170 | serve: `[ 171 | {"jsonrpc": "2.0", "result": {"code": 2, "msg": "hello"}, "id": 2}, 172 | {"jsonrpc": "2.0", "result": {"code": 1, "msg": "world"}, "id": 1} 173 | ]`, 174 | expect: func(t *testing.T, resp map[jsonrpc.RequestID]*jsonrpc.Response, err error) { 175 | require.NoError(t, err) 176 | require.Len(t, resp, 2) 177 | require.NotNil(t, resp[1]) 178 | require.NotNil(t, resp[2]) 179 | var ret result 180 | ok, _ := resp[1].UnmarshalResult(&ret) 181 | require.True(t, ok) 182 | assert.Equal(t, uint64(1), ret.Code) 183 | assert.Equal(t, "world", ret.Msg) 184 | 185 | ok, _ = resp[2].UnmarshalResult(&ret) 186 | require.True(t, ok) 187 | assert.Equal(t, uint64(2), ret.Code) 188 | assert.Equal(t, "hello", ret.Msg) 189 | }, 190 | }, 191 | { 192 | name: "success with one error", 193 | requests: []*jsonrpc.Request{ 194 | jsonrpc.NewRequestWithID(1, "get_code"), 195 | jsonrpc.NewRequestWithID(2, "get_code"), 196 | }, 197 | serve: `[ 198 | {"jsonrpc": "2.0", "result": {"code": 2, "msg": "hello"}, "id": 2}, 199 | {"jsonrpc": "2.0", "error": {"code": 32000, "message": "hello world", "data": null}, "id": 1} 200 | ]`, 201 | expect: func(t *testing.T, resp map[jsonrpc.RequestID]*jsonrpc.Response, err error) { 202 | require.NoError(t, err) 203 | require.Len(t, resp, 2) 204 | require.NotNil(t, resp[1]) 205 | require.NotNil(t, resp[2]) 206 | var ret result 207 | ok, _ := resp[1].UnmarshalResult(&ret) 208 | require.False(t, ok) 209 | assert.Error(t, resp[1].Error) 210 | 211 | ok, _ = resp[2].UnmarshalResult(&ret) 212 | require.True(t, ok) 213 | assert.Equal(t, uint64(2), ret.Code) 214 | assert.Equal(t, "hello", ret.Msg) 215 | }, 216 | }, 217 | { 218 | name: "serialize request error", 219 | requests: []*jsonrpc.Request{ 220 | jsonrpc.NewRequestWithID(1, "get_code"), 221 | jsonrpc.NewRequestWithID(2, "get_code", func() {}), 222 | }, 223 | expect: func(t *testing.T, resp map[jsonrpc.RequestID]*jsonrpc.Response, err error) { 224 | require.Nil(t, resp) 225 | require.Error(t, err) 226 | expectError(jsonrpc.SerializeRequestJsonError)(t, nil, err.(*jsonrpc.Error)) 227 | }, 228 | }, 229 | { 230 | name: "invalid response: missing response", 231 | requests: []*jsonrpc.Request{ 232 | jsonrpc.NewRequestWithID(1, "get_code"), 233 | jsonrpc.NewRequestWithID(2, "get_code"), 234 | }, 235 | serve: `[ 236 | {"jsonrpc": "2.0", "result": {"code": 2, "msg": "hello"}, "id": 2} 237 | ]`, 238 | expect: func(t *testing.T, resp map[jsonrpc.RequestID]*jsonrpc.Response, err error) { 239 | require.Error(t, err) 240 | assert.Equal(t, jsonrpc.InvalidJsonRpcResponseError, 241 | err.(*jsonrpc.Error).ErrorType) 242 | assert.Len(t, resp, 1) 243 | }, 244 | }, 245 | { 246 | name: "invalid response: invalid json", 247 | requests: []*jsonrpc.Request{ 248 | jsonrpc.NewRequestWithID(1, "get_code"), 249 | jsonrpc.NewRequestWithID(2, "get_code"), 250 | }, 251 | serve: `{"jsonrpc": "2.0", "result": {"code": 2, "msg": "hello"}, "id": 2}`, 252 | expect: func(t *testing.T, resp map[jsonrpc.RequestID]*jsonrpc.Response, err error) { 253 | require.Error(t, err) 254 | assert.Equal(t, jsonrpc.ParseResponseJsonError, 255 | err.(*jsonrpc.Error).ErrorType) 256 | assert.Nil(t, resp) 257 | }, 258 | }, 259 | } 260 | for _, tc := range cases { 261 | t.Run(tc.name, func(t *testing.T) { 262 | server := serve(t, tc.serve, tc.requests...) 263 | defer server.Close() 264 | client := jsonrpc.NewClient(server.URL) 265 | resps, err := client.Call(tc.requests...) 266 | tc.expect(t, resps, err) 267 | }) 268 | } 269 | } 270 | 271 | func TestNoRequestsCall(t *testing.T) { 272 | client := jsonrpc.NewClient("url") 273 | resps, err := client.Call() 274 | assert.Error(t, err) 275 | assert.Nil(t, resps) 276 | } 277 | 278 | func TestHandleNon200Response(t *testing.T) { 279 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 280 | w.WriteHeader(http.StatusBadRequest) 281 | })) 282 | defer server.Close() 283 | client := jsonrpc.NewClient(server.URL) 284 | resps, err := client.Call(jsonrpc.NewRequest("hello")) 285 | require.Error(t, err) 286 | assert.Equal(t, jsonrpc.HttpCallError, err.(*jsonrpc.Error).ErrorType) 287 | assert.Nil(t, resps) 288 | } 289 | 290 | func serve(t *testing.T, content string, expectedReqs ...*jsonrpc.Request) *httptest.Server { 291 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 292 | assert.Equal(t, "POST", r.Method) 293 | 294 | defer r.Body.Close() 295 | body, err := ioutil.ReadAll(r.Body) 296 | require.NoError(t, err) 297 | 298 | if len(expectedReqs) == 1 { 299 | req := make(map[string]interface{}) 300 | err = json.Unmarshal(body, &req) 301 | require.NoError(t, err) 302 | expectSame(t, expectedReqs[0], req) 303 | } else { 304 | var req []map[string]interface{} 305 | err = json.Unmarshal(body, &req) 306 | require.NoError(t, err) 307 | assert.Equal(t, len(expectedReqs), len(req)) 308 | for i := range req { 309 | expectSame(t, expectedReqs[i], req[i]) 310 | } 311 | } 312 | 313 | fmt.Fprintln(w, content) 314 | })) 315 | } 316 | 317 | func expectSame(t *testing.T, expected *jsonrpc.Request, req map[string]interface{}) { 318 | assert.Equal(t, "2.0", req["jsonrpc"]) 319 | assert.Equal(t, string(expected.Method), req["method"]) 320 | 321 | reqParams := req["params"].([]interface{}) 322 | require.NotNil(t, reqParams) 323 | assert.Len(t, reqParams, len(expected.Params)) 324 | for i, expected := range expected.Params { 325 | assert.EqualValues(t, expected, reqParams[i]) 326 | } 327 | } 328 | 329 | func response_result() expectation { 330 | return func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 331 | assert.Nil(t, err) 332 | require.NotNil(t, resp) 333 | assert.Nil(t, resp.Error) 334 | assert.NotNil(t, resp.Result) 335 | 336 | var ret result 337 | ok, unmarshalErr := resp.UnmarshalResult(&ret) 338 | assert.True(t, ok) 339 | assert.NoError(t, unmarshalErr) 340 | assert.Equal(t, uint64(1), ret.Code) 341 | assert.Equal(t, "hello", ret.Msg) 342 | } 343 | } 344 | 345 | func response_error(code int, msg string, data interface{}) expectation { 346 | return func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 347 | assert.Nil(t, err) 348 | require.NotNil(t, resp) 349 | assert.NotNil(t, resp.Error) 350 | assert.Nil(t, resp.Result) 351 | 352 | assert.Contains(t, resp.Error.Error(), strconv.Itoa(code)) 353 | assert.Contains(t, resp.Error.Error(), msg) 354 | 355 | assert.Equal(t, int32(code), resp.Error.Code) 356 | assert.Equal(t, msg, resp.Error.Message) 357 | assert.EqualValues(t, data, resp.Error.Data) 358 | } 359 | } 360 | 361 | func diem_extension() expectation { 362 | return func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 363 | require.NotNil(t, resp) 364 | assert.Equal(t, byte(2), resp.DiemChainID) 365 | assert.Equal(t, uint64(3), resp.DiemLedgerTimestampusec) 366 | assert.Equal(t, uint64(4), resp.DiemLedgerVersion) 367 | } 368 | } 369 | 370 | func expectError(errorType jsonrpc.ErrorType) expectation { 371 | return func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 372 | assert.Nil(t, resp) 373 | require.Error(t, err) 374 | require.NotNil(t, err) 375 | assert.Equal(t, errorType, err.ErrorType) 376 | assert.Contains(t, err.Error(), errorType) 377 | } 378 | } 379 | 380 | func list(exps ...expectation) expectation { 381 | return func(t *testing.T, resp *jsonrpc.Response, err *jsonrpc.Error) { 382 | for _, exp := range exps { 383 | exp(t, resp, err) 384 | } 385 | } 386 | } 387 | 388 | type result struct { 389 | Code uint64 `json:"code"` 390 | Msg string `json:"msg"` 391 | } 392 | -------------------------------------------------------------------------------- /jsonrpc/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides a general JSON-RPC client, decodes Diem extension fields. 5 | package jsonrpc 6 | -------------------------------------------------------------------------------- /jsonrpc/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jsonrpc 5 | 6 | import "fmt" 7 | 8 | // ErrorType is type of the `Error`, which maybe caused by different underlining errors 9 | type ErrorType string 10 | 11 | const ( 12 | SerializeRequestJsonError ErrorType = "serialize request json failed" 13 | HttpCallError ErrorType = "http call failed" 14 | ReadHttpResponseBodyError ErrorType = "read http response body failed" 15 | ParseResponseJsonError ErrorType = "parse response json failed" 16 | ParseResponseResultJsonError ErrorType = "parse response result json failed" 17 | InvalidJsonRpcResponseError ErrorType = "invalid JSON-RPC response: missing result / error field" 18 | ) 19 | 20 | // Error is a wrap of a type and underlining `Cause` error 21 | type Error struct { 22 | ErrorType ErrorType 23 | Cause error 24 | } 25 | 26 | // newError creates new `Error` by gien type and cause 27 | func newError(t ErrorType, cause error) *Error { 28 | return &Error{t, cause} 29 | } 30 | 31 | // Error returns `ErrorType` + `Cause#Error()` as message 32 | func (e *Error) Error() string { 33 | return fmt.Sprintf("%s: %s", e.ErrorType, e.Cause.Error()) 34 | } 35 | -------------------------------------------------------------------------------- /jsonrpc/jsonrpctest/stub.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides a simple json-rpc client stub for testing client without connecting to remote server. 5 | package jsonrpctest 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/diem/client-sdk-go/jsonrpc" 11 | "github.com/diem/client-sdk-go/testnet" 12 | ) 13 | 14 | type Stub struct { 15 | Responses map[jsonrpc.RequestID]jsonrpc.Response 16 | } 17 | 18 | func (s *Stub) Call(requests ...*jsonrpc.Request) (map[jsonrpc.RequestID]*jsonrpc.Response, error) { 19 | ret := make(map[jsonrpc.RequestID]*jsonrpc.Response) 20 | for _, req := range requests { 21 | resp := s.Responses[req.ID] 22 | resp.JsonRpc = req.JsonRpc 23 | resp.ID = &req.ID 24 | if resp.DiemChainID == 0 { 25 | resp.DiemChainID = testnet.ChainID 26 | } 27 | if resp.DiemLedgerTimestampusec == 0 { 28 | resp.DiemLedgerTimestampusec = uint64(time.Now().Unix() * 1000000) 29 | } 30 | if resp.DiemLedgerVersion == 0 { 31 | resp.DiemLedgerVersion = 100 32 | } 33 | 34 | ret[req.ID] = &resp 35 | } 36 | return ret, nil 37 | } 38 | -------------------------------------------------------------------------------- /jsonrpc/request.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jsonrpc 5 | 6 | import "encoding/json" 7 | 8 | const ( 9 | jsonRpcVersion = "2.0" 10 | ) 11 | 12 | // Method is type of JSON-RPC method 13 | type Method string 14 | 15 | // Param is type of JSON-RPC params list 16 | type Param interface{} 17 | 18 | // RequestID is type for request ID. 19 | type RequestID uint 20 | 21 | // Request is type of JSON-RPC request struct 22 | type Request struct { 23 | Method Method `json:"method"` 24 | Params []Param `json:"params"` 25 | ID RequestID `json:"id"` 26 | JsonRpc string `json:"jsonrpc"` 27 | } 28 | 29 | // NewRequest creates `Request` with default `JsonRpc` = "2.0" and ID = 1 30 | func NewRequest(m Method, params ...Param) *Request { 31 | return NewRequestWithID(1, m, params...) 32 | } 33 | 34 | // NewRequestWithID creates `Request` with default `JsonRpc` = "2.0" 35 | func NewRequestWithID(id RequestID, m Method, params ...Param) *Request { 36 | if params == nil { 37 | params = []Param{} 38 | } 39 | return &Request{m, params, id, jsonRpcVersion} 40 | } 41 | 42 | // ToString returns json format of the request 43 | func (r *Request) ToString() string { 44 | b, _ := json.MarshalIndent(r, "", "\t") 45 | return string(b) 46 | } 47 | -------------------------------------------------------------------------------- /jsonrpc/response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package jsonrpc 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | ) 10 | 11 | // ResponseError is type of JSON-RPC response error, it implements error interface. 12 | type ResponseError struct { 13 | Code int32 `json:"code"` 14 | Message string `json:"message"` 15 | Data interface{} `json:"data"` 16 | } 17 | 18 | // Error implements error interface, returns `Code` + `Message` 19 | func (e *ResponseError) Error() string { 20 | return fmt.Sprintf("%d - %s", e.Code, e.Message) 21 | } 22 | 23 | // Response is type of JSON-RPC response struct 24 | type Response struct { 25 | JsonRpc string `json:"jsonrpc"` 26 | ID *RequestID `json:"id"` 27 | Result *json.RawMessage `json:"result"` 28 | Error *ResponseError `json:"error"` 29 | DiemChainID byte `json:"diem_chain_id"` 30 | DiemLedgerTimestampusec uint64 `json:"diem_ledger_timestampusec"` 31 | DiemLedgerVersion uint64 `json:"diem_ledger_version"` 32 | } 33 | 34 | // UnmarshalResult unmarshals result json into given struct. 35 | // Returns true, nil for success unmarshal, otherwise first bool 36 | // will always be false. 37 | // Returns false, nil if `Result` is nil. 38 | func (r *Response) UnmarshalResult(result interface{}) (bool, error) { 39 | if r.Result == nil { 40 | return false, nil 41 | } 42 | 43 | if err := json.Unmarshal(*r.Result, result); err != nil { 44 | return false, newError(ParseResponseResultJsonError, err) 45 | } 46 | return true, nil 47 | } 48 | 49 | // Validate validates response data with JSON-RPC 2.0 spec 50 | func (r *Response) Validate() error { 51 | if r.JsonRpc != "2.0" { 52 | return newError(InvalidJsonRpcResponseError, 53 | fmt.Errorf("unexpected jsonrpc version: %s", r.JsonRpc)) 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /stdlib/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides Diem move language stdlib script encoding and decoding utilities. 5 | package stdlib 6 | -------------------------------------------------------------------------------- /testnet/const.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testnet 5 | 6 | import ( 7 | "github.com/diem/client-sdk-go/diemclient" 8 | "github.com/diem/client-sdk-go/diemtypes" 9 | ) 10 | 11 | const ( 12 | URL = "http://testnet.diem.com/v1" 13 | FaucetURL = "http://testnet.diem.com/mint" 14 | ChainID byte = 2 15 | ) 16 | 17 | var ( 18 | // DDAccountAddress is testnet default dd account address 19 | DDAccountAddress = diemtypes.MustMakeAccountAddress("000000000000000000000000000000DD") 20 | // Client is testnet client 21 | Client = diemclient.New(ChainID, URL) 22 | ) 23 | 24 | // Currencies 25 | var ( 26 | XDX = diemtypes.Currency("XDX") 27 | XUS = diemtypes.Currency("XUS") 28 | ) 29 | -------------------------------------------------------------------------------- /testnet/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides Diem Testnet testing utilities. 5 | package testnet 6 | -------------------------------------------------------------------------------- /testnet/faucet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testnet 5 | 6 | import ( 7 | "bytes" 8 | "encoding/hex" 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/diem/client-sdk-go/diemkeys" 15 | "github.com/diem/client-sdk-go/diemtypes" 16 | "github.com/novifinancial/serde-reflection/serde-generate/runtime/golang/bcs" 17 | ) 18 | 19 | // GenAccount generate account with single keys 20 | func GenAccount() *diemkeys.Keys { 21 | keys := diemkeys.MustGenKeys() 22 | MustMint(keys.AuthKey().Hex(), 1000000, "XUS") 23 | return keys 24 | } 25 | 26 | // GenMultiSigAccount generate account with multi sig keys 27 | func GenMultiSigAccount() *diemkeys.Keys { 28 | keys := diemkeys.MustGenMultiSigKeys() 29 | MustMint(keys.AuthKey().Hex(), 2000000, "XUS") 30 | return keys 31 | } 32 | 33 | // MustMint mints coins with retry, and panics if all retries failed. 34 | // This func also wait for next account seq. 35 | func MustMint(authKey string, amount uint64, currencyCode string) { 36 | retry := 5 37 | var err error 38 | var txns []diemtypes.SignedTransaction 39 | for i := 0; i < retry; i++ { 40 | if txns, err = Mint(authKey, amount, currencyCode); err == nil { 41 | if err = waitForTransactionsExecuted(txns); err == nil { 42 | return 43 | } 44 | } 45 | time.Sleep(500 * time.Millisecond) 46 | } 47 | panic(fmt.Sprintf("mint coins failed with retry: %s", err)) 48 | } 49 | 50 | // Mint mints coints once without retry 51 | func Mint(authKey string, amount uint64, currencyCode string) ([]diemtypes.SignedTransaction, error) { 52 | url := fmt.Sprintf("%v?amount=%d&auth_key=%s¤cy_code=%s&return_txns=true", FaucetURL, amount, authKey, currencyCode) 53 | resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte{})) 54 | if err != nil { 55 | return nil, err 56 | } 57 | defer resp.Body.Close() 58 | body, err := ioutil.ReadAll(resp.Body) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if resp.StatusCode != 200 { 63 | return nil, fmt.Errorf("Non 200 response: %s", string(body)) 64 | } 65 | 66 | return deserializeMintTransactions(body) 67 | } 68 | 69 | func waitForTransactionsExecuted(txns []diemtypes.SignedTransaction) error { 70 | for i := range txns { 71 | _, err := Client.WaitForTransaction2(&txns[i], time.Second*30) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func deserializeMintTransactions(body []byte) ([]diemtypes.SignedTransaction, error) { 80 | bytes, err := hex.DecodeString(string(body)) 81 | if err != nil { 82 | return nil, fmt.Errorf("decode mint transactions hex string failed: %v", err) 83 | } 84 | deserializer := bcs.NewDeserializer(bytes) 85 | length, err := deserializer.DeserializeLen() 86 | if err != nil { 87 | return nil, fmt.Errorf("deserialize mint transactions length failed: %v", err) 88 | } 89 | ret := make([]diemtypes.SignedTransaction, length) 90 | for i := range ret { 91 | val, err := diemtypes.DeserializeSignedTransaction(deserializer) 92 | if err != nil { 93 | return nil, fmt.Errorf("deserialize %v mint transaction failed: %v", i, err) 94 | } 95 | ret[i] = val 96 | } 97 | 98 | return ret, nil 99 | } 100 | -------------------------------------------------------------------------------- /testnet/faucet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package testnet_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/diem/client-sdk-go/diemkeys" 10 | "github.com/diem/client-sdk-go/testnet" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestMint(t *testing.T) { 16 | keys := diemkeys.MustGenKeys() 17 | testnet.MustMint(keys.AuthKey().Hex(), 1000, "XUS") 18 | account, err := testnet.Client.GetAccount(keys.AccountAddress()) 19 | require.NoError(t, err) 20 | 21 | assert.Len(t, account.Balances, 1) 22 | assert.Equal(t, "XUS", account.Balances[0].Currency) 23 | assert.Equal(t, uint64(1000), account.Balances[0].Amount) 24 | } 25 | 26 | func TestMustMintPanic(t *testing.T) { 27 | defer func() { 28 | if r := recover(); r != nil { 29 | return 30 | } 31 | assert.Fail(t, "should panic, but not") 32 | }() 33 | 34 | testnet.MustMint("invalid", 1000, "HELLO") 35 | } 36 | -------------------------------------------------------------------------------- /txnmetadata/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Provides utility functions for creating peer to peer transaction metadata 5 | // (https://github.com/diem/lip/blob/master/lips/lip-4.md). 6 | package txnmetadata 7 | -------------------------------------------------------------------------------- /txnmetadata/metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package txnmetadata 5 | 6 | import ( 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/diem/client-sdk-go/diemclient" 12 | "github.com/diem/client-sdk-go/diemtypes" 13 | "github.com/novifinancial/serde-reflection/serde-generate/runtime/golang/bcs" 14 | ) 15 | 16 | // NewTravelRuleMetadata creates metadata and signature message for given 17 | // offChainReferenceID. 18 | // This is used for peer to peer transfer between 2 custodial accounts. 19 | func NewTravelRuleMetadata( 20 | offChainReferenceID string, 21 | senderAccountAddress diemtypes.AccountAddress, 22 | amount uint64, 23 | ) ([]byte, []byte) { 24 | metadata := diemtypes.Metadata__TravelRuleMetadata{ 25 | Value: &diemtypes.TravelRuleMetadata__TravelRuleMetadataVersion0{ 26 | Value: diemtypes.TravelRuleMetadataV0{ 27 | OffChainReferenceId: &offChainReferenceID, 28 | }, 29 | }, 30 | } 31 | 32 | s := bcs.NewSerializer() 33 | metadata.Serialize(s) 34 | senderAccountAddress.Serialize(s) 35 | s.SerializeU64(amount) 36 | sigMsg := append(s.GetBytes(), []byte("@@$$DIEM_ATTEST$$@@")...) 37 | 38 | return diemtypes.ToBCS(&metadata), sigMsg 39 | } 40 | 41 | // NewGeneralMetadataToSubAddress creates metadata for creating peer to peer 42 | // transaction script with ToSubaddress 43 | // This is used for peer to peer transfer from non-custodial account to custodial account. 44 | func NewGeneralMetadataToSubAddress(toSubAddress diemtypes.SubAddress) []byte { 45 | to := toSubAddress[:] 46 | return newGeneralMetadata(nil, &to) 47 | } 48 | 49 | // NewGeneralMetadataFromSubAddress creates metadata for creating peer to peer 50 | // transaction script with FromSubaddress 51 | // This is used for peer to peer transfer from custodial account to non-custodial account. 52 | func NewGeneralMetadataFromSubAddress(fromSubAddress diemtypes.SubAddress) []byte { 53 | from := fromSubAddress[:] 54 | return newGeneralMetadata(&from, nil) 55 | } 56 | 57 | // NewGeneralMetadataWithFromToSubaddresses creates metadata for creating peer to peer 58 | // transaction script with fromSubaddress and toSubaddress. 59 | // Use this function to create metadata with from and to subaddresses for peer to peer transfer 60 | // from custodial account to custodial account under travel rule threshold. 61 | func NewGeneralMetadataWithFromToSubAddresses(fromSubAddress diemtypes.SubAddress, toSubAddress diemtypes.SubAddress) []byte { 62 | from := fromSubAddress[:] 63 | to := toSubAddress[:] 64 | return newGeneralMetadata(&from, &to) 65 | } 66 | 67 | // newGeneralMetadata is internal methods for constructing with *[]byte as from and to subaddress type 68 | func newGeneralMetadata(fromSubAddress *[]byte, toSubAddress *[]byte) []byte { 69 | metadata := diemtypes.Metadata__GeneralMetadata{ 70 | Value: &diemtypes.GeneralMetadata__GeneralMetadataVersion0{ 71 | Value: diemtypes.GeneralMetadataV0{ 72 | FromSubaddress: fromSubAddress, 73 | ToSubaddress: toSubAddress, 74 | }, 75 | }, 76 | } 77 | return diemtypes.ToBCS(&metadata) 78 | } 79 | 80 | // NewRefundMetadata creates metadata for creating refund p2p transaction script with original payment 81 | // transaction version and reason 82 | func NewRefundMetadata(originalTransactionVersion uint64, reason diemtypes.RefundReason) []byte { 83 | metadata := diemtypes.Metadata__RefundMetadata{ 84 | Value: &diemtypes.RefundMetadata__RefundMetadataV0{ 85 | Value: diemtypes.RefundMetadataV0{ 86 | TransactionVersion: originalTransactionVersion, 87 | Reason: reason, 88 | }, 89 | }, 90 | } 91 | return diemtypes.ToBCS(&metadata) 92 | } 93 | 94 | // NewCoinTradeMetadata creates metadata for creating coin trade p2p transaction script with 95 | // a list of trade ids 96 | func NewCoinTradeMetadata(tradeIds []string) []byte { 97 | metadata := diemtypes.Metadata__CoinTradeMetadata{ 98 | Value: &diemtypes.CoinTradeMetadata__CoinTradeMetadataV0{ 99 | Value: diemtypes.CoinTradeMetadataV0{ 100 | TradeIds: tradeIds, 101 | }, 102 | }, 103 | } 104 | return diemtypes.ToBCS(&metadata) 105 | } 106 | 107 | // FindRefundReferenceEventFromTransaction looks for receivedpayment type event in the 108 | // given transaction and event receiver is given receiver account address. 109 | func FindRefundReferenceEventFromTransaction(txn *diemclient.Transaction, receiver diemtypes.AccountAddress) *diemclient.Event { 110 | if txn == nil { 111 | return nil 112 | } 113 | address := receiver.Hex() 114 | for i, event := range txn.Events { 115 | if event.Data.Type == "receivedpayment" && 116 | event.Data.Receiver == address { 117 | return txn.Events[i] 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | // DeserializeMetadata decodes given event's metadata. 124 | // Returns error if given event is nil 125 | // Returns nil without error if given event has no metadata 126 | // Returns error if deserialization failed. 127 | func DeserializeMetadata(event *diemclient.Event) (diemtypes.Metadata, error) { 128 | if event == nil { 129 | return nil, errors.New("must provide refund reference event") 130 | } 131 | if event.Data.Metadata == "" { 132 | return nil, nil 133 | } 134 | bytes, err := hex.DecodeString(event.Data.Metadata) 135 | if err != nil { 136 | return nil, fmt.Errorf("decode event metadata failed: %v", err.Error()) 137 | } 138 | metadata, err := diemtypes.DeserializeMetadata(bcs.NewDeserializer(bytes)) 139 | if err != nil { 140 | return nil, fmt.Errorf("can't deserialize metadata: %v", err) 141 | } 142 | return metadata, nil 143 | } 144 | 145 | // NewRefundMetadataFromEventMetadata creates GeneralMetadata for refunding a receivedpayment event. 146 | // Returns error if given `gm` is nil. 147 | // Returns InvalidGeneralMetadataError if given event metadata is not 148 | // `*diemtypes.GeneralMetadata__GeneralMetadataVersion0`. 149 | // 150 | // Note: for a receivedpayment event with TravelRuleMetadata, refund transaction is same with transfer money 151 | // transaction, no need refund metadata constructed like this function does. 152 | // 153 | // Deprecated: prefer NewRefundMetadata to create refund metadata. 154 | func NewRefundMetadataFromEventMetadata(eventSequenceNumber uint64, gm *diemtypes.Metadata__GeneralMetadata) ([]byte, error) { 155 | if gm == nil { 156 | return nil, errors.New("must provide refund event general metadata") 157 | } 158 | gmv0, ok := gm.Value.(*diemtypes.GeneralMetadata__GeneralMetadataVersion0) 159 | if !ok { 160 | return nil, fmt.Errorf("can't handle GeneralMetadata: %T", gm.Value) 161 | } 162 | return diemtypes.ToBCS(&diemtypes.Metadata__GeneralMetadata{ 163 | Value: &diemtypes.GeneralMetadata__GeneralMetadataVersion0{ 164 | Value: diemtypes.GeneralMetadataV0{ 165 | FromSubaddress: gmv0.Value.ToSubaddress, 166 | ToSubaddress: gmv0.Value.FromSubaddress, 167 | ReferencedEvent: &eventSequenceNumber, 168 | }, 169 | }, 170 | }), nil 171 | } 172 | -------------------------------------------------------------------------------- /txnmetadata/metadata_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Diem Core Contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package txnmetadata_test 5 | 6 | import ( 7 | "encoding/hex" 8 | "testing" 9 | 10 | "github.com/diem/client-sdk-go/diemclient" 11 | "github.com/diem/client-sdk-go/diemclient/diemclienttest" 12 | "github.com/diem/client-sdk-go/diemkeys" 13 | "github.com/diem/client-sdk-go/diemtypes" 14 | "github.com/diem/client-sdk-go/txnmetadata" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestNewTravelRuleMetadata(t *testing.T) { 20 | address, _ := diemtypes.MakeAccountAddress("f72589b71ff4f8d139674a3f7369c69b") 21 | metadata, sigMsg := txnmetadata.NewTravelRuleMetadata( 22 | "off chain reference id", 23 | address, 24 | 1000) 25 | assert.Equal(t, "020001166f666620636861696e207265666572656e6365206964", hex.EncodeToString(metadata)) 26 | assert.Equal(t, "020001166f666620636861696e207265666572656e6365206964f72589b71ff4f8d139674a3f7369c69be803000000000000404024244449454d5f41545445535424244040", hex.EncodeToString(sigMsg)) 27 | } 28 | 29 | func TestNewGeneralMetadataToSubAddress(t *testing.T) { 30 | subAddress, _ := diemtypes.MakeSubAddress("8f8b82153010a1bd") 31 | ret := txnmetadata.NewGeneralMetadataToSubAddress(subAddress) 32 | assert.Equal(t, "010001088f8b82153010a1bd0000", hex.EncodeToString(ret)) 33 | } 34 | 35 | func TestNewGeneralMetadataFromSubAddress(t *testing.T) { 36 | subAddress, _ := diemtypes.MakeSubAddress("8f8b82153010a1bd") 37 | ret := txnmetadata.NewGeneralMetadataFromSubAddress(subAddress) 38 | assert.Equal(t, "01000001088f8b82153010a1bd00", hex.EncodeToString(ret)) 39 | } 40 | 41 | func TestNewGeneralMetadataWithFromToSubaddresses(t *testing.T) { 42 | subAddress1, _ := diemtypes.MakeSubAddress("8f8b82153010a1bd") 43 | subAddress2, _ := diemtypes.MakeSubAddress("111111153010a111") 44 | ret := txnmetadata.NewGeneralMetadataWithFromToSubAddresses(subAddress1, subAddress2) 45 | assert.Equal(t, "01000108111111153010a11101088f8b82153010a1bd00", hex.EncodeToString(ret)) 46 | } 47 | 48 | func TestFindRefundReferenceEventFromTransaction(t *testing.T) { 49 | receiver := diemkeys.MustGenKeys() 50 | 51 | t.Run("return nil for given transaction is nil", func(t *testing.T) { 52 | ret := txnmetadata.FindRefundReferenceEventFromTransaction(nil, receiver.AccountAddress()) 53 | assert.Nil(t, ret) 54 | }) 55 | t.Run("return event that is receivedpayment type and receiver account address", func(t *testing.T) { 56 | txn := diemclienttest.TransactionBuilder{}.Events( 57 | diemclienttest.EventBuilder{}. 58 | Type("unknowntype"). 59 | Receiver(receiver.AccountAddress().Hex()), 60 | diemclienttest.EventBuilder{}. 61 | Type("receivedpayment"). 62 | Receiver("unknwon address"), 63 | diemclienttest.EventBuilder{}. 64 | Type("receivedpayment"). 65 | Receiver(receiver.AccountAddress().Hex()), 66 | ).Build() 67 | ret := txnmetadata.FindRefundReferenceEventFromTransaction(txn, receiver.AccountAddress()) 68 | require.NotNil(t, ret) 69 | assert.Equal(t, "receivedpayment", ret.Data.Type) 70 | assert.Equal(t, receiver.AccountAddress().Hex(), ret.Data.Receiver) 71 | }) 72 | t.Run("return nil event if not found", func(t *testing.T) { 73 | txn := diemclienttest.TransactionBuilder{}.Events( 74 | diemclienttest.EventBuilder{}. 75 | Type("unknowntype"). 76 | Receiver(receiver.AccountAddress().Hex()), 77 | diemclienttest.EventBuilder{}. 78 | Type("receivedpayment"). 79 | Receiver("unknwon address"), 80 | ).Build() 81 | ret := txnmetadata.FindRefundReferenceEventFromTransaction(txn, receiver.AccountAddress()) 82 | require.Nil(t, ret) 83 | }) 84 | } 85 | 86 | func TestRefundMetadata(t *testing.T) { 87 | txnVersion := 12343 88 | reason := new(diemtypes.RefundReason__UserInitiatedFullRefund) 89 | ret := txnmetadata.NewRefundMetadata(uint64(txnVersion), reason) 90 | assert.Equal(t, hex.EncodeToString(ret), "0400373000000000000003") 91 | 92 | metadata, err := diemtypes.BcsDeserializeMetadata(ret) 93 | assert.NoError(t, err) 94 | md := metadata.(*diemtypes.Metadata__RefundMetadata).Value.(*diemtypes.RefundMetadata__RefundMetadataV0).Value 95 | assert.Equal(t, md.TransactionVersion, uint64(txnVersion)) 96 | assert.Equal(t, md.Reason, reason) 97 | } 98 | 99 | func TestCoinTradeMetadata(t *testing.T) { 100 | tradeIds := []string { 101 | "abc", 102 | "efg", 103 | } 104 | ret := txnmetadata.NewCoinTradeMetadata(tradeIds) 105 | assert.Equal(t, hex.EncodeToString(ret), "0500020361626303656667") 106 | 107 | metadata, err := diemtypes.BcsDeserializeMetadata(ret) 108 | assert.NoError(t, err) 109 | md := metadata.(*diemtypes.Metadata__CoinTradeMetadata).Value.(*diemtypes.CoinTradeMetadata__CoinTradeMetadataV0).Value 110 | assert.Equal(t, md.TradeIds, tradeIds) 111 | } 112 | 113 | func TestNewRefundMetadataFromEvent(t *testing.T) { 114 | referencedEventSeqNum := uint64(123) 115 | 116 | cases := []struct { 117 | name string 118 | event *diemclienttest.EventBuilder 119 | expectedErrorMsg string 120 | expected *diemtypes.Metadata__GeneralMetadata 121 | }{ 122 | { 123 | name: "return event metadata with referenced event: include both from & to subaddress", 124 | event: diemclienttest.EventBuilder{}. 125 | Metadata( 126 | hex.EncodeToString(txnmetadata.NewGeneralMetadataWithFromToSubAddresses( 127 | diemtypes.SubAddress{1, 2, 3, 4, 5, 6, 7, 8}, 128 | diemtypes.SubAddress{8, 7, 6, 5, 4, 3, 2, 1}, 129 | ))). 130 | SequenceNumber(referencedEventSeqNum), 131 | expected: &diemtypes.Metadata__GeneralMetadata{ 132 | Value: &diemtypes.GeneralMetadata__GeneralMetadataVersion0{ 133 | Value: diemtypes.GeneralMetadataV0{ 134 | FromSubaddress: &[]byte{8, 7, 6, 5, 4, 3, 2, 1}, 135 | ToSubaddress: &[]byte{1, 2, 3, 4, 5, 6, 7, 8}, 136 | ReferencedEvent: &referencedEventSeqNum, 137 | }, 138 | }, 139 | }, 140 | }, 141 | { 142 | name: "return event metadata with referenced event: only has to subaddress", 143 | event: diemclienttest.EventBuilder{}. 144 | Metadata( 145 | hex.EncodeToString(txnmetadata.NewGeneralMetadataFromSubAddress( 146 | diemtypes.SubAddress{1, 2, 3, 4, 5, 6, 7, 8}, 147 | ))). 148 | SequenceNumber(referencedEventSeqNum), 149 | expected: &diemtypes.Metadata__GeneralMetadata{ 150 | Value: &diemtypes.GeneralMetadata__GeneralMetadataVersion0{ 151 | Value: diemtypes.GeneralMetadataV0{ 152 | ToSubaddress: &[]byte{1, 2, 3, 4, 5, 6, 7, 8}, 153 | ReferencedEvent: &referencedEventSeqNum, 154 | }, 155 | }, 156 | }, 157 | }, 158 | { 159 | name: "return event metadata with referenced event: only has from subaddress", 160 | event: diemclienttest.EventBuilder{}. 161 | Metadata( 162 | hex.EncodeToString(txnmetadata.NewGeneralMetadataToSubAddress( 163 | diemtypes.SubAddress{1, 2, 3, 4, 5, 6, 7, 8}, 164 | ))). 165 | SequenceNumber(referencedEventSeqNum), 166 | expected: &diemtypes.Metadata__GeneralMetadata{ 167 | Value: &diemtypes.GeneralMetadata__GeneralMetadataVersion0{ 168 | Value: diemtypes.GeneralMetadataV0{ 169 | FromSubaddress: &[]byte{1, 2, 3, 4, 5, 6, 7, 8}, 170 | ReferencedEvent: &referencedEventSeqNum, 171 | }, 172 | }, 173 | }, 174 | }, 175 | { 176 | name: "event is nil", 177 | event: nil, 178 | expectedErrorMsg: "must provide refund reference event", 179 | }, 180 | { 181 | name: "event metadata is not hex-encoded string", 182 | event: diemclienttest.EventBuilder{}.Metadata("lj;lafda"), 183 | expectedErrorMsg: "decode event metadata failed: encoding/hex: invalid byte: U+006C 'l'", 184 | }, 185 | { 186 | name: "event metadata is not valid BCS bytes", 187 | event: diemclienttest.EventBuilder{}.Metadata("1112233333"), 188 | expectedErrorMsg: "can't deserialize metadata: Unknown variant index for Metadata: 17", 189 | }, 190 | { 191 | name: "return nil without error if event metadata is empty", 192 | event: diemclienttest.EventBuilder{}. 193 | Metadata(""). 194 | SequenceNumber(referencedEventSeqNum), 195 | expected: nil, 196 | }, 197 | } 198 | for _, tc := range cases { 199 | t.Run(tc.name, func(t *testing.T) { 200 | var event *diemclient.Event 201 | if tc.event != nil { 202 | event = tc.event.Build() 203 | } 204 | ret, err := newRefundMetadataFromEvent(event) 205 | if tc.expectedErrorMsg != "" { 206 | assert.EqualError(t, err, tc.expectedErrorMsg) 207 | } else if tc.expected != nil { 208 | require.NoError(t, err) 209 | ret, err := diemtypes.BcsDeserializeMetadata(ret) 210 | require.NoError(t, err) 211 | assert.EqualValues(t, tc.expected, ret) 212 | } else { 213 | assert.Nil(t, ret) 214 | assert.Nil(t, err) 215 | } 216 | }) 217 | } 218 | } 219 | 220 | func newRefundMetadataFromEvent(event *diemclient.Event) ([]byte, error) { 221 | md, err := txnmetadata.DeserializeMetadata(event) 222 | if err != nil { 223 | return nil, err 224 | } 225 | if md == nil { 226 | return nil, nil 227 | } 228 | return txnmetadata.NewRefundMetadataFromEventMetadata(event.SequenceNumber, 229 | md.(*diemtypes.Metadata__GeneralMetadata)) 230 | } 231 | --------------------------------------------------------------------------------