├── .github └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── README.md ├── certs └── trade.valorem.xyz.pem ├── docker-compose.yml ├── examples ├── abi │ ├── IERC1155.json │ ├── IERC20.json │ ├── ISeaport.json │ ├── ISeaportConduitController.json │ ├── ISeaportDomainRegistry.json │ ├── ISeaportOneOneValidator.json │ └── IValoremOptionsClearinghouse.json ├── rust │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── examples │ │ ├── maker │ │ │ ├── README.md │ │ │ ├── maker.rs │ │ │ ├── rfq_request.rs │ │ │ ├── seaport_helper.rs │ │ │ ├── settings.rs │ │ │ ├── settings.yaml.template │ │ │ ├── soft_quote_request.rs │ │ │ └── token_approvals.rs │ │ └── taker │ │ │ ├── README.md │ │ │ ├── seaport_helper.rs │ │ │ ├── settings.rs │ │ │ ├── settings.yaml.template │ │ │ ├── taker.rs │ │ │ └── token_approvals.rs │ └── src │ │ ├── bindings │ │ ├── erc1155.rs │ │ ├── erc20.rs │ │ ├── mod.rs │ │ ├── seaport.rs │ │ ├── seaport_counduit_controller.rs │ │ ├── seaport_domain_registry.rs │ │ ├── seaport_validator.rs │ │ └── valorem_clear.rs │ │ ├── grpc_adapters.rs │ │ ├── lib.rs │ │ └── utils │ │ ├── mod.rs │ │ └── session_interceptor.rs └── typescript │ ├── .prettierrc.json │ ├── package.json │ ├── src │ ├── RFQ_maker.ts │ └── RFQ_taker.ts │ └── tsconfig.json ├── img └── valorem-trade-api-banner.png └── proto ├── google └── rpc │ ├── error_details.proto │ └── status.proto ├── grpc ├── health │ └── v1 │ │ └── health.proto └── reflection │ └── v1alpha │ └── reflection.proto └── valorem └── trade └── v1 ├── auth.proto ├── fees.proto ├── rfq.proto ├── seaport.proto ├── soft_quote.proto ├── spot.proto └── types.proto /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: 'Parallel Test Suite' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'v**' 8 | - 'releases/v**' 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | branches: 12 | - main 13 | - 'v**' 14 | - 'releases/v**' 15 | 16 | jobs: 17 | examples: 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@master 22 | with: 23 | submodules: true 24 | 25 | # Start docker stack here to allow migrations time to run before the end-to-end tests start 26 | - name: Log in to GHCR 27 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin 28 | 29 | # Start these here so migrations have time to run while we check formatting and clippy 30 | - name: Init docker dev stack (anvil, redis, db, trade API, indexer) 31 | run: | 32 | docker-compose -f docker-compose.yml up -d 33 | 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | toolchain: stable 38 | override: true 39 | profile: minimal 40 | components: clippy, rustfmt 41 | 42 | # Check formatting, clippy, and run examples test 43 | - name: Check Formatting 44 | run: cargo fmt --all -- --check 45 | working-directory: examples/rust 46 | 47 | - name: Build and Test 48 | run: cargo test 49 | working-directory: examples/rust 50 | env: 51 | APP_RPC__URI: http://127.0.0.1:8545 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Compiled output 9 | dist/ 10 | build/ 11 | examples/typescript/gen/ 12 | 13 | # Dependency directories 14 | node_modules/ 15 | 16 | # TypeScript-specific 17 | *.tsbuildinfo 18 | 19 | # Editor-specific 20 | .vscode/ 21 | .idea/ 22 | *.swp 23 | *.swo 24 | 25 | # Miscellaneous 26 | .DS_Store 27 | .env 28 | 29 | # lockfile 30 | examples/typescript/pnpm-lock.yaml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Valorem Labs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /certs/trade.valorem.xyz.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 3 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 4 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 5 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3 6 | MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 7 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE 8 | CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD 9 | EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi 10 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD 11 | BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv 12 | K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e 13 | cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY 14 | pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n 15 | eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB 16 | AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV 17 | HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv 18 | 9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v 19 | b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n 20 | b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG 21 | CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv 22 | MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz 23 | 91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2 24 | RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi 25 | DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11 26 | GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x 27 | LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | postgres: 4 | image: ghcr.io/valorem-labs-inc/customgres:latest 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: password 8 | POSTGRES_DB: trade 9 | ports: 10 | - 5432:5432 11 | migrator: 12 | image: ghcr.io/valorem-labs-inc/migrator:latest 13 | environment: 14 | DATABASE_URL: postgres://postgres:password@postgres:5432/trade 15 | command: '/scripts/run_migrations.sh' 16 | depends_on: 17 | - postgres 18 | redis: 19 | image: redis:7 20 | ports: 21 | - 6379:6379 22 | anvil: 23 | image: ghcr.io/valorem-labs-inc/anvil:latest 24 | ports: 25 | - 8545:8545 26 | tradeapi: 27 | image: ghcr.io/valorem-labs-inc/trade-web-api:latest 28 | environment: 29 | APP_ENVIRONMENT: local 30 | ports: 31 | - 8080:8080 32 | depends_on: 33 | - postgres 34 | - redis 35 | - anvil 36 | indexer: 37 | image: ghcr.io/valorem-labs-inc/indexer:latest 38 | depends_on: 39 | - postgres 40 | - redis 41 | - anvil 42 | -------------------------------------------------------------------------------- /examples/abi/IERC1155.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "uri_", 7 | "type": "string" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "account", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "operator", 26 | "type": "address" 27 | }, 28 | { 29 | "indexed": false, 30 | "internalType": "bool", 31 | "name": "approved", 32 | "type": "bool" 33 | } 34 | ], 35 | "name": "ApprovalForAll", 36 | "type": "event" 37 | }, 38 | { 39 | "anonymous": false, 40 | "inputs": [ 41 | { 42 | "indexed": true, 43 | "internalType": "address", 44 | "name": "operator", 45 | "type": "address" 46 | }, 47 | { 48 | "indexed": true, 49 | "internalType": "address", 50 | "name": "from", 51 | "type": "address" 52 | }, 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "to", 57 | "type": "address" 58 | }, 59 | { 60 | "indexed": false, 61 | "internalType": "uint256[]", 62 | "name": "ids", 63 | "type": "uint256[]" 64 | }, 65 | { 66 | "indexed": false, 67 | "internalType": "uint256[]", 68 | "name": "values", 69 | "type": "uint256[]" 70 | } 71 | ], 72 | "name": "TransferBatch", 73 | "type": "event" 74 | }, 75 | { 76 | "anonymous": false, 77 | "inputs": [ 78 | { 79 | "indexed": true, 80 | "internalType": "address", 81 | "name": "operator", 82 | "type": "address" 83 | }, 84 | { 85 | "indexed": true, 86 | "internalType": "address", 87 | "name": "from", 88 | "type": "address" 89 | }, 90 | { 91 | "indexed": true, 92 | "internalType": "address", 93 | "name": "to", 94 | "type": "address" 95 | }, 96 | { 97 | "indexed": false, 98 | "internalType": "uint256", 99 | "name": "id", 100 | "type": "uint256" 101 | }, 102 | { 103 | "indexed": false, 104 | "internalType": "uint256", 105 | "name": "value", 106 | "type": "uint256" 107 | } 108 | ], 109 | "name": "TransferSingle", 110 | "type": "event" 111 | }, 112 | { 113 | "anonymous": false, 114 | "inputs": [ 115 | { 116 | "indexed": false, 117 | "internalType": "string", 118 | "name": "value", 119 | "type": "string" 120 | }, 121 | { 122 | "indexed": true, 123 | "internalType": "uint256", 124 | "name": "id", 125 | "type": "uint256" 126 | } 127 | ], 128 | "name": "URI", 129 | "type": "event" 130 | }, 131 | { 132 | "inputs": [ 133 | { 134 | "internalType": "address", 135 | "name": "account", 136 | "type": "address" 137 | }, 138 | { 139 | "internalType": "uint256", 140 | "name": "id", 141 | "type": "uint256" 142 | } 143 | ], 144 | "name": "balanceOf", 145 | "outputs": [ 146 | { 147 | "internalType": "uint256", 148 | "name": "", 149 | "type": "uint256" 150 | } 151 | ], 152 | "stateMutability": "view", 153 | "type": "function" 154 | }, 155 | { 156 | "inputs": [ 157 | { 158 | "internalType": "address[]", 159 | "name": "accounts", 160 | "type": "address[]" 161 | }, 162 | { 163 | "internalType": "uint256[]", 164 | "name": "ids", 165 | "type": "uint256[]" 166 | } 167 | ], 168 | "name": "balanceOfBatch", 169 | "outputs": [ 170 | { 171 | "internalType": "uint256[]", 172 | "name": "", 173 | "type": "uint256[]" 174 | } 175 | ], 176 | "stateMutability": "view", 177 | "type": "function" 178 | }, 179 | { 180 | "inputs": [ 181 | { 182 | "internalType": "address", 183 | "name": "account", 184 | "type": "address" 185 | }, 186 | { 187 | "internalType": "address", 188 | "name": "operator", 189 | "type": "address" 190 | } 191 | ], 192 | "name": "isApprovedForAll", 193 | "outputs": [ 194 | { 195 | "internalType": "bool", 196 | "name": "", 197 | "type": "bool" 198 | } 199 | ], 200 | "stateMutability": "view", 201 | "type": "function" 202 | }, 203 | { 204 | "inputs": [ 205 | { 206 | "internalType": "address", 207 | "name": "from", 208 | "type": "address" 209 | }, 210 | { 211 | "internalType": "address", 212 | "name": "to", 213 | "type": "address" 214 | }, 215 | { 216 | "internalType": "uint256[]", 217 | "name": "ids", 218 | "type": "uint256[]" 219 | }, 220 | { 221 | "internalType": "uint256[]", 222 | "name": "amounts", 223 | "type": "uint256[]" 224 | }, 225 | { 226 | "internalType": "bytes", 227 | "name": "data", 228 | "type": "bytes" 229 | } 230 | ], 231 | "name": "safeBatchTransferFrom", 232 | "outputs": [], 233 | "stateMutability": "nonpayable", 234 | "type": "function" 235 | }, 236 | { 237 | "inputs": [ 238 | { 239 | "internalType": "address", 240 | "name": "from", 241 | "type": "address" 242 | }, 243 | { 244 | "internalType": "address", 245 | "name": "to", 246 | "type": "address" 247 | }, 248 | { 249 | "internalType": "uint256", 250 | "name": "id", 251 | "type": "uint256" 252 | }, 253 | { 254 | "internalType": "uint256", 255 | "name": "amount", 256 | "type": "uint256" 257 | }, 258 | { 259 | "internalType": "bytes", 260 | "name": "data", 261 | "type": "bytes" 262 | } 263 | ], 264 | "name": "safeTransferFrom", 265 | "outputs": [], 266 | "stateMutability": "nonpayable", 267 | "type": "function" 268 | }, 269 | { 270 | "inputs": [ 271 | { 272 | "internalType": "address", 273 | "name": "operator", 274 | "type": "address" 275 | }, 276 | { 277 | "internalType": "bool", 278 | "name": "approved", 279 | "type": "bool" 280 | } 281 | ], 282 | "name": "setApprovalForAll", 283 | "outputs": [], 284 | "stateMutability": "nonpayable", 285 | "type": "function" 286 | }, 287 | { 288 | "inputs": [ 289 | { 290 | "internalType": "bytes4", 291 | "name": "interfaceId", 292 | "type": "bytes4" 293 | } 294 | ], 295 | "name": "supportsInterface", 296 | "outputs": [ 297 | { 298 | "internalType": "bool", 299 | "name": "", 300 | "type": "bool" 301 | } 302 | ], 303 | "stateMutability": "view", 304 | "type": "function" 305 | }, 306 | { 307 | "inputs": [ 308 | { 309 | "internalType": "uint256", 310 | "name": "", 311 | "type": "uint256" 312 | } 313 | ], 314 | "name": "uri", 315 | "outputs": [ 316 | { 317 | "internalType": "string", 318 | "name": "", 319 | "type": "string" 320 | } 321 | ], 322 | "stateMutability": "view", 323 | "type": "function" 324 | } 325 | ] 326 | -------------------------------------------------------------------------------- /examples/abi/IERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /examples/abi/ISeaportConduitController.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "inputs": [ 9 | { 10 | "internalType": "address", 11 | "name": "conduit", 12 | "type": "address" 13 | } 14 | ], 15 | "name": "CallerIsNotNewPotentialOwner", 16 | "type": "error" 17 | }, 18 | { 19 | "inputs": [ 20 | { 21 | "internalType": "address", 22 | "name": "conduit", 23 | "type": "address" 24 | } 25 | ], 26 | "name": "CallerIsNotOwner", 27 | "type": "error" 28 | }, 29 | { 30 | "inputs": [ 31 | { 32 | "internalType": "address", 33 | "name": "conduit", 34 | "type": "address" 35 | } 36 | ], 37 | "name": "ChannelOutOfRange", 38 | "type": "error" 39 | }, 40 | { 41 | "inputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "conduit", 45 | "type": "address" 46 | } 47 | ], 48 | "name": "ConduitAlreadyExists", 49 | "type": "error" 50 | }, 51 | { 52 | "inputs": [], 53 | "name": "InvalidCreator", 54 | "type": "error" 55 | }, 56 | { 57 | "inputs": [], 58 | "name": "InvalidInitialOwner", 59 | "type": "error" 60 | }, 61 | { 62 | "inputs": [ 63 | { 64 | "internalType": "address", 65 | "name": "conduit", 66 | "type": "address" 67 | }, 68 | { 69 | "internalType": "address", 70 | "name": "newPotentialOwner", 71 | "type": "address" 72 | } 73 | ], 74 | "name": "NewPotentialOwnerAlreadySet", 75 | "type": "error" 76 | }, 77 | { 78 | "inputs": [ 79 | { 80 | "internalType": "address", 81 | "name": "conduit", 82 | "type": "address" 83 | } 84 | ], 85 | "name": "NewPotentialOwnerIsZeroAddress", 86 | "type": "error" 87 | }, 88 | { 89 | "inputs": [], 90 | "name": "NoConduit", 91 | "type": "error" 92 | }, 93 | { 94 | "inputs": [ 95 | { 96 | "internalType": "address", 97 | "name": "conduit", 98 | "type": "address" 99 | } 100 | ], 101 | "name": "NoPotentialOwnerCurrentlySet", 102 | "type": "error" 103 | }, 104 | { 105 | "anonymous": false, 106 | "inputs": [ 107 | { 108 | "indexed": false, 109 | "internalType": "address", 110 | "name": "conduit", 111 | "type": "address" 112 | }, 113 | { 114 | "indexed": false, 115 | "internalType": "bytes32", 116 | "name": "conduitKey", 117 | "type": "bytes32" 118 | } 119 | ], 120 | "name": "NewConduit", 121 | "type": "event" 122 | }, 123 | { 124 | "anonymous": false, 125 | "inputs": [ 126 | { 127 | "indexed": true, 128 | "internalType": "address", 129 | "name": "conduit", 130 | "type": "address" 131 | }, 132 | { 133 | "indexed": true, 134 | "internalType": "address", 135 | "name": "previousOwner", 136 | "type": "address" 137 | }, 138 | { 139 | "indexed": true, 140 | "internalType": "address", 141 | "name": "newOwner", 142 | "type": "address" 143 | } 144 | ], 145 | "name": "OwnershipTransferred", 146 | "type": "event" 147 | }, 148 | { 149 | "anonymous": false, 150 | "inputs": [ 151 | { 152 | "indexed": true, 153 | "internalType": "address", 154 | "name": "newPotentialOwner", 155 | "type": "address" 156 | } 157 | ], 158 | "name": "PotentialOwnerUpdated", 159 | "type": "event" 160 | }, 161 | { 162 | "inputs": [ 163 | { 164 | "internalType": "address", 165 | "name": "conduit", 166 | "type": "address" 167 | } 168 | ], 169 | "name": "acceptOwnership", 170 | "outputs": [], 171 | "stateMutability": "nonpayable", 172 | "type": "function" 173 | }, 174 | { 175 | "inputs": [ 176 | { 177 | "internalType": "address", 178 | "name": "conduit", 179 | "type": "address" 180 | } 181 | ], 182 | "name": "cancelOwnershipTransfer", 183 | "outputs": [], 184 | "stateMutability": "nonpayable", 185 | "type": "function" 186 | }, 187 | { 188 | "inputs": [ 189 | { 190 | "internalType": "bytes32", 191 | "name": "conduitKey", 192 | "type": "bytes32" 193 | }, 194 | { 195 | "internalType": "address", 196 | "name": "initialOwner", 197 | "type": "address" 198 | } 199 | ], 200 | "name": "createConduit", 201 | "outputs": [ 202 | { 203 | "internalType": "address", 204 | "name": "conduit", 205 | "type": "address" 206 | } 207 | ], 208 | "stateMutability": "nonpayable", 209 | "type": "function" 210 | }, 211 | { 212 | "inputs": [ 213 | { 214 | "internalType": "address", 215 | "name": "conduit", 216 | "type": "address" 217 | }, 218 | { 219 | "internalType": "uint256", 220 | "name": "channelIndex", 221 | "type": "uint256" 222 | } 223 | ], 224 | "name": "getChannel", 225 | "outputs": [ 226 | { 227 | "internalType": "address", 228 | "name": "channel", 229 | "type": "address" 230 | } 231 | ], 232 | "stateMutability": "view", 233 | "type": "function" 234 | }, 235 | { 236 | "inputs": [ 237 | { 238 | "internalType": "address", 239 | "name": "conduit", 240 | "type": "address" 241 | }, 242 | { 243 | "internalType": "address", 244 | "name": "channel", 245 | "type": "address" 246 | } 247 | ], 248 | "name": "getChannelStatus", 249 | "outputs": [ 250 | { 251 | "internalType": "bool", 252 | "name": "isOpen", 253 | "type": "bool" 254 | } 255 | ], 256 | "stateMutability": "view", 257 | "type": "function" 258 | }, 259 | { 260 | "inputs": [ 261 | { 262 | "internalType": "address", 263 | "name": "conduit", 264 | "type": "address" 265 | } 266 | ], 267 | "name": "getChannels", 268 | "outputs": [ 269 | { 270 | "internalType": "address[]", 271 | "name": "channels", 272 | "type": "address[]" 273 | } 274 | ], 275 | "stateMutability": "view", 276 | "type": "function" 277 | }, 278 | { 279 | "inputs": [ 280 | { 281 | "internalType": "bytes32", 282 | "name": "conduitKey", 283 | "type": "bytes32" 284 | } 285 | ], 286 | "name": "getConduit", 287 | "outputs": [ 288 | { 289 | "internalType": "address", 290 | "name": "conduit", 291 | "type": "address" 292 | }, 293 | { 294 | "internalType": "bool", 295 | "name": "exists", 296 | "type": "bool" 297 | } 298 | ], 299 | "stateMutability": "view", 300 | "type": "function" 301 | }, 302 | { 303 | "inputs": [], 304 | "name": "getConduitCodeHashes", 305 | "outputs": [ 306 | { 307 | "internalType": "bytes32", 308 | "name": "creationCodeHash", 309 | "type": "bytes32" 310 | }, 311 | { 312 | "internalType": "bytes32", 313 | "name": "runtimeCodeHash", 314 | "type": "bytes32" 315 | } 316 | ], 317 | "stateMutability": "view", 318 | "type": "function" 319 | }, 320 | { 321 | "inputs": [ 322 | { 323 | "internalType": "address", 324 | "name": "conduit", 325 | "type": "address" 326 | } 327 | ], 328 | "name": "getKey", 329 | "outputs": [ 330 | { 331 | "internalType": "bytes32", 332 | "name": "conduitKey", 333 | "type": "bytes32" 334 | } 335 | ], 336 | "stateMutability": "view", 337 | "type": "function" 338 | }, 339 | { 340 | "inputs": [ 341 | { 342 | "internalType": "address", 343 | "name": "conduit", 344 | "type": "address" 345 | } 346 | ], 347 | "name": "getPotentialOwner", 348 | "outputs": [ 349 | { 350 | "internalType": "address", 351 | "name": "potentialOwner", 352 | "type": "address" 353 | } 354 | ], 355 | "stateMutability": "view", 356 | "type": "function" 357 | }, 358 | { 359 | "inputs": [ 360 | { 361 | "internalType": "address", 362 | "name": "conduit", 363 | "type": "address" 364 | } 365 | ], 366 | "name": "getTotalChannels", 367 | "outputs": [ 368 | { 369 | "internalType": "uint256", 370 | "name": "totalChannels", 371 | "type": "uint256" 372 | } 373 | ], 374 | "stateMutability": "view", 375 | "type": "function" 376 | }, 377 | { 378 | "inputs": [ 379 | { 380 | "internalType": "address", 381 | "name": "conduit", 382 | "type": "address" 383 | } 384 | ], 385 | "name": "ownerOf", 386 | "outputs": [ 387 | { 388 | "internalType": "address", 389 | "name": "owner", 390 | "type": "address" 391 | } 392 | ], 393 | "stateMutability": "view", 394 | "type": "function" 395 | }, 396 | { 397 | "inputs": [ 398 | { 399 | "internalType": "address", 400 | "name": "conduit", 401 | "type": "address" 402 | }, 403 | { 404 | "internalType": "address", 405 | "name": "newPotentialOwner", 406 | "type": "address" 407 | } 408 | ], 409 | "name": "transferOwnership", 410 | "outputs": [], 411 | "stateMutability": "nonpayable", 412 | "type": "function" 413 | }, 414 | { 415 | "inputs": [ 416 | { 417 | "internalType": "address", 418 | "name": "conduit", 419 | "type": "address" 420 | }, 421 | { 422 | "internalType": "address", 423 | "name": "channel", 424 | "type": "address" 425 | }, 426 | { 427 | "internalType": "bool", 428 | "name": "isOpen", 429 | "type": "bool" 430 | } 431 | ], 432 | "name": "updateChannel", 433 | "outputs": [], 434 | "stateMutability": "nonpayable", 435 | "type": "function" 436 | } 437 | ] -------------------------------------------------------------------------------- /examples/abi/ISeaportDomainRegistry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "string", 6 | "name": "domain", 7 | "type": "string" 8 | } 9 | ], 10 | "name": "DomainAlreadyRegistered", 11 | "type": "error" 12 | }, 13 | { 14 | "inputs": [ 15 | { 16 | "internalType": "bytes4", 17 | "name": "tag", 18 | "type": "bytes4" 19 | }, 20 | { 21 | "internalType": "uint256", 22 | "name": "maxIndex", 23 | "type": "uint256" 24 | }, 25 | { 26 | "internalType": "uint256", 27 | "name": "suppliedIndex", 28 | "type": "uint256" 29 | } 30 | ], 31 | "name": "DomainIndexOutOfRange", 32 | "type": "error" 33 | }, 34 | { 35 | "anonymous": false, 36 | "inputs": [ 37 | { 38 | "indexed": false, 39 | "internalType": "string", 40 | "name": "domain", 41 | "type": "string" 42 | }, 43 | { 44 | "indexed": false, 45 | "internalType": "bytes4", 46 | "name": "tag", 47 | "type": "bytes4" 48 | }, 49 | { 50 | "indexed": false, 51 | "internalType": "uint256", 52 | "name": "index", 53 | "type": "uint256" 54 | } 55 | ], 56 | "name": "DomainRegistered", 57 | "type": "event" 58 | }, 59 | { 60 | "inputs": [ 61 | { 62 | "internalType": "bytes4", 63 | "name": "tag", 64 | "type": "bytes4" 65 | }, 66 | { 67 | "internalType": "uint256", 68 | "name": "index", 69 | "type": "uint256" 70 | } 71 | ], 72 | "name": "getDomain", 73 | "outputs": [ 74 | { 75 | "internalType": "string", 76 | "name": "domain", 77 | "type": "string" 78 | } 79 | ], 80 | "stateMutability": "view", 81 | "type": "function" 82 | }, 83 | { 84 | "inputs": [ 85 | { 86 | "internalType": "bytes4", 87 | "name": "tag", 88 | "type": "bytes4" 89 | } 90 | ], 91 | "name": "getDomains", 92 | "outputs": [ 93 | { 94 | "internalType": "string[]", 95 | "name": "domains", 96 | "type": "string[]" 97 | } 98 | ], 99 | "stateMutability": "view", 100 | "type": "function" 101 | }, 102 | { 103 | "inputs": [ 104 | { 105 | "internalType": "bytes4", 106 | "name": "tag", 107 | "type": "bytes4" 108 | } 109 | ], 110 | "name": "getNumberOfDomains", 111 | "outputs": [ 112 | { 113 | "internalType": "uint256", 114 | "name": "totalDomains", 115 | "type": "uint256" 116 | } 117 | ], 118 | "stateMutability": "view", 119 | "type": "function" 120 | }, 121 | { 122 | "inputs": [ 123 | { 124 | "internalType": "string", 125 | "name": "domain", 126 | "type": "string" 127 | } 128 | ], 129 | "name": "setDomain", 130 | "outputs": [ 131 | { 132 | "internalType": "bytes4", 133 | "name": "tag", 134 | "type": "bytes4" 135 | } 136 | ], 137 | "stateMutability": "nonpayable", 138 | "type": "function" 139 | } 140 | ] -------------------------------------------------------------------------------- /examples/rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .env 4 | .vscode 5 | 6 | broadcast/ 7 | cache/ 8 | out/ 9 | 10 | # Local settings file - if stored under the repository 11 | settings.yaml 12 | 13 | # Scripts lockfile 14 | .lock 15 | 16 | # lcov 17 | lcov.info 18 | -------------------------------------------------------------------------------- /examples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "valorem-trade-interfaces" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | arrayref = "0.3.7" 8 | prost = "0.11.8" 9 | serde_json = "1.0.95" 10 | 11 | [dependencies.ethers] 12 | version = "2.0.0" 13 | features = ["abigen", "ws", "rustls", "ipc"] 14 | optional = false 15 | 16 | [dependencies.serde] 17 | version = "1.0.160" 18 | features = ["derive"] 19 | optional = false 20 | 21 | [dependencies.tonic] 22 | version = "0.9.1" 23 | features = ["tls"] 24 | optional = false 25 | 26 | [dev-dependencies] 27 | config = "*" 28 | http = "*" 29 | log = "*" 30 | pretty_env_logger = "*" 31 | rpassword = "*" 32 | time = "*" 33 | tokio-stream = "*" 34 | 35 | [dev-dependencies.siwe] 36 | version = "*" 37 | features = ["serde"] 38 | optional = false 39 | 40 | [dev-dependencies.tokio] 41 | version = "*" 42 | features = ["macros", "rt-multi-thread"] 43 | optional = false 44 | 45 | [build-dependencies] 46 | tonic-build = "0.9.1" 47 | prost = "0.12.1" 48 | protox = "0.5.0" 49 | 50 | [lib] 51 | path = "src/lib.rs" 52 | plugin = false 53 | proc-macro = false 54 | required-features = [] 55 | 56 | [[example]] 57 | path = "examples/maker/maker.rs" 58 | name = "maker" 59 | plugin = false 60 | proc-macro = false 61 | 62 | [[example]] 63 | path = "examples/taker/taker.rs" 64 | name = "taker" 65 | plugin = false 66 | proc-macro = false 67 | -------------------------------------------------------------------------------- /examples/rust/build.rs: -------------------------------------------------------------------------------- 1 | use prost::Message; 2 | use std::path::PathBuf; 3 | use std::{env, fs}; 4 | 5 | // generated by `sqlx migrate build-script` 6 | fn main() -> Result<(), Box> { 7 | // Compile the proto files with ProtoX 8 | let file_descriptors = protox::compile( 9 | [ 10 | "../../proto/valorem/trade/v1/rfq.proto", 11 | "../../proto/valorem/trade/v1/auth.proto", 12 | "../../proto/valorem/trade/v1/soft_quote.proto", 13 | ], 14 | ["../../proto/valorem/trade/v1/"], 15 | ) 16 | .unwrap(); 17 | 18 | // Get the path to the output directory for the file descriptor set 19 | let file_descriptor_path = 20 | PathBuf::from(env::var("OUT_DIR").unwrap()).join("valorem_interfaces.bin"); 21 | 22 | // Write the file descriptor set to the output directory 23 | fs::write(&file_descriptor_path, file_descriptors.encode_to_vec()).unwrap(); 24 | 25 | // Tonic codegen. Skip the protoc run here since we compiled the proto files with ProtoX earlier 26 | dbg!(tonic_build::configure().build_server(true)) 27 | .file_descriptor_set_path(file_descriptor_path) 28 | .skip_protoc_run() 29 | .compile( 30 | &[ 31 | "../../proto/valorem/trade/v1/rfq.proto", 32 | "../../proto/valorem/trade/v1/auth.proto", 33 | "../../proto/valorem/trade/v1/soft_quote.proto", 34 | ], 35 | &["../../proto/valorem/trade/v1/"], // specify the root location to search proto dependencies 36 | ) 37 | .unwrap(); 38 | // trigger recompilation when a new migration is added 39 | println!("cargo:rerun-if-changed=migrations"); 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/README.md: -------------------------------------------------------------------------------- 1 | ## Running the example 2 | 3 | First copy the template config file `settings.yaml.template` and rename to `settings.yaml`, then fill in the relevant fields. 4 | 5 | The default settings is using the CA root for trade.valorem.xyz (located at the project root, `certs/trade.valorem.xyz.pem`). 6 | 7 | Then run the example like so: 8 | 9 | ```bash 10 | cargo run --example maker examples/maker/settings.yaml 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/maker.rs: -------------------------------------------------------------------------------- 1 | use crate::rfq_request::{handle_rfq_request, validate_rfq}; 2 | use crate::settings::Settings; 3 | use crate::soft_quote_request::{handle_soft_quote_request, validate_soft_quote}; 4 | use crate::token_approvals::approve_tokens; 5 | use ethers::prelude::{ 6 | Address, Http, Ipc, JsonRpcClient, LocalWallet, Middleware, Provider, Signer, SignerMiddleware, 7 | Ws, U256, 8 | }; 9 | use http::Uri; 10 | use log::{error, info, warn}; 11 | use siwe::{TimeStamp, Version}; 12 | use std::time::{SystemTime, UNIX_EPOCH}; 13 | use std::{env, process::exit, sync::Arc, time::Duration}; 14 | use time::OffsetDateTime; 15 | use tokio::select; 16 | use tokio::{sync::mpsc, time::sleep}; 17 | use tonic::transport::{Channel, ClientTlsConfig}; 18 | use valorem_trade_interfaces::utils::session_interceptor::SessionInterceptor; 19 | use valorem_trade_interfaces::{ 20 | bindings, grpc_codegen, 21 | grpc_codegen::{ 22 | auth_client::AuthClient, rfq_client::RfqClient, soft_quote_client::SoftQuoteClient, Empty, 23 | QuoteRequest, QuoteResponse, SoftQuoteResponse, VerifyText, 24 | }, 25 | }; 26 | 27 | mod rfq_request; 28 | mod seaport_helper; 29 | mod settings; 30 | mod soft_quote_request; 31 | mod token_approvals; 32 | 33 | enum EthersProvider { 34 | HttpProvider(Provider), 35 | WsProvider(Provider), 36 | IpcProvider(Provider), 37 | } 38 | 39 | const SESSION_COOKIE_KEY: &str = "set-cookie"; 40 | 41 | const TOS_ACCEPTANCE: &str = "I accept the Valorem Terms of Service at https://app.valorem.xyz/tos and Privacy Policy at https://app.valorem.xyz/privacy"; 42 | 43 | /// An example Market Maker (MM) client interface to Valorem. 44 | /// 45 | /// The Market Maker will receive Request For Quote (RFQ) from the Valorem server formatted as 46 | /// `QuoteRequest` and the MM needs to respond with `QuoteResponse`. 47 | #[tokio::main] 48 | async fn main() { 49 | // If no logging options are given, by default set global logging to warn and the maker to info. 50 | let value = env::var("RUST_LOG").unwrap_or(String::from("warn,maker=info")); 51 | env::set_var("RUST_LOG", value); 52 | 53 | // Initialise a coloured timed logger 54 | pretty_env_logger::init_timed(); 55 | 56 | let args: Vec = env::args().skip(1).collect(); 57 | if args.len() != 1 { 58 | error!("Unexpected command line arguments. Received {:?}", args); 59 | error!("Usage: maker "); 60 | exit(1); 61 | } 62 | 63 | let settings = Settings::load(&args[0]); 64 | 65 | loop { 66 | let provider = connect_to_node_provider(settings.node_endpoint.clone()).await; 67 | 68 | match provider { 69 | EthersProvider::HttpProvider(provider) => { 70 | run(Arc::new(provider), settings.clone()).await; 71 | } 72 | EthersProvider::WsProvider(provider) => { 73 | run(Arc::new(provider), settings.clone()).await; 74 | } 75 | EthersProvider::IpcProvider(provider) => { 76 | run(Arc::new(provider), settings.clone()).await; 77 | } 78 | } 79 | 80 | // We never expect the run function above to end, however it can primarily due to the connection to 81 | // Valorem being closed. Sleep for a set period of time in case there are other issues at play 82 | // (i.e. Valorem down) before retrying to connect. 83 | warn!("Reconnection Event: Returned from the main run function, reconnecting to Valorem"); 84 | sleep(Duration::from_secs(10)).await; 85 | } 86 | } 87 | 88 | // Helper function to connect to a node provider. 89 | async fn connect_to_node_provider(node_endpoint: String) -> EthersProvider { 90 | if node_endpoint.starts_with("http") { 91 | let provider = match Provider::::try_from(node_endpoint) { 92 | Ok(connection) => { 93 | // Decrease the ethers polling time from the default of 7 seconds to 1 second. 94 | connection.interval(Duration::from_secs(1)) 95 | } 96 | Err(connection_error) => { 97 | // If we cannot connect to our provider, we are in trouble. Panic out instead of 98 | // trying to implement reconnection logic as most of the time the node will be local. 99 | panic!("HTTP connection error: {connection_error:?}") 100 | } 101 | }; 102 | 103 | EthersProvider::HttpProvider(provider) 104 | } else if node_endpoint.starts_with("ws") { 105 | let provider = match Ws::connect(node_endpoint).await { 106 | Ok(connection) => { 107 | // Decrease the ethers polling time from the default of 7 seconds to 1 second. 108 | Provider::::new(connection).interval(Duration::from_secs(1)) 109 | } 110 | Err(connection_error) => { 111 | // If we cannot connect to our provider, we are in trouble. Panic out instead of 112 | // trying to implement reconnection logic as most of the time the node will be local. 113 | panic!("Websocket connection error: {connection_error:?}") 114 | } 115 | }; 116 | 117 | EthersProvider::WsProvider(provider) 118 | } else { 119 | let provider = match Provider::connect_ipc(node_endpoint).await { 120 | Ok(connection) => { 121 | // Decrease the ethers polling time from the default of 7 seconds to 1 second. 122 | connection.interval(Duration::from_secs(1)) 123 | } 124 | Err(connection_error) => { 125 | // If we cannot connect to our local provider, we are in trouble just panic out instead. 126 | panic!("IPC connection error: {connection_error:?}") 127 | } 128 | }; 129 | 130 | EthersProvider::IpcProvider(provider) 131 | } 132 | } 133 | 134 | // Main execution function. This is not expected to return. 135 | async fn run( 136 | provider: Arc>, 137 | settings: Settings, 138 | ) -> Option<()> { 139 | let session_cookie = setup( 140 | settings.valorem_endpoint.clone(), 141 | settings.wallet.clone(), 142 | settings.tls_config.clone(), 143 | &provider, 144 | ) 145 | .await?; 146 | 147 | // Now there is a valid authenticated session, connect to the RFQ stream 148 | let channel_builder = connect_to_valorem( 149 | settings.valorem_endpoint.clone(), 150 | settings.tls_config.clone(), 151 | ) 152 | .await?; 153 | 154 | let mut rfq_client = RfqClient::with_interceptor( 155 | channel_builder.clone(), 156 | SessionInterceptor { 157 | session_cookie: session_cookie.clone(), 158 | }, 159 | ); 160 | 161 | let mut soft_quote_client = 162 | SoftQuoteClient::with_interceptor(channel_builder, SessionInterceptor { session_cookie }); 163 | 164 | // Setup a signer so we can send transactions 165 | let settlement_engine = bindings::valorem_clear::SettlementEngine::new( 166 | settings.settlement_contract, 167 | Arc::clone(&provider), 168 | ); 169 | 170 | // We do an unchecked unwrap since if an error is returned it was due to not being able to get the chain_id 171 | // from the provider. 172 | let signer = match SignerMiddleware::new_with_provider_chain( 173 | Arc::clone(&provider), 174 | settings.wallet.clone(), 175 | ) 176 | .await 177 | { 178 | Ok(signer) => signer, 179 | Err(error) => { 180 | warn!("Error while attempting to create signer. Internally Ethers-rs will query the chain_id. Reported error {error:?}"); 181 | return None; 182 | } 183 | }; 184 | 185 | // Seaport 1.5 contract address 186 | // Note: We allow the unchecked unwrap here, since this address will always parse correctly. 187 | let seaport_contract_address = "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC" 188 | .parse::
() 189 | .unwrap(); 190 | 191 | let seaport = bindings::seaport::Seaport::new(seaport_contract_address, Arc::clone(&provider)); 192 | 193 | // Approve the tokens the example will be using 194 | if settings.approve_tokens { 195 | approve_tokens(&provider, &settings, &signer, &settlement_engine, &seaport).await; 196 | } 197 | 198 | // The gRPC stream might end for a couple of reasons, for example: 199 | // * There are no clients connected after a RFQ 200 | // * Infrastructure middle men (like Cloudflare) has killed the connection. 201 | loop { 202 | // Setup the stream between us and Valorem which the Soft Quoting gRPC connection will use. 203 | let (tx_soft_quote_response, rx_soft_quote_response) = 204 | mpsc::channel::(64); 205 | let soft_stream = match soft_quote_client 206 | .maker(tokio_stream::wrappers::ReceiverStream::new( 207 | rx_soft_quote_response, 208 | )) 209 | .await 210 | { 211 | Ok(soft_quote_stream) => soft_quote_stream, 212 | Err(error) => { 213 | warn!("Unable to create the Maker Soft Quote stream. Reported error {error:?}"); 214 | return None; 215 | } 216 | }; 217 | 218 | // Setup the stream between us and Valorem which the RFQ gRPC connection will use. 219 | let (tx_quote_response, rx_quote_response) = mpsc::channel::(64); 220 | 221 | let maker_stream = match rfq_client 222 | .maker(tokio_stream::wrappers::ReceiverStream::new( 223 | rx_quote_response, 224 | )) 225 | .await 226 | { 227 | Ok(maker_stream) => maker_stream, 228 | Err(error) => { 229 | warn!("Unable to create the Maker RFQ stream. Reported error {error:?}"); 230 | return None; 231 | } 232 | }; 233 | 234 | let mut rfq_stream = maker_stream.into_inner(); 235 | let mut quote_stream = soft_stream.into_inner(); 236 | 237 | info!("Ready for RFQs and Soft Quotes from Takers"); 238 | 239 | loop { 240 | select! { 241 | quote = rfq_stream.message() => { 242 | if let Ok(Some(quote)) = quote { 243 | // Check the chain-id is valid 244 | if quote.chain_id.is_none() { 245 | warn!( 246 | "Invalid RFQ request was received. No chain-id was given, ignoring the request" 247 | ); 248 | continue; 249 | } 250 | 251 | let chain_id: U256 = quote.chain_id.clone().unwrap().into(); 252 | if chain_id != U256::from(421614_u64) && chain_id != U256::from(31337_u64) { 253 | warn!("RFQ request was not on the testnet chain ({:?}). Ignoring the request", chain_id); 254 | continue; 255 | } 256 | 257 | let quote_offer = if validate_rfq(seaport_contract_address, quote.clone()).is_none() { 258 | // Malformed RFQ return a no-quote 259 | create_no_offer("e, &signer) 260 | } else { 261 | handle_rfq_request( 262 | quote, 263 | &settlement_engine, 264 | &signer, 265 | &seaport, 266 | settings.usdc_address, 267 | ) 268 | .await? 269 | }; 270 | 271 | if tx_quote_response.send(quote_offer).await.is_err() { 272 | warn!("Received error while attempting to send offer back on Maker RFQ channel. Reconnecting."); 273 | return None; 274 | } 275 | } else { 276 | warn!("Error while handling the RFQ stream"); 277 | return None; 278 | } 279 | }, 280 | soft_quote = quote_stream.message() => { 281 | if let Ok(Some(quote)) = soft_quote { 282 | // Check the chain-id is valid 283 | if quote.chain_id.is_none() { 284 | warn!( 285 | "Invalid Soft Quote request was received. No chain-id was given, ignoring the request" 286 | ); 287 | continue; 288 | } 289 | 290 | let chain_id: U256 = quote.chain_id.clone().unwrap().into(); 291 | if chain_id != U256::from(421614_u64) && chain_id != U256::from(31337_u64) { 292 | warn!("Soft Quote request was not on the testnet chain ({:?}). Ignoring the request", chain_id); 293 | continue; 294 | } 295 | 296 | let quote_offer = if validate_soft_quote(quote.clone()).is_none() { 297 | // Malformed soft-quote return a no-quote 298 | create_soft_quote_no_offer("e, &signer) 299 | } else { 300 | handle_soft_quote_request( 301 | quote, 302 | &settlement_engine, 303 | &signer, 304 | &seaport, 305 | settings.usdc_address, 306 | ) 307 | .await? 308 | }; 309 | 310 | if tx_soft_quote_response.send(quote_offer).await.is_err() { 311 | warn!("Received error while attempting to send offer back on Maker Soft Quote channel. Reconnecting."); 312 | return None; 313 | } 314 | } else { 315 | warn!("Error while handling the Soft Quote stream"); 316 | return None; 317 | } 318 | } 319 | } 320 | } 321 | } 322 | } 323 | 324 | // Create the "No offer" response data 325 | fn create_no_offer( 326 | request_for_quote: &QuoteRequest, 327 | signer: &SignerMiddleware>, LocalWallet>, 328 | ) -> QuoteResponse { 329 | QuoteResponse { 330 | ulid: request_for_quote.ulid.clone(), 331 | maker_address: Some(grpc_codegen::H160::from(signer.address())), 332 | order: None, 333 | chain_id: request_for_quote.chain_id.clone(), 334 | seaport_address: request_for_quote.seaport_address.clone(), 335 | } 336 | } 337 | 338 | // Create the "No offer" response data 339 | fn create_soft_quote_no_offer( 340 | request_for_quote: &QuoteRequest, 341 | signer: &SignerMiddleware>, LocalWallet>, 342 | ) -> SoftQuoteResponse { 343 | SoftQuoteResponse { 344 | ulid: request_for_quote.ulid.clone(), 345 | maker_address: Some(grpc_codegen::H160::from(signer.address())), 346 | order: None, 347 | chain_id: request_for_quote.chain_id.clone(), 348 | seaport_address: request_for_quote.seaport_address.clone(), 349 | } 350 | } 351 | 352 | // Connect and setup a valid session 353 | async fn setup( 354 | valorem_uri: Uri, 355 | wallet: LocalWallet, 356 | tls_config: ClientTlsConfig, 357 | provider: &Arc>, 358 | ) -> Option { 359 | // Connect and authenticate with Valorem 360 | let channel_builder = connect_to_valorem(valorem_uri.clone(), tls_config.clone()).await?; 361 | let mut client: AuthClient = AuthClient::new(channel_builder); 362 | 363 | let response = match client.nonce(Empty::default()).await { 364 | Ok(response) => response, 365 | Err(_) => { 366 | error!("Unable to fetch Nonce from endpoint"); 367 | return None; 368 | } 369 | }; 370 | 371 | // Fetch the session cookie for all future requests 372 | let session_cookie = match response.metadata().get(SESSION_COOKIE_KEY) { 373 | Some(session_cookie_raw) => match session_cookie_raw.to_str() { 374 | Ok(session_cookie) => session_cookie.to_string(), 375 | Err(_) => { 376 | error!("Unable to fetch session cookie from Nonce response"); 377 | return None; 378 | } 379 | }, 380 | None => { 381 | error!("Session cookie was not returned in Nonce response"); 382 | return None; 383 | } 384 | }; 385 | 386 | let nonce = response.into_inner().nonce; 387 | 388 | // Verify & authenticate with Valorem before connecting to RFQ endpoint. 389 | let channel_builder = connect_to_valorem(valorem_uri, tls_config).await?; 390 | let mut client = AuthClient::with_interceptor( 391 | channel_builder, 392 | SessionInterceptor { 393 | session_cookie: session_cookie.clone(), 394 | }, 395 | ); 396 | 397 | let chain_id = fetch_chain_id(&provider).await?.as_u64(); 398 | 399 | // Create a sign in with ethereum message 400 | let message = siwe::Message { 401 | domain: "localhost.com".parse().unwrap(), 402 | address: wallet.address().0, 403 | statement: Some(TOS_ACCEPTANCE.into()), 404 | uri: "http://localhost/".parse().unwrap(), 405 | version: Version::V1, 406 | chain_id, 407 | nonce, 408 | issued_at: TimeStamp::from(OffsetDateTime::now_utc()), 409 | expiration_time: None, 410 | not_before: None, 411 | request_id: None, 412 | resources: vec![], 413 | }; 414 | 415 | // Generate a signature 416 | let message_string = message.to_string(); 417 | let signature = wallet 418 | .sign_message(message_string.as_bytes()) 419 | .await 420 | .unwrap(); 421 | 422 | // Create the SignedMessage 423 | let signature_string = signature.to_string(); 424 | let mut signed_message = serde_json::Map::new(); 425 | signed_message.insert( 426 | "signature".to_string(), 427 | serde_json::Value::from(signature_string), 428 | ); 429 | signed_message.insert( 430 | "message".to_string(), 431 | serde_json::Value::from(message_string), 432 | ); 433 | let body = serde_json::Value::from(signed_message).to_string(); 434 | 435 | let response = client.verify(VerifyText { body }).await; 436 | match response { 437 | Ok(_) => (), 438 | Err(error) => { 439 | error!("Unable to verify client. Reported error:\n{error:?}"); 440 | return None; 441 | } 442 | } 443 | 444 | // Check that we have an authenticated session 445 | let response = client.authenticate(Empty::default()).await; 446 | match response { 447 | Ok(_) => (), 448 | Err(error) => { 449 | error!("Unable to check authentication with Valorem. Reported error:\n{error:?}"); 450 | return None; 451 | } 452 | } 453 | 454 | info!("Maker has authenticated with Valorem"); 455 | Some(session_cookie) 456 | } 457 | 458 | fn time_now() -> u64 { 459 | SystemTime::now() 460 | .duration_since(UNIX_EPOCH) 461 | .unwrap() 462 | .as_secs() 463 | } 464 | 465 | // Helper function to fetch the chain id. 466 | async fn fetch_chain_id(provider: &Provider

) -> Option { 467 | match provider.get_chainid().await { 468 | Ok(chain_id) => Some(chain_id), 469 | Err(error) => { 470 | warn!("ChainId Fetch: Error while attempting to get the chain_id. Reported error {error:?}"); 471 | return None; 472 | } 473 | } 474 | } 475 | 476 | // Helper function to connect to Valorem. 477 | async fn connect_to_valorem( 478 | valorem_uri: Uri, 479 | tls_config: ClientTlsConfig, 480 | ) -> Option { 481 | let builder = match Channel::builder(valorem_uri).tls_config(tls_config) { 482 | Ok(builder) => builder, 483 | Err(error) => { 484 | panic!("Unable to add in TLS configuration to the channel builder. Error returned {error:?}") 485 | } 486 | }; 487 | 488 | match builder 489 | .http2_keep_alive_interval(Duration::new(75, 0)) 490 | .keep_alive_timeout(Duration::new(10, 0)) 491 | .timeout(Duration::from_secs(10)) 492 | .connect_timeout(Duration::from_secs(10)) 493 | .connect() 494 | .await 495 | { 496 | Ok(builder) => Some(builder), 497 | Err(error) => { 498 | warn!("Unable to connect to Valorem endpoint. Reported error {error:?}"); 499 | return None; 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/rfq_request.rs: -------------------------------------------------------------------------------- 1 | use crate::create_no_offer; 2 | use crate::fetch_chain_id; 3 | use crate::seaport_helper::sign_order; 4 | use crate::seaport_helper::write_option; 5 | use crate::time_now; 6 | use ethers::prelude::{ 7 | rand::{thread_rng, Rng}, 8 | Address, JsonRpcClient, LocalWallet, Middleware, Provider, SignerMiddleware, U256, 9 | }; 10 | use log::{info, warn}; 11 | use std::{ops::Mul, sync::Arc}; 12 | use valorem_trade_interfaces::{ 13 | bindings, grpc_codegen, 14 | grpc_codegen::{ 15 | Action, ConsiderationItem, ItemType, OfferItem, Order, OrderType, QuoteRequest, 16 | QuoteResponse, H256, 17 | }, 18 | }; 19 | 20 | /// Validate the received RFQ is not malformed and supported. 21 | pub fn validate_rfq(seaport_address: Address, rfq: QuoteRequest) -> Option { 22 | // We always expect Valorem to set a ulid therefore if we don't see one, return a no offer. 23 | if rfq.ulid.is_none() { 24 | warn!("Received a RFQ without a ULID set. ULID was None."); 25 | return None; 26 | } 27 | 28 | // Taker address is completely optional. 29 | 30 | // Item Type needs to be valid and can only be ERC1155 for Valorem (for now) 31 | let item_type = ItemType::from_i32(rfq.item_type).unwrap_or(ItemType::Native); 32 | if item_type != ItemType::Erc1155 { 33 | warn!("Received a RFQ with an invalid ItemType. ItemType was not Erc1155."); 34 | return None; 35 | } 36 | 37 | // Since this is an RFQ there should always be a token_address (settlement contract) and 38 | // an identifier_or_criteria set. 39 | if rfq.token_address.is_none() { 40 | warn!("Received a RFQ with invalid token information. The token_address was None."); 41 | return None; 42 | } 43 | 44 | if rfq.identifier_or_criteria.is_none() { 45 | warn!("Received a RFQ with invalid token information. Identifier or Criteria was None."); 46 | return None; 47 | } 48 | 49 | // Amount needs to be non-zero 50 | if rfq.amount.is_none() { 51 | warn!("Received a RFQ with an invalid amount. Amount was None."); 52 | return None; 53 | } else { 54 | let amount: U256 = rfq.amount.clone().unwrap().into(); 55 | if amount.is_zero() { 56 | warn!("Received a RFQ with an invalid amount. Amount was Zero."); 57 | return None; 58 | } 59 | } 60 | 61 | // Action needs to be valid 62 | let action: Action = rfq.action.into(); 63 | if action == Action::Invalid { 64 | warn!("Received a RFQ with an invalid action. The Action was mapped to Invalid."); 65 | return None; 66 | } 67 | 68 | // Check the seaport address is against the one we support. 69 | if let Some(rfq_seaport_address) = rfq.seaport_address.clone() { 70 | if seaport_address != rfq_seaport_address.into() { 71 | warn!("Received an RFQ against a non-supported seaport address."); 72 | return None; 73 | } 74 | } else { 75 | warn!("Did not receive a seaport address in the RFQ."); 76 | return None; 77 | } 78 | 79 | Some(rfq) 80 | } 81 | 82 | pub async fn handle_rfq_request( 83 | request_for_quote: QuoteRequest, 84 | settlement_engine: &bindings::valorem_clear::SettlementEngine>, 85 | signer: &SignerMiddleware>, LocalWallet>, 86 | seaport: &bindings::seaport::Seaport>, 87 | usdc_address: Address, 88 | ) -> Option { 89 | // Return an offer with an hardcoded price in USDC. 90 | let fee = 10; 91 | 92 | info!( 93 | "RFQ received. Returning offer with {:?} as price.", 94 | U256::from(fee).mul(U256::exp10(6usize)) 95 | ); 96 | 97 | let request_action: Action = request_for_quote.action.into(); 98 | let (offered_item, consideration_item) = match request_action { 99 | Action::Buy => { 100 | info!( 101 | "Handling Buy Order for Option Type {:?}", 102 | U256::from(request_for_quote.identifier_or_criteria.clone().unwrap()) 103 | ); 104 | let (option_id, _claim_id) = 105 | match write_option(&request_for_quote, settlement_engine, signer).await { 106 | Some((option_id, claim_id)) => (option_id, claim_id), 107 | None => { 108 | // This signals an error, so we write no offer instead. 109 | let no_offer = create_no_offer(&request_for_quote, signer); 110 | return Some(no_offer); 111 | } 112 | }; 113 | 114 | // Option we are offering 115 | let option = OfferItem { 116 | item_type: i32::from(ItemType::Erc1155 as u8), 117 | token: Some(settlement_engine.address().into()), 118 | identifier_or_criteria: Some(option_id.into()), 119 | start_amount: request_for_quote.amount.clone(), 120 | end_amount: request_for_quote.amount.clone(), 121 | }; 122 | 123 | // Price we want for the option 124 | let price = ConsiderationItem { 125 | item_type: i32::from(ItemType::Erc20 as u8), 126 | token: Some(usdc_address.into()), 127 | identifier_or_criteria: None, 128 | start_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 129 | end_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 130 | recipient: Some(signer.address().into()), 131 | }; 132 | 133 | (option, price) 134 | } 135 | Action::Sell => { 136 | let option_id = U256::from(request_for_quote.identifier_or_criteria.clone().unwrap()); 137 | info!("Handling Sell Order for Option Id {:?}", option_id); 138 | 139 | // We are offering the following price for the given option 140 | let price = OfferItem { 141 | item_type: i32::from(ItemType::Erc20 as u8), 142 | token: Some(usdc_address.into()), 143 | identifier_or_criteria: None, 144 | start_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 145 | end_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 146 | }; 147 | 148 | // The option we want in return 149 | let option = ConsiderationItem { 150 | item_type: i32::from(ItemType::Erc1155 as u8), 151 | token: Some(settlement_engine.address().into()), 152 | identifier_or_criteria: Some(option_id.into()), 153 | start_amount: request_for_quote.amount.clone(), 154 | end_amount: request_for_quote.amount.clone(), 155 | recipient: Some(signer.address().into()), 156 | }; 157 | 158 | (price, option) 159 | } 160 | Action::Invalid => { 161 | info!("Received invalid action from the RFQ, returning no offer"); 162 | let no_offer = create_no_offer(&request_for_quote, signer); 163 | return Some(no_offer); 164 | } 165 | }; 166 | 167 | // Offer is only valid for 30 minutes 168 | let now: H256 = U256::from(time_now()).into(); 169 | let now_plus_30_minutes: H256 = (U256::from(time_now()) + U256::from(1200u64)).into(); 170 | 171 | // Reference https://docs.opensea.io/reference/seaport-overview 172 | // https://docs.opensea.io/reference/create-an-offer 173 | // Arbitrary source of entropy for the order 174 | let salt = U256::from(thread_rng().gen::()); 175 | 176 | // Valorem have a domain tag: 60DD32CF 177 | let domain_tag = U256::from_str_radix("60DD32CF", 16_u32).unwrap(); 178 | let mask = U256::from(2_u8) 179 | .pow(U256::from(32)) 180 | .saturating_sub(U256::one()); 181 | let salt = (salt & !mask) + domain_tag; 182 | 183 | let parameters = Order { 184 | zone: None, 185 | zone_hash: None, 186 | conduit_key: None, 187 | 188 | // OpenSea: Must be open order 189 | // Note: We use a FULL fill here as we don't want to allow partial fills of the order 190 | // this can change based on MM strategy 191 | order_type: i32::from(OrderType::FullOpen as u8), 192 | 193 | offerer: Some(signer.address().into()), 194 | offer: vec![offered_item], 195 | start_time: Some(now), 196 | end_time: Some(now_plus_30_minutes), 197 | consideration: vec![consideration_item], 198 | salt: Some(salt.into()), 199 | }; 200 | 201 | let signed_order = sign_order(signer, parameters, seaport).await?; 202 | let chain_id = fetch_chain_id(&signer.provider()).await?; 203 | 204 | Some(QuoteResponse { 205 | ulid: request_for_quote.ulid, 206 | maker_address: Some(grpc_codegen::H160::from(signer.address())), 207 | order: Some(signed_order), 208 | chain_id: Some(grpc_codegen::H256::from(chain_id)), 209 | seaport_address: Some(grpc_codegen::H160::from(seaport.address())), 210 | }) 211 | } 212 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/seaport_helper.rs: -------------------------------------------------------------------------------- 1 | use ethers::abi::{AbiEncode, RawLog}; 2 | use ethers::prelude::{ 3 | Address, EthLogDecode, JsonRpcClient, LocalWallet, Middleware, Provider, SignerMiddleware, U256, 4 | }; 5 | use ethers::utils::keccak256; 6 | use log::{info, warn}; 7 | use std::{ops::Mul, sync::Arc}; 8 | use valorem_trade_interfaces::{ 9 | bindings, 10 | grpc_codegen::{EthSignature, Order, QuoteRequest, SignedOrder, H256}, 11 | }; 12 | 13 | pub async fn sign_order( 14 | signer: &SignerMiddleware>, LocalWallet>, 15 | order_parameters: Order, 16 | seaport: &bindings::seaport::Seaport>, 17 | ) -> Option { 18 | // As we are going to be returning an Order, we clone the order parameters here, so we can 19 | // then use them in the order and avoid the `as_ref` and `clone` calls throughout the 20 | // transformation code (this has no performance impact, just reads a little better). 21 | let order_parameters_copy = order_parameters.clone(); 22 | 23 | // In order to sign the seaport order, we firstly transform the OrderParameters 24 | // into the ethers equivalents as we need to call the Seaport contract in order to get the 25 | // order hash. 26 | let mut offer = Vec::::new(); 27 | for offer_item in order_parameters.offer { 28 | offer.push(bindings::seaport::OfferItem { 29 | item_type: offer_item.item_type as u8, 30 | token: Address::from(offer_item.token.unwrap_or_default()), 31 | identifier_or_criteria: U256::from( 32 | offer_item.identifier_or_criteria.unwrap_or_default(), 33 | ), 34 | start_amount: U256::from(offer_item.start_amount.unwrap_or_default()), 35 | end_amount: U256::from(offer_item.end_amount.unwrap_or_default()), 36 | }); 37 | } 38 | 39 | let mut consideration = Vec::::new(); 40 | for consideration_item in order_parameters.consideration { 41 | consideration.push(bindings::seaport::ConsiderationItem { 42 | item_type: consideration_item.item_type as u8, 43 | token: Address::from(consideration_item.token.unwrap_or_default()), 44 | identifier_or_criteria: U256::from( 45 | consideration_item 46 | .identifier_or_criteria 47 | .unwrap_or_default(), 48 | ), 49 | start_amount: U256::from(consideration_item.start_amount.unwrap_or_default()), 50 | end_amount: U256::from(consideration_item.end_amount.unwrap_or_default()), 51 | recipient: Address::from(consideration_item.recipient.unwrap_or_default()), 52 | }); 53 | } 54 | 55 | let mut zone_hash: [u8; 32] = Default::default(); 56 | match order_parameters.zone_hash { 57 | Some(zone_hash_param) if zone_hash_param != H256::default() => { 58 | // We need to transform the H256 into a U256 in order for the encode into u8 to work 59 | // as we expect. 60 | zone_hash.copy_from_slice(U256::from(zone_hash_param).encode().as_slice()); 61 | } 62 | _ => zone_hash.fill(0), 63 | } 64 | 65 | let mut conduit_key: [u8; 32] = Default::default(); 66 | match order_parameters.conduit_key { 67 | Some(conduit_key_param) if conduit_key_param != H256::default() => { 68 | // We need to transform the H256 into a U256 in order for the encode into u8 to work 69 | // as we expect. 70 | conduit_key.copy_from_slice(U256::from(conduit_key_param).encode().as_slice()); 71 | } 72 | _ => conduit_key.fill(0), 73 | } 74 | 75 | let counter = match seaport.get_counter(signer.address()).await { 76 | Ok(counter) => counter, 77 | Err(error) => { 78 | warn!("Unable to get the on-chain counter from Seaport. Reported error {error:?}"); 79 | return None; 80 | } 81 | }; 82 | 83 | let order_components = bindings::seaport::OrderComponents { 84 | offerer: Address::from(order_parameters.offerer.unwrap()), 85 | zone: Address::from(order_parameters.zone.unwrap_or_default()), 86 | offer, 87 | consideration, 88 | order_type: order_parameters.order_type as u8, 89 | start_time: U256::from(order_parameters.start_time.unwrap()), 90 | end_time: U256::from(order_parameters.end_time.unwrap()), 91 | zone_hash, 92 | salt: U256::from(order_parameters.salt.unwrap()), 93 | conduit_key, 94 | counter, 95 | }; 96 | 97 | // Construct the required signature, this was taken from the Seaport tests: 98 | // https://github.com/ProjectOpenSea/seaport/blob/main/test/foundry/utils/BaseConsiderationTest.sol#L208 99 | let mut encoded_message = Vec::::new(); 100 | let order_hash = match seaport.get_order_hash(order_components).call().await { 101 | Ok(order_hash) => order_hash, 102 | Err(error) => { 103 | warn!("Unable to fetch the order hash for the order from the Seaport contract. Reported error: {error:?}"); 104 | return None; 105 | } 106 | }; 107 | let (_, domain_separator, _) = match seaport.information().call().await { 108 | Ok(seaport_information) => seaport_information, 109 | Err(error) => { 110 | warn!("Unable to retrieve on-chain Seaport information. Reported error: {error:?}"); 111 | return None; 112 | } 113 | }; 114 | 115 | // bytes2(0x1901) 116 | for byte in &[25u8, 1u8] { 117 | encoded_message.push(*byte); 118 | } 119 | 120 | for byte in &domain_separator { 121 | encoded_message.push(*byte); 122 | } 123 | 124 | for byte in &order_hash { 125 | encoded_message.push(*byte); 126 | } 127 | 128 | let hash = keccak256(encoded_message.as_slice()); 129 | let signature = signer 130 | .signer() 131 | .sign_hash(ethers::types::H256::from(hash)) 132 | .unwrap(); 133 | 134 | // We don't want to directly encode v, as this will be encoded as a u64 where leading 135 | // zeros matter (so it will be included). We know its only 1 byte, therefore only push 1 byte 136 | // of data so the signature remains 65 bytes on the wire. 137 | let eth_signature = EthSignature { 138 | v: vec![signature.v.to_le_bytes()[0]], 139 | r: signature.r.encode(), 140 | s: signature.s.encode(), 141 | }; 142 | 143 | Some(SignedOrder { 144 | parameters: Some(order_parameters_copy), 145 | signature: Some(eth_signature), 146 | }) 147 | } 148 | 149 | // This function will call "write" on the SettlementEngine contract for the Option Type 150 | // and start_amount given within the RFQ 151 | pub async fn write_option( 152 | request_for_quote: &QuoteRequest, 153 | settlement_engine: &bindings::valorem_clear::SettlementEngine>, 154 | signer: &SignerMiddleware>, LocalWallet>, 155 | ) -> Option<(U256, U256)> { 156 | let option_type: U256 = request_for_quote 157 | .identifier_or_criteria 158 | .as_ref() 159 | .unwrap() 160 | .clone() 161 | .into(); 162 | let amount: U256 = request_for_quote.amount.as_ref().unwrap().clone().into(); 163 | 164 | // Take gas estimation out of the equation which can be dicey on the Arbitrum testnet. 165 | // todo - this is true for now, in the future we should check the chain id 166 | let gas = U256::from(500000u64); 167 | let gas_price = U256::from(200).mul(U256::exp10(8usize)); 168 | 169 | let mut write_tx = settlement_engine.write(option_type, amount.as_u128()).tx; 170 | write_tx.set_gas(gas); 171 | write_tx.set_gas_price(gas_price); 172 | let pending_tx = match signer.send_transaction(write_tx, None).await { 173 | Ok(pending_tx) => pending_tx, 174 | Err(err) => { 175 | warn!("WriteTxError: Reported error {err:?}"); 176 | warn!("WriteTxError: Unable to continue creation of offer. Failed to call write with Option Type {option_type:?}."); 177 | warn!("WriteTxError: Returning no offer instead."); 178 | return None; 179 | } 180 | }; 181 | 182 | let receipt = match pending_tx.await { 183 | Ok(Some(receipt)) => receipt, 184 | Ok(None) => { 185 | warn!("WritePendingTxError: Did not get a pending transaction returned. This is bad since we made state changing call."); 186 | warn!("WritePendingTxError: Unable to continue creation of offer."); 187 | warn!("WritePendingTxError: Returning no offer instead."); 188 | return None; 189 | } 190 | Err(err) => { 191 | warn!("WritePendingTxError: Reported error {err:?}"); 192 | warn!("WritePendingTxError: Unable to continue creation of offer."); 193 | warn!("WritePendingTxError: Returning no offer instead."); 194 | return None; 195 | } 196 | }; 197 | 198 | // Fetch the logs and get the required option_id. Since we don't get the return data via the 199 | // tx we can either fetch the trace for the tx and decode the output, or we can simply 200 | // fetch the tx, lookup the logs it generated and fetch the event which has these ids. 201 | // We choose the later here we know these RPCs will always work, `debug_traceTransaction` 202 | // requires node cooperation. 203 | let mut option_id = U256::default(); 204 | let mut claim_id = U256::default(); 205 | for log_entry in receipt.logs { 206 | let topics = log_entry.topics.clone(); 207 | let data = log_entry.data.to_vec(); 208 | 209 | let event = if let Ok(log) = 210 | bindings::valorem_clear::SettlementEngineEvents::decode_log(&RawLog { topics, data }) 211 | { 212 | log 213 | } else { 214 | continue; 215 | }; 216 | 217 | if let bindings::valorem_clear::SettlementEngineEvents::OptionsWrittenFilter(event) = event 218 | { 219 | info!( 220 | "Successfully written {:?} options. Option Id {:?}. Claim Id {:?}.", 221 | event.amount, event.option_id, event.claim_id 222 | ); 223 | option_id = event.option_id; 224 | claim_id = event.claim_id; 225 | } 226 | } 227 | 228 | if option_id == U256::default() || claim_id == U256::default() { 229 | warn!("WriteError: Option Id or Claim Id did not change from the default."); 230 | warn!("WriteError: Option Id {option_id:?}. Claim Id {claim_id:?}."); 231 | warn!("WriteError: Unable to continue creation of offer."); 232 | warn!("WriteError: Returning no offer instead."); 233 | return None; 234 | } 235 | 236 | Some((option_id, claim_id)) 237 | } 238 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/settings.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, File}; 2 | use ethers::prelude::{Address, LocalWallet, Wallet}; 3 | use http::Uri; 4 | use rpassword::read_password; 5 | use serde::Deserialize; 6 | use std::fs::read_to_string; 7 | use std::io::{stdout, Write}; 8 | use std::str::FromStr; 9 | use tonic::transport::{Certificate, ClientTlsConfig}; 10 | 11 | #[derive(Deserialize, Clone, Debug)] 12 | struct InnerSettings { 13 | node_endpoint: String, 14 | valorem_endpoint: String, 15 | settlement_contract: String, 16 | keystore: Option, 17 | private_key: Option, 18 | ca_root: Option, 19 | domain_name: Option, 20 | approve_tokens: bool, 21 | magic_address: String, 22 | usdc_address: String, 23 | weth_address: String, 24 | wbtc_address: String, 25 | gmx_address: String, 26 | } 27 | 28 | #[derive(Clone, Debug)] 29 | pub struct Settings { 30 | pub node_endpoint: String, 31 | pub valorem_endpoint: Uri, 32 | pub settlement_contract: Address, 33 | pub wallet: LocalWallet, 34 | pub tls_config: ClientTlsConfig, 35 | pub approve_tokens: bool, 36 | pub magic_address: Address, 37 | pub usdc_address: Address, 38 | pub weth_address: Address, 39 | pub wbtc_address: Address, 40 | pub gmx_address: Address, 41 | } 42 | 43 | impl Settings { 44 | pub fn load(file: &str) -> Self { 45 | let settings = Config::builder() 46 | .add_source(File::with_name(file)) 47 | .build() 48 | .unwrap(); 49 | let inner: InnerSettings = settings.try_deserialize().unwrap(); 50 | 51 | let wallet = if let Some(keystore) = inner.keystore { 52 | decrypt_keystore(&keystore) 53 | } else { 54 | fetch_private_key(inner.private_key) 55 | }; 56 | 57 | // TLS Configuration, use default settings unless provided with an alternate 58 | let pem = if let Some(ca_root) = inner.ca_root { 59 | read_to_string(ca_root).unwrap() 60 | } else { 61 | read_to_string("/etc/ssl/cert.pem").unwrap() 62 | }; 63 | 64 | let domain_name = inner 65 | .domain_name 66 | .unwrap_or(String::from("trade.valorem.xyz")); 67 | 68 | let ca = Certificate::from_pem(pem); 69 | let tls_config = ClientTlsConfig::new() 70 | .ca_certificate(ca) 71 | .domain_name(domain_name); 72 | 73 | Settings { 74 | node_endpoint: inner.node_endpoint, 75 | valorem_endpoint: inner.valorem_endpoint.parse::().unwrap(), 76 | settlement_contract: inner.settlement_contract.parse::

().unwrap(), 77 | magic_address: inner.magic_address.parse::
().unwrap(), 78 | usdc_address: inner.usdc_address.parse::
().unwrap(), 79 | weth_address: inner.weth_address.parse::
().unwrap(), 80 | wbtc_address: inner.wbtc_address.parse::
().unwrap(), 81 | gmx_address: inner.gmx_address.parse::
().unwrap(), 82 | wallet, 83 | tls_config, 84 | approve_tokens: inner.approve_tokens, 85 | } 86 | } 87 | } 88 | 89 | fn decrypt_keystore(path: &str) -> LocalWallet { 90 | print!("Enter password for keystore {path} (will not be shown): "); 91 | stdout().flush().unwrap(); 92 | let password = read_password().unwrap(); 93 | 94 | match Wallet::decrypt_keystore(path, password) { 95 | Ok(wallet) => wallet, 96 | Err(err) => panic!("Failed to decrypt keystore ({path}) with the error: {err:?}"), 97 | } 98 | } 99 | 100 | fn fetch_private_key(inner_private_key: Option) -> LocalWallet { 101 | let private_key = inner_private_key.unwrap_or_else(|| { 102 | print!("Enter private key (will not be shown): "); 103 | stdout().flush().unwrap(); 104 | read_password().unwrap().trim().to_string() 105 | }); 106 | 107 | match LocalWallet::from_str(private_key.as_str()) { 108 | Ok(wallet) => wallet, 109 | Err(err) => panic!("Unable to create wallet from private key. Error returned {err:?}"), 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/settings.yaml.template: -------------------------------------------------------------------------------- 1 | # Private key is an optional setting to allow for quicker development/testing cycles, this should not be used 2 | # under operational context. 3 | #private_key: "" 4 | 5 | node_endpoint: "" 6 | valorem_endpoint: "https://trade.valorem.xyz/" 7 | settlement_contract: "0x402A401B1944EBb5A3030F36Aa70d6b5794190c9" 8 | magic_address: "0xb795f8278458443f6C43806C020a84EB5109403c" 9 | usdc_address: "0x8AE0EeedD35DbEFe460Df12A20823eFDe9e03458" 10 | weth_address: "0x618b9a2Db0CF23Bb20A849dAa2963c72770C1372" 11 | wbtc_address: "0xf8Fe24D6Ea205dd5057aD2e5FE5e313AeFd52f2e" 12 | gmx_address: "0x5337deF26Da2506e08e37682b0d6E50b26a704BB" 13 | 14 | # Approve the Taker Arbitrum testnet tokens for spending by the settlement contract & Seaport 15 | approve_tokens: false 16 | 17 | # Keystore is an optional setting. If not given a private key will need to be provided on startup 18 | #keystore: "/path/to/keystore.ks" 19 | 20 | # TLS configuration is an optional setting. 21 | # If not given the location of /etc/ssl/cert.pem will be used for the root certificate authority and trade.valorem.xyz 22 | # for the domain name. 23 | ca_root: "../../../../certs/trade.valorem.xyz.pem" 24 | domain_name: "trade.valorem.xyz" -------------------------------------------------------------------------------- /examples/rust/examples/maker/soft_quote_request.rs: -------------------------------------------------------------------------------- 1 | use crate::create_soft_quote_no_offer; 2 | use crate::fetch_chain_id; 3 | use crate::time_now; 4 | use ethers::prelude::{ 5 | rand::{thread_rng, Rng}, 6 | Address, JsonRpcClient, LocalWallet, Middleware, Provider, SignerMiddleware, U256, 7 | }; 8 | use log::{info, warn}; 9 | use std::{ops::Mul, sync::Arc}; 10 | use valorem_trade_interfaces::{ 11 | bindings, grpc_codegen, 12 | grpc_codegen::{ 13 | Action, ConsiderationItem, ItemType, OfferItem, Order, OrderType, QuoteRequest, 14 | SoftQuoteResponse, H256, 15 | }, 16 | }; 17 | 18 | /// Validate the received soft-quote is not malformed and supported. 19 | pub fn validate_soft_quote(rfq: QuoteRequest) -> Option { 20 | // Since this is an RFQ there should always be a token_address (settlement contract) and 21 | // an identifier_or_criteria set. 22 | if rfq.token_address.is_none() { 23 | warn!("Received a RFQ with invalid token information. The token_address was None."); 24 | return None; 25 | } 26 | 27 | if rfq.identifier_or_criteria.is_none() { 28 | warn!("Received a RFQ with invalid token information. Identifier or Criteria was None."); 29 | return None; 30 | } 31 | 32 | // Amount needs to be non-zero 33 | if rfq.amount.is_none() { 34 | warn!("Received a RFQ with an invalid amount. Amount was None."); 35 | return None; 36 | } else { 37 | let amount: U256 = rfq.amount.clone().unwrap().into(); 38 | if amount.is_zero() { 39 | warn!("Received a RFQ with an invalid amount. Amount was Zero."); 40 | return None; 41 | } 42 | } 43 | 44 | // Action needs to be valid 45 | let action: Action = rfq.action.into(); 46 | if action != Action::Buy && action != Action::Sell { 47 | warn!("Received a RFQ with an invalid action."); 48 | return None; 49 | } 50 | 51 | Some(rfq) 52 | } 53 | 54 | pub async fn handle_soft_quote_request( 55 | request_for_quote: QuoteRequest, 56 | settlement_engine: &bindings::valorem_clear::SettlementEngine>, 57 | signer: &SignerMiddleware>, LocalWallet>, 58 | seaport: &bindings::seaport::Seaport>, 59 | usdc_address: Address, 60 | ) -> Option { 61 | // Return an offer with an hardcoded price in USDC. 62 | let fee = 10; 63 | 64 | info!( 65 | "Soft Quote received. Returning quote with {:?} as price.", 66 | U256::from(fee).mul(U256::exp10(6usize)) 67 | ); 68 | 69 | let request_action: Action = request_for_quote.action.into(); 70 | let (offered_item, consideration_item) = match request_action { 71 | Action::Buy => { 72 | // Option we are offering 73 | let option = OfferItem { 74 | item_type: i32::from(ItemType::Erc1155 as u8), 75 | token: Some(settlement_engine.address().into()), 76 | identifier_or_criteria: request_for_quote.identifier_or_criteria, 77 | start_amount: request_for_quote.amount.clone(), 78 | end_amount: request_for_quote.amount.clone(), 79 | }; 80 | 81 | // Price we want for the option 82 | let price = ConsiderationItem { 83 | item_type: i32::from(ItemType::Erc20 as u8), 84 | token: Some(usdc_address.into()), 85 | identifier_or_criteria: None, 86 | start_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 87 | end_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 88 | recipient: Some(signer.address().into()), 89 | }; 90 | 91 | (option, price) 92 | } 93 | Action::Sell => { 94 | let option_id = U256::from(request_for_quote.identifier_or_criteria.clone().unwrap()); 95 | info!("Handling Sell Order for Option Id {:?}", option_id); 96 | 97 | // We are offering the following price for the given option 98 | let price = OfferItem { 99 | item_type: i32::from(ItemType::Erc20 as u8), 100 | token: Some(usdc_address.into()), 101 | identifier_or_criteria: None, 102 | start_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 103 | end_amount: Some(U256::from(fee).mul(U256::exp10(6usize)).into()), 104 | }; 105 | 106 | // The option we want in return 107 | let option = ConsiderationItem { 108 | item_type: i32::from(ItemType::Erc1155 as u8), 109 | token: Some(settlement_engine.address().into()), 110 | identifier_or_criteria: Some(option_id.into()), 111 | start_amount: request_for_quote.amount.clone(), 112 | end_amount: request_for_quote.amount.clone(), 113 | recipient: Some(signer.address().into()), 114 | }; 115 | 116 | (price, option) 117 | } 118 | _ => { 119 | info!("Received invalid action {:?} from the RFQ, returning no offer", request_action); 120 | let no_offer = create_soft_quote_no_offer(&request_for_quote, signer); 121 | return Some(no_offer); 122 | } 123 | }; 124 | 125 | // Offer is only valid for 30 minutes 126 | let now: H256 = U256::from(time_now()).into(); 127 | let now_plus_30_minutes: H256 = (U256::from(time_now()) + U256::from(1200u64)).into(); 128 | 129 | // Reference https://docs.opensea.io/reference/seaport-overview 130 | // https://docs.opensea.io/reference/create-an-offer 131 | // Arbitrary source of entropy for the order 132 | let salt = U256::from(thread_rng().gen::()); 133 | 134 | // Valorem have a domain tag: 60DD32CF 135 | let domain_tag = U256::from_str_radix("60DD32CF", 16_u32).unwrap(); 136 | let mask = U256::from(2_u8) 137 | .pow(U256::from(32)) 138 | .saturating_sub(U256::one()); 139 | let salt = (salt & !mask) + domain_tag; 140 | 141 | let parameters = Order { 142 | zone: None, 143 | zone_hash: None, 144 | conduit_key: None, 145 | 146 | // OpenSea: Must be open order 147 | // Note: We use a FULL fill here as we don't want to allow partial fills of the order 148 | // this can change based on MM strategy 149 | order_type: i32::from(OrderType::FullOpen as u8), 150 | 151 | offerer: Some(signer.address().into()), 152 | offer: vec![offered_item], 153 | start_time: Some(now), 154 | end_time: Some(now_plus_30_minutes), 155 | consideration: vec![consideration_item], 156 | salt: Some(salt.into()), 157 | }; 158 | 159 | let chain_id = fetch_chain_id(&signer.provider()).await?; 160 | 161 | Some(SoftQuoteResponse { 162 | ulid: request_for_quote.ulid, 163 | maker_address: Some(grpc_codegen::H160::from(signer.address())), 164 | order: Some(parameters), 165 | chain_id: Some(grpc_codegen::H256::from(chain_id)), 166 | seaport_address: Some(grpc_codegen::H160::from(seaport.address())), 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /examples/rust/examples/maker/token_approvals.rs: -------------------------------------------------------------------------------- 1 | use crate::settings::Settings; 2 | use ethers::prelude::{JsonRpcClient, LocalWallet, Middleware, Provider, SignerMiddleware, U256}; 3 | use log::info; 4 | use std::{ops::Mul, sync::Arc}; 5 | use valorem_trade_interfaces::bindings; 6 | 7 | pub async fn approve_tokens( 8 | provider: &Arc>, 9 | settings: &Settings, 10 | signer: &SignerMiddleware>, LocalWallet>, 11 | settlement_contract: &bindings::valorem_clear::SettlementEngine>, 12 | seaport_contract: &bindings::seaport::Seaport>, 13 | ) { 14 | // Note: This approval logic is tied to what the example Taker is doing and may need to 15 | // to be updated for your example 16 | // Take gas estimation out of the equation which can be dicey on the Arbitrum testnet. 17 | let gas = U256::from(900000u64); 18 | let gas_price = U256::from(300).mul(U256::exp10(8usize)); 19 | 20 | // Approval for the Seaport contract 21 | let erc20_contract = bindings::erc20::Erc20::new(settings.usdc_address, Arc::clone(provider)); 22 | let mut approval_tx = erc20_contract 23 | .approve(seaport_contract.address(), U256::MAX) 24 | .tx; 25 | approval_tx.set_gas(gas); 26 | approval_tx.set_gas_price(gas_price); 27 | signer 28 | .send_transaction(approval_tx, None) 29 | .await 30 | .unwrap() 31 | .await 32 | .unwrap(); 33 | info!( 34 | "Approved Seaport ({:?}) to spend USDC ({:?})", 35 | seaport_contract.address(), 36 | settings.usdc_address 37 | ); 38 | 39 | // Pre-approve all Options for Seaport 40 | let mut approval_tx = settlement_contract 41 | .set_approval_for_all(seaport_contract.address(), true) 42 | .tx; 43 | approval_tx.set_gas(gas); 44 | approval_tx.set_gas_price(gas_price); 45 | signer 46 | .send_transaction(approval_tx, None) 47 | .await 48 | .unwrap() 49 | .await 50 | .unwrap(); 51 | info!( 52 | "Pre-approved Seaport {:?} to move option tokens", 53 | seaport_contract.address() 54 | ); 55 | 56 | // Token approval for the Valorem SettlementEngine 57 | let erc20_contract = bindings::erc20::Erc20::new(settings.usdc_address, Arc::clone(provider)); 58 | let mut approve_tx = erc20_contract 59 | .approve(settings.settlement_contract, U256::MAX) 60 | .tx; 61 | approve_tx.set_gas(gas); 62 | approve_tx.set_gas_price(gas_price); 63 | signer 64 | .send_transaction(approve_tx, None) 65 | .await 66 | .unwrap() 67 | .await 68 | .unwrap(); 69 | info!( 70 | "Approved Settlement Engine ({:?}) to spend USDC ({:?})", 71 | settings.settlement_contract, settings.usdc_address 72 | ); 73 | 74 | let erc20_contract = bindings::erc20::Erc20::new(settings.weth_address, Arc::clone(provider)); 75 | let mut approve_tx = erc20_contract 76 | .approve(settings.settlement_contract, U256::MAX) 77 | .tx; 78 | approve_tx.set_gas(gas); 79 | approve_tx.set_gas_price(gas_price); 80 | signer 81 | .send_transaction(approve_tx, None) 82 | .await 83 | .unwrap() 84 | .await 85 | .unwrap(); 86 | info!( 87 | "Approved Settlement Engine ({:?}) to spend WETH ({:?})", 88 | settings.settlement_contract, settings.weth_address 89 | ); 90 | 91 | let erc20_contract = bindings::erc20::Erc20::new(settings.wbtc_address, Arc::clone(provider)); 92 | let mut approve_tx = erc20_contract 93 | .approve(settings.settlement_contract, U256::MAX) 94 | .tx; 95 | approve_tx.set_gas(gas); 96 | approve_tx.set_gas_price(gas_price); 97 | signer 98 | .send_transaction(approve_tx, None) 99 | .await 100 | .unwrap() 101 | .await 102 | .unwrap(); 103 | info!( 104 | "Approved Settlement Engine ({:?}) to spend WBTC ({:?})", 105 | settings.settlement_contract, settings.wbtc_address 106 | ); 107 | 108 | let erc20_contract = bindings::erc20::Erc20::new(settings.gmx_address, Arc::clone(provider)); 109 | let mut approve_tx = erc20_contract 110 | .approve(settings.settlement_contract, U256::MAX) 111 | .tx; 112 | approve_tx.set_gas(gas); 113 | approve_tx.set_gas_price(gas_price); 114 | signer 115 | .send_transaction(approve_tx, None) 116 | .await 117 | .unwrap() 118 | .await 119 | .unwrap(); 120 | info!( 121 | "Approved Settlement Engine ({:?}) to spend GMX ({:?})", 122 | settings.settlement_contract, settings.gmx_address 123 | ); 124 | 125 | let erc20_contract = bindings::erc20::Erc20::new(settings.magic_address, Arc::clone(provider)); 126 | let mut approve_tx = erc20_contract 127 | .approve(settings.settlement_contract, U256::MAX) 128 | .tx; 129 | approve_tx.set_gas(gas); 130 | approve_tx.set_gas_price(gas_price); 131 | signer 132 | .send_transaction(approve_tx, None) 133 | .await 134 | .unwrap() 135 | .await 136 | .unwrap(); 137 | info!( 138 | "Approved Settlement Engine ({:?}) to spend MAGIC ({:?})", 139 | settings.settlement_contract, settings.magic_address 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /examples/rust/examples/taker/README.md: -------------------------------------------------------------------------------- 1 | ## Running the example 2 | 3 | First copy the template config file `settings.yaml.template` and rename to `settings.yaml`, then fill in the relevant fields. 4 | 5 | The default settings is using the CA root for trade.valorem.xyz (located at the project root, `certs/trade.valorem.xyz.pem`). 6 | 7 | Then run the example like so: 8 | 9 | ```bash 10 | cargo run --example taker examples/taker/settings.yaml 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/rust/examples/taker/seaport_helper.rs: -------------------------------------------------------------------------------- 1 | use ethers::abi::AbiEncode; 2 | use ethers::prelude::{Address, Bytes, Signature, U256}; 3 | use valorem_trade_interfaces::bindings; 4 | use valorem_trade_interfaces::grpc_codegen::H256; 5 | 6 | // Transform the gRPC details into an ethers-rs Order structure so we can call 7 | // the on-chain seaport contract. 8 | // Note: Even though we have all the disassembled parameters the order has been 9 | // pre-signed by the Maker, so if we change anything the signature will not 10 | // match. 11 | pub fn transform_to_seaport_order( 12 | signed_order: &valorem_trade_interfaces::grpc_codegen::SignedOrder, 13 | offer_parameters: valorem_trade_interfaces::grpc_codegen::Order, 14 | ) -> bindings::seaport::Order { 15 | let signature_bytes: Signature = signed_order.signature.clone().unwrap().into(); 16 | let signature = Bytes::from(signature_bytes.to_vec()); 17 | 18 | let mut offer = Vec::::new(); 19 | for offer_item in offer_parameters.offer { 20 | offer.push(bindings::seaport::OfferItem { 21 | item_type: offer_item.item_type as u8, 22 | token: Address::from(offer_item.token.unwrap()), 23 | identifier_or_criteria: U256::from( 24 | offer_item.identifier_or_criteria.unwrap_or_default(), 25 | ), 26 | start_amount: U256::from(offer_item.start_amount.unwrap()), 27 | end_amount: U256::from(offer_item.end_amount.unwrap()), 28 | }) 29 | } 30 | 31 | let mut consideration = Vec::::new(); 32 | for consideration_item in offer_parameters.consideration { 33 | consideration.push(bindings::seaport::ConsiderationItem { 34 | item_type: consideration_item.item_type as u8, 35 | token: Address::from(consideration_item.token.unwrap_or_default()), 36 | identifier_or_criteria: U256::from( 37 | consideration_item 38 | .identifier_or_criteria 39 | .unwrap_or_default(), 40 | ), 41 | start_amount: U256::from(consideration_item.start_amount.unwrap()), 42 | end_amount: U256::from(consideration_item.end_amount.unwrap()), 43 | recipient: Address::from(consideration_item.recipient.unwrap()), 44 | }) 45 | } 46 | 47 | let total_original_consideration_items = U256::from(consideration.len()); 48 | 49 | let mut zone_hash: [u8; 32] = Default::default(); 50 | match offer_parameters.zone_hash { 51 | Some(zone_hash_param) if zone_hash_param != H256::default() => { 52 | // We need to transform the H256 into a U256 in order for the encode into u8 to work 53 | // as we expect. 54 | zone_hash.copy_from_slice(U256::from(zone_hash_param).encode().as_slice()); 55 | } 56 | _ => zone_hash.fill(0), 57 | } 58 | 59 | let mut conduit_key: [u8; 32] = Default::default(); 60 | match offer_parameters.conduit_key { 61 | Some(conduit_key_param) if conduit_key_param != H256::default() => { 62 | // We need to transform the H256 into a U256 in order for the encode into u8 to work 63 | // as we expect. 64 | conduit_key.copy_from_slice(U256::from(conduit_key_param).encode().as_slice()); 65 | } 66 | _ => conduit_key.fill(0), 67 | } 68 | 69 | let order_parameters = bindings::seaport::OrderParameters { 70 | offerer: Address::from(offer_parameters.offerer.unwrap()), 71 | zone: Address::from(offer_parameters.zone.unwrap_or_default()), 72 | offer, 73 | consideration, 74 | order_type: offer_parameters.order_type as u8, 75 | start_time: U256::from(offer_parameters.start_time.unwrap()), 76 | end_time: U256::from(offer_parameters.end_time.unwrap()), 77 | zone_hash, 78 | salt: U256::from(offer_parameters.salt.unwrap()), 79 | conduit_key, 80 | total_original_consideration_items, 81 | }; 82 | 83 | bindings::seaport::Order { 84 | parameters: order_parameters, 85 | signature, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/rust/examples/taker/settings.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, File}; 2 | use ethers::prelude::{Address, LocalWallet, Wallet}; 3 | use http::Uri; 4 | use rpassword::read_password; 5 | use serde::Deserialize; 6 | use std::fs::read_to_string; 7 | use std::io::{stdout, Write}; 8 | use std::str::FromStr; 9 | use tonic::transport::{Certificate, ClientTlsConfig}; 10 | 11 | #[derive(Deserialize)] 12 | struct InnerSettings { 13 | node_endpoint: String, 14 | valorem_endpoint: String, 15 | settlement_contract: String, 16 | keystore: Option, 17 | private_key: Option, 18 | ca_root: Option, 19 | domain_name: Option, 20 | approve_tokens: bool, 21 | chain_id: u64, 22 | } 23 | 24 | pub struct Settings { 25 | pub node_endpoint: String, 26 | pub valorem_endpoint: Uri, 27 | pub settlement_contract: Address, 28 | pub wallet: LocalWallet, 29 | pub tls_config: ClientTlsConfig, 30 | pub approve_tokens: bool, 31 | pub chain_id: u64, 32 | } 33 | 34 | impl Settings { 35 | pub fn load(file: &str) -> Self { 36 | let settings = Config::builder() 37 | .add_source(File::with_name(file)) 38 | .build() 39 | .unwrap(); 40 | 41 | let inner: InnerSettings = settings.try_deserialize().unwrap(); 42 | 43 | let wallet = if let Some(keystore) = inner.keystore { 44 | decrypt_keystore(&keystore) 45 | } else { 46 | fetch_private_key(inner.private_key) 47 | }; 48 | 49 | // TLS Configuration, use a standard path unless provided with an alternate 50 | let pem = if let Some(ca_root) = inner.ca_root { 51 | read_to_string(ca_root).unwrap() 52 | } else { 53 | read_to_string("/etc/ssl/cert.pem").unwrap() 54 | }; 55 | 56 | let domain_name = inner 57 | .domain_name 58 | .unwrap_or(String::from("trade.valorem.xyz")); 59 | 60 | let ca = Certificate::from_pem(pem); 61 | let tls_config = ClientTlsConfig::new() 62 | .ca_certificate(ca) 63 | .domain_name(domain_name); 64 | 65 | Settings { 66 | node_endpoint: inner.node_endpoint, 67 | valorem_endpoint: inner.valorem_endpoint.parse::().unwrap(), 68 | settlement_contract: inner.settlement_contract.parse::
().unwrap(), 69 | wallet, 70 | tls_config, 71 | approve_tokens: inner.approve_tokens, 72 | chain_id: inner.chain_id, 73 | } 74 | } 75 | } 76 | 77 | fn decrypt_keystore(path: &str) -> LocalWallet { 78 | print!("Enter password for keystore {path} (will not be shown): "); 79 | stdout().flush().unwrap(); 80 | let password = read_password().unwrap(); 81 | 82 | match Wallet::decrypt_keystore(path, password) { 83 | Ok(wallet) => wallet, 84 | Err(err) => panic!("Failed to decrypt keystore ({path}) with the error: {err:?}"), 85 | } 86 | } 87 | 88 | fn fetch_private_key(inner_private_key: Option) -> LocalWallet { 89 | let private_key = inner_private_key.unwrap_or_else(|| { 90 | print!("Enter private key (will not be shown): "); 91 | stdout().flush().unwrap(); 92 | read_password().unwrap().trim().to_string() 93 | }); 94 | 95 | match LocalWallet::from_str(private_key.as_str()) { 96 | Ok(wallet) => wallet, 97 | Err(err) => panic!("Unable to create wallet from private key. Error returned {err:?}"), 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/rust/examples/taker/settings.yaml.template: -------------------------------------------------------------------------------- 1 | # Private key is an optional setting to allow for quicker development/testing cycles, this should not be used 2 | # under operational context. 3 | #private_key: "" 4 | 5 | node_endpoint: "" 6 | valorem_endpoint: "https://trade.valorem.xyz/" 7 | settlement_contract: "0x402A401B1944EBb5A3030F36Aa70d6b5794190c9" 8 | 9 | # Approve the Taker Arbitrum testnet tokens for spending by the settlement contract & Seaport 10 | approve_tokens: false 11 | 12 | # The chain_id setting determines which chain to use. By default, the setting is configured for the Arbitrum Goerli testnet 13 | # To switch to the Arbitrum Mainnet, change the value to 42161 14 | chain_id: 421614 15 | 16 | # Keystore is an optional setting. If not given a private key will need to be provided on startup 17 | #keystore: "/path/to/keystore.ks" 18 | 19 | # TLS configuration is an optional setting. 20 | # If not given the location of /etc/ssl/cert.pem will be used for the root certificate authority and trade.valorem.xyz 21 | # for the domain name. 22 | ca_root: "../../../../certs/trade.valorem.xyz.pem" 23 | #domain_name: "trade.valorem.xyz" -------------------------------------------------------------------------------- /examples/rust/examples/taker/taker.rs: -------------------------------------------------------------------------------- 1 | use crate::seaport_helper::transform_to_seaport_order; 2 | use crate::settings::Settings; 3 | use crate::token_approvals::approve_test_tokens; 4 | use ethers::abi::RawLog; 5 | use ethers::prelude::{ 6 | Address, EthLogDecode, Http, JsonRpcClient, LocalWallet, Middleware, Provider, Signer, 7 | SignerMiddleware, Ws, U256, 8 | }; 9 | use http::Uri; 10 | use siwe::{TimeStamp, Version}; 11 | use std::env; 12 | use std::ops::Mul; 13 | use std::process::exit; 14 | use std::sync::Arc; 15 | use time::OffsetDateTime; 16 | use tokio::sync::mpsc; 17 | use tonic::transport::{Channel, ClientTlsConfig}; 18 | use valorem_trade_interfaces::bindings; 19 | use valorem_trade_interfaces::grpc_codegen::auth_client::AuthClient; 20 | use valorem_trade_interfaces::grpc_codegen::rfq_client::RfqClient; 21 | use valorem_trade_interfaces::grpc_codegen::soft_quote_client::SoftQuoteClient; 22 | use valorem_trade_interfaces::grpc_codegen::{Action, ItemType, QuoteRequest}; 23 | use valorem_trade_interfaces::grpc_codegen::{Empty, VerifyText}; 24 | use valorem_trade_interfaces::utils::session_interceptor::SessionInterceptor; 25 | 26 | mod seaport_helper; 27 | mod settings; 28 | mod token_approvals; 29 | 30 | const SESSION_COOKIE_KEY: &str = "set-cookie"; 31 | const SECONDS_IN_A_DAY: u64 = 86400u64; 32 | const SECONDS_IN_THIRTY_MINUTES: u64 = 1800u64; 33 | const TOS_ACCEPTANCE: &str = "I accept the Valorem Terms of Service at https://app.valorem.xyz/tos and Privacy Policy at https://app.valorem.xyz/privacy"; 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<(), Box> { 37 | let args: Vec = env::args().skip(1).collect(); 38 | if args.len() != 1 { 39 | eprintln!("Unexpected command line arguments. Received {:?}", args); 40 | eprintln!("Usage: taker "); 41 | exit(1); 42 | } 43 | 44 | let settings = Settings::load(&args[0]); 45 | 46 | if settings.node_endpoint.starts_with("http") { 47 | let provider = Provider::::try_from(settings.node_endpoint.clone())?; 48 | run(Arc::new(provider), settings).await; 49 | } else if settings.node_endpoint.starts_with("ws") { 50 | // Websockets (ws & wss) 51 | let provider = Provider::::new(Ws::connect(settings.node_endpoint.clone()).await?); 52 | run(Arc::new(provider), settings).await; 53 | } else { 54 | // IPC 55 | let provider = Provider::connect_ipc(settings.node_endpoint.clone()).await?; 56 | run(Arc::new(provider), settings).await; 57 | } 58 | 59 | Ok(()) 60 | } 61 | 62 | /// Main execution function. The Taker will execute the following use case. 63 | /// 1. Connect and authorise itself with Valorem 64 | /// 2. Create an Option Type 65 | /// 3. Request a buy quote from the Maker 66 | /// 4. If the Maker offers a quote: 67 | /// 5. Accept and fulfill the Order from the Maker 68 | /// 6. Request a sell quote from the Maker 69 | /// 7. Accept and fulfill the Order from the Maker 70 | /// 8. Exit 71 | /// 72 | /// If there are any unexpected errors the function will print what information it has and then 73 | /// exit. 74 | async fn run(provider: Arc>, settings: Settings) { 75 | let session_cookie = setup_valorem_connection( 76 | settings.valorem_endpoint.clone(), 77 | settings.wallet.clone(), 78 | settings.tls_config.clone(), 79 | &provider, 80 | ) 81 | .await; 82 | 83 | // Now there is a valid authenticated session, connect to the gRPC streams 84 | let mut rfq_client = RfqClient::with_interceptor( 85 | Channel::builder(settings.valorem_endpoint.clone()) 86 | .tls_config(settings.tls_config.clone()) 87 | .unwrap() 88 | .http2_keep_alive_interval(std::time::Duration::from_secs(75)) 89 | .connect() 90 | .await 91 | .unwrap(), 92 | SessionInterceptor { 93 | session_cookie: session_cookie.clone(), 94 | }, 95 | ); 96 | 97 | let mut quote_client = SoftQuoteClient::with_interceptor( 98 | Channel::builder(settings.valorem_endpoint.clone()) 99 | .tls_config(settings.tls_config.clone()) 100 | .unwrap() 101 | .http2_keep_alive_interval(std::time::Duration::from_secs(75)) 102 | .connect() 103 | .await 104 | .unwrap(), 105 | SessionInterceptor { session_cookie }, 106 | ); 107 | 108 | // Valorem Settlement Engine 109 | let settlement_engine = bindings::valorem_clear::SettlementEngine::new( 110 | settings.settlement_contract, 111 | Arc::clone(&provider), 112 | ); 113 | let signer = 114 | SignerMiddleware::new_with_provider_chain(Arc::clone(&provider), settings.wallet.clone()) 115 | .await 116 | .unwrap(); 117 | 118 | // Seaport address 119 | let seaport_contract_address = "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC" 120 | .parse::
() 121 | .unwrap(); 122 | let seaport = bindings::seaport::Seaport::new(seaport_contract_address, Arc::clone(&provider)); 123 | 124 | // Approve the tokens the example will be using on Arbitrum Testnet 125 | if settings.approve_tokens { 126 | approve_test_tokens(&provider, &signer, &settlement_engine, &seaport).await; 127 | } 128 | 129 | // Setup the stream between us and Valorem which the RFQ connection will use. 130 | // Note: We don't setup any auto-reconnect functionality since we are only executing for a 131 | // small amount of time. However this should be considered for an operational taker. 132 | let (tx_rfq, rx_rfq) = mpsc::channel::(64); 133 | let mut rfq_stream = rfq_client 134 | .taker(tokio_stream::wrappers::ReceiverStream::new(rx_rfq)) 135 | .await 136 | .unwrap() 137 | .into_inner(); 138 | 139 | // Setup the stream between us and Valorem which the Soft Quote connection will use. 140 | let (tx_quote, rx_quote) = mpsc::channel::(64); 141 | let mut quote_stream = quote_client 142 | .taker(tokio_stream::wrappers::ReceiverStream::new(rx_quote)) 143 | .await 144 | .unwrap() 145 | .into_inner(); 146 | 147 | // Create the option type we will use to request an RFQ on 148 | let option_id = setup_option(&settlement_engine, &signer).await; 149 | 150 | // Take gas estimation out of the equation which can be dicey on the Arbitrum testnet. 151 | let gas = U256::from(500000u64); 152 | let gas_price = U256::from(2000).mul(U256::exp10(8usize)); 153 | 154 | // Lets get a quote from the maker for the option we just created. 155 | let quote = QuoteRequest { 156 | ulid: None, 157 | taker_address: Some(settings.wallet.address().into()), 158 | item_type: ItemType::Erc1155 as i32, 159 | token_address: Some(settings.settlement_contract.into()), 160 | identifier_or_criteria: Some(option_id.into()), 161 | amount: Some(U256::from(5u8).into()), 162 | action: Action::Buy as i32, 163 | chain_id: Some(U256::from(settings.chain_id).into()), 164 | seaport_address: Some(seaport_contract_address.into()), 165 | }; 166 | 167 | // Send a quote 168 | println!(); 169 | println!("Sending quote to Maker for 5 Options of {option_id:?}"); 170 | tx_quote.send(quote.clone()).await.unwrap(); 171 | 172 | loop { 173 | match quote_stream.message().await { 174 | Ok(Some(quote_response)) => { 175 | if quote_response.order.is_none() { 176 | println!("Maker did not wish to provide a quote on the Order."); 177 | println!(); 178 | println!("Sending quote to Maker for 5 Options of {option_id:?}"); 179 | tx_quote.send(quote.clone()).await.unwrap(); 180 | continue; 181 | } 182 | 183 | let order_parameters = quote_response.order.unwrap(); 184 | println!( 185 | "Received quote from Maker. {:?} ({:?}) for {:?} options", 186 | U256::from( 187 | order_parameters.consideration[0] 188 | .start_amount 189 | .clone() 190 | .unwrap() 191 | ), 192 | Address::from(order_parameters.consideration[0].token.clone().unwrap()), 193 | U256::from(order_parameters.offer[0].start_amount.clone().unwrap()), 194 | ); 195 | println!("We like it!"); 196 | break; 197 | } 198 | Ok(None) => { 199 | panic!("Error: Soft Quote stream ended unexpectedly."); 200 | } 201 | Err(error) => { 202 | panic!( 203 | "Error while reading from the servers request stream: {:?}", 204 | error 205 | ); 206 | } 207 | } 208 | } 209 | 210 | let mut sell_rfq = false; 211 | 212 | // Send the RFQ buy order 213 | let rfq = quote.clone(); 214 | println!(); 215 | println!("Sending Buy RFQ to Maker for Option Type {:?}", option_id); 216 | tx_rfq.send(rfq.clone()).await.unwrap(); 217 | 218 | loop { 219 | // We expect the message to be returned on the stream back 220 | match rfq_stream.message().await { 221 | Ok(Some(offer)) => { 222 | if offer.order.is_none() { 223 | println!("Maker did not wish to make a quote on the Order."); 224 | println!(); 225 | println!("Sending Buy RFQ to Maker for Option Type {:?}", option_id); 226 | tx_rfq.send(rfq.clone()).await.unwrap(); 227 | continue; 228 | } 229 | 230 | let offered_order = offer.order.unwrap(); 231 | let offer_parameters = offered_order.parameters.clone().unwrap(); 232 | println!( 233 | "Received offer from Maker. {:?} ({:?}) for {:?} options", 234 | U256::from( 235 | offer_parameters.consideration[0] 236 | .start_amount 237 | .clone() 238 | .unwrap() 239 | ), 240 | Address::from(offer_parameters.consideration[0].token.clone().unwrap()), 241 | U256::from(offer_parameters.offer[0].start_amount.clone().unwrap()), 242 | ); 243 | 244 | let order = transform_to_seaport_order(&offered_order, offer_parameters); 245 | let option_id = order.parameters.offer[0].identifier_or_criteria; 246 | 247 | let mut order_tx = seaport.fulfill_order(order, [0u8; 32]).tx; 248 | order_tx.set_gas(gas); 249 | order_tx.set_gas_price(gas_price); 250 | let pending_tx = match signer.send_transaction(order_tx, None).await { 251 | Ok(pending_tx) => pending_tx, 252 | Err(error) => { 253 | eprintln!("Error: Unable to send fulfill order transaction to Seaport for fulfillment."); 254 | eprintln!("Reported error: {:?}", error); 255 | exit(1); 256 | } 257 | }; 258 | 259 | // Wait until the tx has been handled by the sequencer. 260 | pending_tx.await.unwrap(); 261 | 262 | if !sell_rfq { 263 | let owned_tokens = settlement_engine 264 | .balance_of(signer.address(), option_id) 265 | .call() 266 | .await 267 | .unwrap(); 268 | assert_eq!(owned_tokens, U256::from(5u8)); 269 | 270 | // Now sell all the options right back 271 | // Sell Order 272 | let rfq = QuoteRequest { 273 | ulid: None, 274 | taker_address: Some(settings.wallet.address().into()), 275 | item_type: ItemType::Erc1155 as i32, 276 | token_address: Some(settlement_engine.address().into()), 277 | identifier_or_criteria: Some(option_id.into()), 278 | amount: Some(U256::from(5u8).into()), 279 | action: Action::Sell as i32, 280 | chain_id: Some(U256::from(settings.chain_id).into()), 281 | seaport_address: Some(seaport_contract_address.into()), 282 | }; 283 | println!("Sending Sell RFQ to Maker for Option Id {:?}", option_id); 284 | sell_rfq = true; 285 | tx_rfq.send(rfq).await.unwrap(); 286 | } else { 287 | let owned_tokens = settlement_engine 288 | .balance_of(signer.address(), option_id) 289 | .call() 290 | .await 291 | .unwrap(); 292 | assert_eq!(owned_tokens, U256::zero()); 293 | println!("Sold all options back to Maker"); 294 | println!("Test case successfully finished."); 295 | exit(1); 296 | } 297 | } 298 | Ok(None) => { 299 | panic!("Error: RFQ stream ended unexpectedly."); 300 | } 301 | Err(error) => { 302 | panic!( 303 | "Error while reading from the servers request stream: {:?}", 304 | error 305 | ); 306 | } 307 | } 308 | } 309 | } 310 | 311 | // Create and setup the connection to Valorem 312 | async fn setup_valorem_connection( 313 | valorem_uri: Uri, 314 | wallet: LocalWallet, 315 | tls_config: ClientTlsConfig, 316 | provider: &Arc>, 317 | ) -> String { 318 | // Connect and authenticate with Valorem 319 | let mut client: AuthClient = AuthClient::new( 320 | Channel::builder(valorem_uri.clone()) 321 | .tls_config(tls_config.clone()) 322 | .unwrap() 323 | .connect() 324 | .await 325 | .unwrap(), 326 | ); 327 | let response = client 328 | .nonce(Empty::default()) 329 | .await 330 | .expect("Unable to fetch Nonce"); 331 | 332 | // Fetch the session cookie for all future requests 333 | let session_cookie = response 334 | .metadata() 335 | .get(SESSION_COOKIE_KEY) 336 | .expect("Session cookie was not returned in Nonce response") 337 | .to_str() 338 | .expect("Unable to fetch session cookie from Nonce response") 339 | .to_string(); 340 | 341 | let nonce = response.into_inner().nonce; 342 | 343 | // Verify & authenticate with Valorem before connecting to RFQ endpoint. 344 | let mut client = AuthClient::with_interceptor( 345 | Channel::builder(valorem_uri) 346 | .tls_config(tls_config) 347 | .unwrap() 348 | .connect() 349 | .await 350 | .unwrap(), 351 | SessionInterceptor { 352 | session_cookie: session_cookie.clone(), 353 | }, 354 | ); 355 | 356 | // Create a sign in with ethereum message 357 | let message = siwe::Message { 358 | domain: "localhost.com".parse().unwrap(), 359 | address: wallet.address().0, 360 | statement: Some(TOS_ACCEPTANCE.into()), 361 | uri: "http://localhost/".parse().unwrap(), 362 | version: Version::V1, 363 | chain_id: provider.get_chainid().await.unwrap().as_u64(), 364 | nonce, 365 | issued_at: TimeStamp::from(OffsetDateTime::now_utc()), 366 | expiration_time: None, 367 | not_before: None, 368 | request_id: None, 369 | resources: vec![], 370 | }; 371 | 372 | // Generate a signature 373 | let message_string = message.to_string(); 374 | let signature = wallet 375 | .sign_message(message_string.as_bytes()) 376 | .await 377 | .unwrap(); 378 | 379 | // Create the SignedMessage 380 | let signature_string = signature.to_string(); 381 | let mut signed_message = serde_json::Map::new(); 382 | signed_message.insert( 383 | "signature".to_string(), 384 | serde_json::Value::from(signature_string), 385 | ); 386 | signed_message.insert( 387 | "message".to_string(), 388 | serde_json::Value::from(message_string), 389 | ); 390 | let body = serde_json::Value::from(signed_message).to_string(); 391 | 392 | let response = client.verify(VerifyText { body }).await; 393 | match response { 394 | Ok(_) => (), 395 | Err(error) => { 396 | eprintln!("Error: Unable to verify client. Reported error:\n{error:?}"); 397 | exit(1); 398 | } 399 | } 400 | 401 | // Check that we have an authenticated session 402 | let response = client.authenticate(Empty::default()).await; 403 | match response { 404 | Ok(_) => (), 405 | Err(error) => { 406 | eprintln!( 407 | "Error: Unable to check authentication with Valorem. Reported error:\n{error:?}" 408 | ); 409 | exit(1); 410 | } 411 | } 412 | 413 | println!("Client has authenticated with Valorem"); 414 | session_cookie 415 | } 416 | 417 | // Create the option that we'll send RFQs on. 418 | // Note: Ideally we also return the exercise and expiry timestamps in order to ensure we can 419 | // exercise the option (if profitable) before it expires. However as this is an example 420 | // and the minimum duration of an option is 1 day from creation to exercise and than 1 day 421 | // from exercise to expiry we leave this part out. 422 | async fn setup_option( 423 | contract: &bindings::valorem_clear::SettlementEngine>, 424 | signer: &SignerMiddleware>, LocalWallet>, 425 | ) -> U256 { 426 | // WETH on Arbitrum testnet 427 | let underlying_asset = "0x618b9a2Db0CF23Bb20A849dAa2963c72770C1372" 428 | .parse::
() 429 | .unwrap(); 430 | let underlying_amount = U256::from_dec_str("1000000000000000000").unwrap().as_u128(); 431 | 432 | // USDC on Arbitrum testnet 433 | let exercise_asset = "0x8AE0EeedD35DbEFe460Df12A20823eFDe9e03458" 434 | .parse::
() 435 | .unwrap(); 436 | let exercise_amount = U256::from_dec_str("1550000000").unwrap().as_u128(); 437 | 438 | // Create the option 439 | let block_number = signer.provider().get_block_number().await.unwrap(); 440 | let block_timestamp = signer 441 | .provider() 442 | .get_block(block_number) 443 | .await 444 | .unwrap() 445 | .unwrap() 446 | .timestamp 447 | .as_u64(); 448 | let exercise_timestamp = block_timestamp + SECONDS_IN_A_DAY + SECONDS_IN_THIRTY_MINUTES; 449 | let expiry_timestamp = exercise_timestamp + SECONDS_IN_A_DAY; 450 | 451 | let mut tx = contract 452 | .new_option_type( 453 | underlying_asset, 454 | underlying_amount, 455 | exercise_asset, 456 | exercise_amount, 457 | exercise_timestamp, 458 | expiry_timestamp, 459 | ) 460 | .tx; 461 | 462 | // Take gas estimation out of the equation which can be dicey on the Arbitrum testnet. 463 | tx.set_gas(U256::from(500000u64)); 464 | tx.set_gas_price(U256::from(2000).mul(U256::exp10(8usize))); 465 | 466 | let pending_tx = match signer.send_transaction(tx, None).await { 467 | Ok(pending_tx) => pending_tx, 468 | Err(err) => { 469 | eprintln!("Error: Unable to create a new option type. Reported error: {err:?}"); 470 | exit(1); 471 | } 472 | }; 473 | 474 | let transaction_receipt = match pending_tx.await { 475 | Ok(Some(transaction_receipt)) => transaction_receipt, 476 | Ok(None) => { 477 | eprintln!("Error: No transaction receipt returned from the pending tx that is creating the option"); 478 | exit(1); 479 | } 480 | Err(err) => { 481 | eprintln!("Error: Provider error while awaiting the pending tx that is creating the option. Reported error: {err:?}"); 482 | exit(1); 483 | } 484 | }; 485 | 486 | for log_entry in transaction_receipt.logs { 487 | let topics = log_entry.topics.clone(); 488 | let data = log_entry.data.to_vec(); 489 | let event = 490 | bindings::valorem_clear::SettlementEngineEvents::decode_log(&RawLog { topics, data }) 491 | .unwrap(); 492 | 493 | if let bindings::valorem_clear::SettlementEngineEvents::NewOptionTypeFilter(event) = event { 494 | println!( 495 | "Option Id successfully created. Option Id {:?}", 496 | event.option_id 497 | ); 498 | return event.option_id; 499 | } 500 | } 501 | 502 | eprintln!("Error: Unable to find NewOptionType event within the logs of the tx that created the option!"); 503 | exit(1); 504 | } 505 | -------------------------------------------------------------------------------- /examples/rust/examples/taker/token_approvals.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::{ 2 | Address, JsonRpcClient, LocalWallet, Middleware, Provider, SignerMiddleware, U256, 3 | }; 4 | use std::ops::Mul; 5 | use std::sync::Arc; 6 | use valorem_trade_interfaces::bindings; 7 | 8 | // Approve the test tokens to used within the Arbitrum testnet 9 | pub async fn approve_test_tokens( 10 | provider: &Arc>, 11 | signer: &SignerMiddleware>, LocalWallet>, 12 | settlement_contract: &bindings::valorem_clear::SettlementEngine>, 13 | seaport_contract: &bindings::seaport::Seaport>, 14 | ) { 15 | // Take gas estimation out of the equation which can be dicey on the testnet. 16 | let gas = U256::from(500000u64); 17 | let gas_price = U256::from(2000).mul(U256::exp10(8usize)); 18 | 19 | // Approval for the Seaport contract 20 | let magic = "0xb795f8278458443f6C43806C020a84EB5109403c" 21 | .parse::
() 22 | .unwrap(); 23 | let erc20_contract = bindings::erc20::Erc20::new(magic, Arc::clone(provider)); 24 | let mut approval_tx = erc20_contract 25 | .approve(seaport_contract.address(), U256::MAX) 26 | .tx; 27 | approval_tx.set_gas(gas); 28 | approval_tx.set_gas_price(gas_price); 29 | signer 30 | .send_transaction(approval_tx, None) 31 | .await 32 | .unwrap() 33 | .await 34 | .unwrap(); 35 | println!( 36 | "Approved Seaport ({:?}) to spend MAGIC ({:?})", 37 | seaport_contract.address(), 38 | magic 39 | ); 40 | 41 | // Pre-approve all Options for Seaport (which will be the conduit in this case) 42 | let mut approval_tx = settlement_contract 43 | .set_approval_for_all(seaport_contract.address(), true) 44 | .tx; 45 | approval_tx.set_gas(gas); 46 | approval_tx.set_gas_price(gas_price); 47 | signer 48 | .send_transaction(approval_tx, None) 49 | .await 50 | .unwrap() 51 | .await 52 | .unwrap(); 53 | println!( 54 | "Pre-approved Seaport {:?} to move option tokens", 55 | seaport_contract.address() 56 | ); 57 | 58 | // Token approval for the Valorem SettlementEngine 59 | let usdc = "0x8FB1E3fC51F3b789dED7557E680551d93Ea9d892" 60 | .parse::
() 61 | .unwrap(); 62 | let erc20_contract = bindings::erc20::Erc20::new(usdc, Arc::clone(provider)); 63 | let mut approve_tx = erc20_contract 64 | .approve(settlement_contract.address(), U256::MAX) 65 | .tx; 66 | approve_tx.set_gas(gas); 67 | approve_tx.set_gas_price(gas_price); 68 | signer 69 | .send_transaction(approve_tx, None) 70 | .await 71 | .unwrap() 72 | .await 73 | .unwrap(); 74 | println!( 75 | "Approved Settlement Engine ({:?}) to spend USDC ({:?})", 76 | settlement_contract.address(), 77 | usdc 78 | ); 79 | 80 | let weth = "0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3" 81 | .parse::
() 82 | .unwrap(); 83 | let erc20_contract = bindings::erc20::Erc20::new(weth, Arc::clone(provider)); 84 | let mut approve_tx = erc20_contract 85 | .approve(settlement_contract.address(), U256::MAX) 86 | .tx; 87 | approve_tx.set_gas(gas); 88 | approve_tx.set_gas_price(gas_price); 89 | signer 90 | .send_transaction(approve_tx, None) 91 | .await 92 | .unwrap() 93 | .await 94 | .unwrap(); 95 | println!( 96 | "Approved Settlement Engine ({:?}) to spend WETH ({:?})", 97 | settlement_contract.address(), 98 | weth 99 | ); 100 | 101 | let wbtc = "0xf8Fe24D6Ea205dd5057aD2e5FE5e313AeFd52f2e" 102 | .parse::
() 103 | .unwrap(); 104 | let erc20_contract = bindings::erc20::Erc20::new(wbtc, Arc::clone(provider)); 105 | let mut approve_tx = erc20_contract 106 | .approve(settlement_contract.address(), U256::MAX) 107 | .tx; 108 | approve_tx.set_gas(gas); 109 | approve_tx.set_gas_price(gas_price); 110 | signer 111 | .send_transaction(approve_tx, None) 112 | .await 113 | .unwrap() 114 | .await 115 | .unwrap(); 116 | println!( 117 | "Approved Settlement Engine ({:?}) to spend WBTC ({:?})", 118 | settlement_contract.address(), 119 | wbtc 120 | ); 121 | 122 | let gmx = "0x5337deF26Da2506e08e37682b0d6E50b26a704BB" 123 | .parse::
() 124 | .unwrap(); 125 | let erc20_contract = bindings::erc20::Erc20::new(gmx, Arc::clone(provider)); 126 | let mut approve_tx = erc20_contract 127 | .approve(settlement_contract.address(), U256::MAX) 128 | .tx; 129 | approve_tx.set_gas(gas); 130 | approve_tx.set_gas_price(gas_price); 131 | signer 132 | .send_transaction(approve_tx, None) 133 | .await 134 | .unwrap() 135 | .await 136 | .unwrap(); 137 | println!( 138 | "Approved Settlement Engine ({:?}) to spend GMX ({:?})", 139 | settlement_contract.address(), 140 | gmx 141 | ); 142 | 143 | let magic = "0xb795f8278458443f6C43806C020a84EB5109403c" 144 | .parse::
() 145 | .unwrap(); 146 | let erc20_contract = bindings::erc20::Erc20::new(magic, Arc::clone(provider)); 147 | let mut approve_tx = erc20_contract 148 | .approve(settlement_contract.address(), U256::MAX) 149 | .tx; 150 | approve_tx.set_gas(gas); 151 | approve_tx.set_gas_price(gas_price); 152 | signer 153 | .send_transaction(approve_tx, None) 154 | .await 155 | .unwrap() 156 | .await 157 | .unwrap(); 158 | println!( 159 | "Approved Settlement Engine ({:?}) to spend MAGIC ({:?})", 160 | settlement_contract.address(), 161 | magic 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/erc1155.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | abigen!( 4 | Erc1155, 5 | "../abi/IERC1155.json", 6 | derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/erc20.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | abigen!( 4 | Erc20, 5 | "../abi/IERC20.json", 6 | derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod erc1155; 2 | pub mod erc20; 3 | pub mod seaport; 4 | pub mod seaport_counduit_controller; 5 | pub mod seaport_domain_registry; 6 | pub mod seaport_validator; 7 | pub mod valorem_clear; 8 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/seaport.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | abigen!( 4 | Seaport, 5 | "../abi/ISeaport.json", 6 | derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/seaport_counduit_controller.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | // We abigen the contract bindings from json definitions 4 | abigen!( 5 | ConduitController, 6 | "../abi/ISeaportConduitController.json", 7 | derives(serde::Deserialize, serde::Serialize) 8 | ); 9 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/seaport_domain_registry.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | // We abigen the contract bindings from json definitions 4 | abigen!( 5 | DomainRegistry, 6 | "../abi/ISeaportDomainRegistry.json", 7 | derives(serde::Deserialize, serde::Serialize) 8 | ); 9 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/seaport_validator.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | // We abigen the contract bindings from json definitions 4 | abigen!( 5 | SeaportValidator, 6 | "../abi/ISeaportOneOneValidator.json", 7 | derives(serde::Deserialize, serde::Serialize) 8 | ); 9 | -------------------------------------------------------------------------------- /examples/rust/src/bindings/valorem_clear.rs: -------------------------------------------------------------------------------- 1 | use ethers::contract::abigen; 2 | 3 | abigen!( 4 | SettlementEngine, 5 | "../abi/IValoremOptionsClearinghouse.json", 6 | event_derives(serde::Deserialize, serde::Serialize) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/rust/src/grpc_adapters.rs: -------------------------------------------------------------------------------- 1 | // Setup From traits allowing the conversion between proto types and ethers types. 2 | // Reference: https://github.com/ledgerwatch/interfaces/blob/master/src/lib.rs 3 | use crate::grpc_codegen::*; 4 | use arrayref::array_ref; 5 | use ethers::abi::AbiEncode; 6 | 7 | // Macro allowing for proto types to be converted into numbers (and vice versa), moving 8 | // through the fixed hash type first. 9 | macro_rules! into_from { 10 | ($proto:ty, $hash:ty, $num:ty) => { 11 | impl From<$num> for $proto { 12 | fn from(value: $num) -> Self { 13 | Self::from(<$hash>::from(<[u8; <$hash>::len_bytes()]>::from(value))) 14 | } 15 | } 16 | 17 | impl From<$proto> for $num { 18 | fn from(value: $proto) -> Self { 19 | Self::from(<$hash>::from(value).0) 20 | } 21 | } 22 | }; 23 | } 24 | 25 | into_from!(H128, ethers::types::H128, ethers::types::U128); 26 | into_from!(H256, ethers::types::H256, ethers::types::U256); 27 | 28 | impl From for H128 { 29 | fn from(value: ethers::types::H128) -> Self { 30 | Self { 31 | hi: u64::from_be_bytes(*array_ref!(value, 0, 8)), 32 | lo: u64::from_be_bytes(*array_ref!(value, 8, 8)), 33 | } 34 | } 35 | } 36 | 37 | impl From for ethers::types::H128 { 38 | fn from(value: H128) -> Self { 39 | let mut v = [0; Self::len_bytes()]; 40 | v[..8].copy_from_slice(&value.hi.to_be_bytes()); 41 | v[8..].copy_from_slice(&value.lo.to_be_bytes()); 42 | v.into() 43 | } 44 | } 45 | 46 | impl From for H160 { 47 | fn from(value: ethers::types::H160) -> Self { 48 | Self { 49 | hi: Some(ethers::types::H128::from_slice(&value[..16]).into()), 50 | lo: u32::from_be_bytes(*array_ref!(value, 16, 4)), 51 | } 52 | } 53 | } 54 | 55 | impl From for ethers::types::H160 { 56 | fn from(value: H160) -> Self { 57 | type H = ethers::types::H128; 58 | 59 | let mut v = [0; Self::len_bytes()]; 60 | v[..H::len_bytes()].copy_from_slice(H::from(value.hi.unwrap_or_default()).as_fixed_bytes()); 61 | v[H::len_bytes()..].copy_from_slice(&value.lo.to_be_bytes()); 62 | 63 | v.into() 64 | } 65 | } 66 | 67 | impl From for H256 { 68 | fn from(value: ethers::types::H256) -> Self { 69 | Self { 70 | hi: Some(ethers::types::H128::from_slice(&value[..16]).into()), 71 | lo: Some(ethers::types::H128::from_slice(&value[16..]).into()), 72 | } 73 | } 74 | } 75 | 76 | impl From for ethers::types::H256 { 77 | fn from(value: H256) -> Self { 78 | type H = ethers::types::H128; 79 | 80 | let mut v = [0; Self::len_bytes()]; 81 | v[..H::len_bytes()].copy_from_slice(H::from(value.hi.unwrap_or_default()).as_fixed_bytes()); 82 | v[H::len_bytes()..].copy_from_slice(H::from(value.lo.unwrap_or_default()).as_fixed_bytes()); 83 | 84 | v.into() 85 | } 86 | } 87 | 88 | impl From for ethers::types::Signature { 89 | fn from(value: EthSignature) -> Self { 90 | let mut bytes = [0u8; 65]; 91 | bytes[..32].copy_from_slice(value.r.as_slice()); 92 | bytes[32..64].copy_from_slice(value.s.as_slice()); 93 | bytes[64] = *value.v.first().unwrap(); 94 | ethers::types::Signature::try_from(bytes.as_slice()).unwrap() 95 | } 96 | } 97 | 98 | impl From for EthSignature { 99 | fn from(value: ethers::types::Signature) -> Self { 100 | // We don't want to directly encode v, as this will be encoded as a u64 where leading 101 | // zeros matter (so it will be included). We know its only 1 byte, therefore only push 1 byte 102 | // of data so the signature remains 65 bytes on the wire. 103 | Self { 104 | v: vec![value.v.to_le_bytes()[0]], 105 | r: value.r.encode(), 106 | s: value.s.encode(), 107 | } 108 | } 109 | } 110 | 111 | impl From for Action { 112 | fn from(value: i32) -> Self { 113 | Action::from_i32(value).unwrap_or(Action::Invalid) 114 | } 115 | } 116 | 117 | impl From for ItemType { 118 | fn from(value: i32) -> Self { 119 | ItemType::from_i32(value).unwrap_or(ItemType::Native) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bindings; 2 | pub mod grpc_adapters; 3 | pub mod utils; 4 | 5 | pub mod grpc_codegen { 6 | #![allow(clippy::derive_partial_eq_without_eq)] 7 | tonic::include_proto!("valorem.trade.v1"); 8 | } 9 | -------------------------------------------------------------------------------- /examples/rust/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod session_interceptor; 2 | -------------------------------------------------------------------------------- /examples/rust/src/utils/session_interceptor.rs: -------------------------------------------------------------------------------- 1 | use tonic::metadata::AsciiMetadataValue; 2 | use tonic::service::Interceptor; 3 | use tonic::{Request, Status}; 4 | 5 | const COOKIE_HEADER_KEY: &str = "cookie"; 6 | 7 | /// The Session Interceptor is a gRPC interceptor for the client to add session 8 | /// authentication details into the `request` header information such that the server can 9 | /// validate/confirm the client is using a valid session. 10 | #[derive(Default)] 11 | pub struct SessionInterceptor { 12 | pub session_cookie: String, 13 | } 14 | 15 | impl Interceptor for SessionInterceptor { 16 | fn call(&mut self, request: Request<()>) -> Result, Status> { 17 | // If we have established a session set the appropriate session headers before the request 18 | // goes to the server. 19 | let request = if !self.session_cookie.is_empty() { 20 | let mut request = request; 21 | 22 | // We should always be able to transform a String into an `AsciiMetadataValue` so its 23 | // safe to unwrap without checking. 24 | let cookie_value = AsciiMetadataValue::try_from(&self.session_cookie).unwrap(); 25 | 26 | // Insert the session cookie. 27 | request 28 | .metadata_mut() 29 | .insert(COOKIE_HEADER_KEY, cookie_value); 30 | request 31 | } else { 32 | request 33 | }; 34 | 35 | Ok(request) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/typescript/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trade-interfaces", 3 | "version": "1.0.0", 4 | "repository": "https://github.com/valorem-labs-inc/trade-interfaces.git", 5 | "license": "MIT", 6 | "type": "module", 7 | "dependencies": { 8 | "@bufbuild/buf": "^1.27.1", 9 | "@bufbuild/protobuf": "^1.2.1", 10 | "@connectrpc/connect": "^1.1.2", 11 | "@connectrpc/connect-node": "^1.1.2", 12 | "@valorem-labs-inc/sdk": "0.0.11", 13 | "@wagmi/core": "^1.4.4", 14 | "tsx": "^3.14.0", 15 | "typescript": "^5.0.4", 16 | "viem": "^1.16.6" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20.8.6", 20 | "tsconfig-paths": "^4.2.0" 21 | }, 22 | "scripts": { 23 | "codegen": "npx buf generate", 24 | "maker": "ts-node -r tsconfig-paths/register src/RFQ_maker.ts", 25 | "taker": "ts-node -r tsconfig-paths/register src/RFQ_taker.ts" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/typescript/src/RFQ_maker.ts: -------------------------------------------------------------------------------- 1 | import { createPromiseClient } from '@bufbuild/connect'; 2 | import { createGrpcTransport } from '@bufbuild/connect-node'; 3 | import { SiweMessage } from 'siwe'; 4 | import { 5 | Wallet, 6 | providers, 7 | Contract, 8 | BigNumber, 9 | utils, 10 | constants, 11 | } from 'ethers'; // v5.5.0 12 | const { JsonRpcProvider } = providers; 13 | const { parseUnits, randomBytes, splitSignature, arrayify } = utils; 14 | 15 | import { Auth } from '../gen/valorem/trade/v1/auth_connect'; // generated from auth.proto 16 | import { RFQ } from '../gen/valorem/trade/v1/rfq_connect'; // generated from rfq.proto 17 | import { 18 | Action, 19 | QuoteRequest, 20 | QuoteResponse, 21 | } from '../gen/valorem/trade/v1/rfq_pb'; // generated from rfq.proto 22 | import { EthSignature } from '../gen/valorem/trade/v1/types_pb'; // generated from types.proto 23 | import { 24 | Order, 25 | SignedOrder, 26 | ConsiderationItem, 27 | OfferItem, 28 | OrderType, 29 | ItemType, 30 | } from '../gen/valorem/trade/v1/seaport_pb'; // generated from seaport.proto 31 | 32 | import { toH160, toH256 } from './lib/fromBNToH'; // library script for H number conversions 33 | import { fromH256 } from './lib/fromHToBN'; // library script for H number conversions 34 | 35 | import IValoremOptionsClearinghouse from '../../abi/IValoremOptionsClearinghouse.json'; 36 | import ISeaport from '../../abi/ISeaport.json'; 37 | import IERC1155 from '../../abi/IERC1155.json'; 38 | import IERC20 from '../../abi/IERC20.json'; 39 | 40 | const NODE_ENDPOINT = 'https://goerli-rollup.arbitrum.io/rpc'; 41 | const GRPC_ENDPOINT = 'https://trade.valorem.xyz'; 42 | const DOMAIN = 'trade.valorem.xyz'; 43 | 44 | const VALOREM_CLEAR_ADDRESS = '0x7513F78472606625A9B505912e3C80762f6C9Efb'; // Valorem Clearinghouse on Arbitrum Goerli 45 | const SEAPORT_ADDRESS = '0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC'; // Seaport 1.5 46 | const USDC_ADDRESS = '0x8AE0EeedD35DbEFe460Df12A20823eFDe9e03458'; // our mock USDC on Arbitrum Goerli 47 | const WETH_ADDRESS = '0x618b9a2Db0CF23Bb20A849dAa2963c72770C1372'; // our mock Wrapped ETH on Arbitrum Goerli 48 | 49 | // 1. Authenticate with Valorem Trade 50 | const provider = new JsonRpcProvider(NODE_ENDPOINT); 51 | // replace with your own account to use for signing 52 | const PRIVATE_KEY = 53 | '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; 54 | const signer = new Wallet(PRIVATE_KEY, provider); 55 | 56 | let cookie: string; // to be used for all server interactions 57 | // custom Connect interceptor for retrieving cookie 58 | const trackCookie = (next: any) => async (req: any) => { 59 | const res = await next(req); 60 | cookie = res.header?.get('set-cookie')?.split(';')[0] ?? cookie; 61 | return res; 62 | }; 63 | 64 | // transport for connection to Valorem Trade gRPC server 65 | const transport = createGrpcTransport({ 66 | baseUrl: GRPC_ENDPOINT, 67 | httpVersion: '2', 68 | interceptors: [trackCookie], 69 | }); 70 | 71 | async function authenticateWithTrade() { 72 | /* Authenticate with Valorem Trade */ 73 | 74 | const authClient = createPromiseClient(Auth, transport); 75 | const { nonce } = await authClient.nonce({}); 76 | const { chainId } = await provider.getNetwork(); 77 | 78 | // create SIWE message 79 | const message = new SiweMessage({ 80 | domain: DOMAIN, 81 | address: signer.address, 82 | uri: GRPC_ENDPOINT, 83 | version: '1', 84 | chainId: chainId, 85 | nonce, 86 | issuedAt: new Date().toISOString(), 87 | }).toMessage(); 88 | 89 | // sign SIWE message 90 | const signature = await signer.signMessage(message); 91 | 92 | // verify with Valorem Trade 93 | await authClient.verify( 94 | { 95 | body: JSON.stringify({ 96 | message: message, 97 | signature: signature, 98 | }), 99 | }, 100 | { headers: [['cookie', cookie]] } 101 | ); 102 | 103 | // authenticate with Valorem Trade 104 | await authClient.authenticate({}, { headers: [['cookie', cookie]] }); 105 | 106 | console.log('Client has authenticated with Valorem Trade!'); 107 | } 108 | 109 | // 2. Listen for RFQs and respond with signed Seaport offers 110 | async function respondToRfqs() { 111 | /* Listen for RFQs and respond with signed Seaport offers */ 112 | const rfqClient = createPromiseClient(RFQ, transport); 113 | 114 | // Create your own quote request and response stream handling logic here! 115 | 116 | // empty response used to open the stream 117 | const emptyQuoteResponse = new QuoteResponse(); 118 | var emptyQuoteResponseStream = async function* () { 119 | yield emptyQuoteResponse; 120 | }; 121 | 122 | console.log('Listening for RFQs...'); 123 | 124 | while (true) { 125 | for await (const quoteRequest of rfqClient.maker( 126 | emptyQuoteResponseStream(), 127 | { headers: [['cookie', cookie]] } 128 | )) { 129 | console.log('Received a quote request...'); 130 | 131 | // construct a quote response with a signed seaport offer 132 | const quoteResponse = await constructQuoteResponse(quoteRequest); 133 | 134 | console.log('Sending quote response...'); 135 | 136 | // send response over RFQ service 137 | const quoteResponseStream = async function* () { 138 | yield quoteResponse; 139 | }; 140 | 141 | // approve spend of option amount before sending offer 142 | // valorem options are ERC1155 tokens 143 | const optionTokenContract = new Contract( 144 | VALOREM_CLEAR_ADDRESS, 145 | IERC1155, 146 | provider 147 | ); 148 | 149 | let txReceipt = await ( 150 | await optionTokenContract 151 | .connect(signer) 152 | .setApprovalForAll(SEAPORT_ADDRESS, true) 153 | ).wait(); 154 | if (txReceipt.status == 0) { 155 | console.log( 156 | 'Skipping responding to RFQ; Option ERC1155 token spend approval failed.' 157 | ); 158 | return; 159 | } 160 | 161 | rfqClient.maker(quoteResponseStream(), { headers: [['cookie', cookie]] }); 162 | } 163 | } 164 | } 165 | 166 | // 3. Construct the signed Seaport offer for a quote request and wrap in a quote response 167 | const clearinghouseContract = new Contract( 168 | VALOREM_CLEAR_ADDRESS, 169 | IValoremOptionsClearinghouse, 170 | provider 171 | ); 172 | const wethContract = new Contract(WETH_ADDRESS, IERC20, provider); 173 | 174 | async function constructQuoteResponse(quoteRequest: QuoteRequest) { 175 | /* Construct the signed Seaport offer for a quote request and wrap in a quote response */ 176 | if (!quoteRequest.identifierOrCriteria) { 177 | console.log( 178 | 'Skipping Quote Request because "identifierOrCriteria" is undefined.' 179 | ); 180 | } 181 | const optionId = fromH256(quoteRequest.identifierOrCriteria); 182 | 183 | if (quoteRequest.action !== Action.BUY) { 184 | console.log( 185 | 'Skipping Quote Request because only responding to buy requests.' 186 | ); 187 | return; 188 | } 189 | 190 | if (!quoteRequest.amount) { 191 | console.log('Skipping Quote Request because "amount" is undefined.'); 192 | } 193 | const optionAmount = fromH256(quoteRequest.amount); 194 | 195 | // get option info 196 | const optionInfo = await clearinghouseContract.option(optionId); 197 | 198 | if (optionInfo.underlyingAsset !== WETH_ADDRESS) { 199 | console.log( 200 | 'Skipping Quote Request because only responding to WETH options.' 201 | ); 202 | return; 203 | } 204 | console.log( 205 | 'Responding to quote request to buy', 206 | optionAmount.toString(), 207 | 'WETH options with ID', 208 | optionId.toString() 209 | ); 210 | console.log('Option info:'); 211 | console.log(optionInfo); 212 | 213 | // approve clearing house transfer of underlying asset 214 | const totalUnderlyingAmount = optionAmount.mul(optionInfo.underlyingAmount); // number options * underlying amount per option 215 | 216 | const txReceiptApprove = await ( 217 | await wethContract 218 | .connect(signer) 219 | .approve(VALOREM_CLEAR_ADDRESS, totalUnderlyingAmount) 220 | ).wait(); 221 | if (txReceiptApprove.status == 0) { 222 | console.log( 223 | 'Skipping responding to RFQ; Underlying ERC20 approval failed.' 224 | ); 225 | return; 226 | } 227 | 228 | // write option with clearing house 229 | const txReceiptWrite = await ( 230 | await clearinghouseContract.connect(signer).write(optionId, optionAmount) 231 | ).wait(); 232 | if (txReceiptWrite.status == 0) { 233 | console.log( 234 | 'Skipping responding to RFQ; Writing option with clearing house failed.' 235 | ); 236 | return; 237 | } 238 | 239 | // Now lets construct the Seaport offer! 240 | // Note we use Seaport v1.1; see https://github.com/ProjectOpenSea/seaport/blob/seaport-1.1/docs/SeaportDocumentation.md 241 | 242 | // option we are offering 243 | const offerItem = { 244 | itemType: ItemType.ERC1155, // see Seaport ItemType enum 245 | token: VALOREM_CLEAR_ADDRESS, 246 | identifierOrCriteria: optionId, 247 | startAmount: fromH256(quoteRequest.amount), 248 | endAmount: fromH256(quoteRequest.amount), 249 | }; 250 | // price we want for the option 251 | const USDCprice = parseUnits('100', 6); // 100 USDC 252 | const considerationItem = { 253 | itemType: ItemType.ERC20, 254 | token: USDC_ADDRESS, 255 | startAmount: USDCprice.toString(), 256 | endAmount: USDCprice.toString(), 257 | recipient: signer.address, 258 | identifierOrCriteria: BigNumber.from(0), // not used for ERC20 259 | }; 260 | 261 | const now = (await provider.getBlock(await provider.getBlockNumber())) 262 | .timestamp; 263 | const in_2_mins = now + 2 * 60; // offer expires in 2 minutes 264 | const salt = `0x${Buffer.from(randomBytes(8)) 265 | .toString('hex') 266 | .padStart(64, '0')}`; 267 | 268 | const seaportContract = new Contract(SEAPORT_ADDRESS, ISeaport, provider); 269 | const counter = await seaportContract.getCounter(signer.address); 270 | 271 | const orderComponents = { 272 | offerer: signer.address, 273 | zone: constants.AddressZero, 274 | offer: [offerItem], 275 | consideration: [considerationItem], 276 | orderType: OrderType.FULL_OPEN, 277 | startTime: now, 278 | endTime: in_2_mins, 279 | zoneHash: constants.HashZero, 280 | salt: salt, 281 | conduitKey: constants.HashZero, 282 | counter: counter, 283 | }; 284 | 285 | // create order signature 286 | const ORDER_TYPES = { 287 | OrderComponents: [ 288 | { name: 'offerer', type: 'address' }, 289 | { name: 'zone', type: 'address' }, 290 | { name: 'offer', type: 'OfferItem[]' }, 291 | { name: 'consideration', type: 'ConsiderationItem[]' }, 292 | { name: 'orderType', type: 'uint8' }, 293 | { name: 'startTime', type: 'uint256' }, 294 | { name: 'endTime', type: 'uint256' }, 295 | { name: 'zoneHash', type: 'bytes32' }, 296 | { name: 'salt', type: 'uint256' }, 297 | { name: 'conduitKey', type: 'bytes32' }, 298 | { name: 'counter', type: 'uint256' }, 299 | ], 300 | OfferItem: [ 301 | { name: 'itemType', type: 'uint8' }, 302 | { name: 'token', type: 'address' }, 303 | { name: 'identifierOrCriteria', type: 'uint256' }, 304 | { name: 'startAmount', type: 'uint256' }, 305 | { name: 'endAmount', type: 'uint256' }, 306 | ], 307 | ConsiderationItem: [ 308 | { name: 'itemType', type: 'uint8' }, 309 | { name: 'token', type: 'address' }, 310 | { name: 'identifierOrCriteria', type: 'uint256' }, 311 | { name: 'startAmount', type: 'uint256' }, 312 | { name: 'endAmount', type: 'uint256' }, 313 | { name: 'recipient', type: 'address' }, 314 | ], 315 | }; 316 | 317 | // see https://docs.ethers.org/v5/api/signer/#Signer-signTypedData 318 | const signature = await signer._signTypedData( 319 | {}, // domain data is optional 320 | ORDER_TYPES, 321 | orderComponents 322 | ); 323 | // Use EIP-2098 compact signatures to save gas. 324 | const splitSig = splitSignature(signature); 325 | const ethSignature = new EthSignature({ 326 | r: arrayify(splitSig.r), 327 | s: arrayify(splitSig.s), 328 | v: arrayify(splitSig.v), 329 | }); 330 | 331 | // convert order fields to H types 332 | const offerItem_H = new OfferItem({ 333 | itemType: offerItem.itemType, 334 | token: toH160(offerItem.token), 335 | identifierOrCriteria: toH256(offerItem.identifierOrCriteria), 336 | startAmount: toH256(offerItem.startAmount), 337 | endAmount: toH256(offerItem.endAmount), 338 | }); 339 | const considerationItem_H = new ConsiderationItem({ 340 | itemType: considerationItem.itemType, 341 | token: toH160(considerationItem.token), 342 | identifierOrCriteria: toH256(considerationItem.identifierOrCriteria), 343 | startAmount: toH256(considerationItem.startAmount), 344 | endAmount: toH256(considerationItem.endAmount), 345 | recipient: toH160(considerationItem.recipient), 346 | }); 347 | const order_H = new Order({ 348 | offerer: toH160(orderComponents.offerer), 349 | zone: toH160(orderComponents.zone), 350 | offer: [offerItem_H], 351 | consideration: [considerationItem_H], 352 | orderType: orderComponents.orderType, 353 | startTime: toH256(orderComponents.startTime), 354 | endTime: toH256(orderComponents.endTime), 355 | zoneHash: toH256(orderComponents.zoneHash), 356 | salt: toH256(orderComponents.salt), 357 | conduitKey: toH256(orderComponents.conduitKey), 358 | }); 359 | 360 | const signedOrder_H = new SignedOrder({ 361 | parameters: order_H, 362 | signature: ethSignature, 363 | }); 364 | 365 | // construct a quote response 366 | const quoteResponse = new QuoteResponse({ 367 | ulid: quoteRequest.ulid, 368 | makerAddress: toH160(signer.address), 369 | order: signedOrder_H, 370 | }); 371 | 372 | return quoteResponse; 373 | } 374 | 375 | async function main() { 376 | await authenticateWithTrade(); 377 | await respondToRfqs(); 378 | } 379 | 380 | main(); 381 | -------------------------------------------------------------------------------- /examples/typescript/src/RFQ_taker.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, createWalletClient, http, parseUnits } from 'viem'; 2 | import { privateKeyToAccount } from 'viem/accounts'; 3 | import { arbitrumSepolia } from 'viem/chains'; 4 | import { 5 | ERC20Contract, 6 | ParsedQuoteResponse, 7 | SEAPORT_ADDRESS, 8 | ValoremSDK, 9 | OptionType, 10 | get24HrTimestamps, 11 | GRPC_ENDPOINT, 12 | trackCookieInterceptor, 13 | Auth, 14 | RFQ, 15 | SupportedAsset, 16 | } from '@valorem-labs-inc/sdk'; 17 | import { createPromiseClient } from '@connectrpc/connect'; 18 | import { createGrpcTransport } from '@connectrpc/connect-node'; 19 | 20 | /** Viem Config */ 21 | 22 | // replace with your own account to use for signing 23 | // you will need a Valorem Access Pass 24 | const PRIVATE_KEY = 25 | '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; 26 | const account = privateKeyToAccount(PRIVATE_KEY); 27 | 28 | const publicClient = createPublicClient({ 29 | chain: arbitrumSepolia, 30 | transport: http(), 31 | }); 32 | const walletClient = createWalletClient({ 33 | account, 34 | chain: arbitrumSepolia, 35 | transport: http(), 36 | }); 37 | 38 | /** GRPC Config */ 39 | const transport = createGrpcTransport({ 40 | baseUrl: GRPC_ENDPOINT, 41 | httpVersion: '2', 42 | interceptors: [trackCookieInterceptor], 43 | nodeOptions: { 44 | rejectUnauthorized: false, 45 | }, 46 | }); 47 | 48 | const authClient = createPromiseClient(Auth, transport); 49 | const rfqClient = createPromiseClient(RFQ, transport); 50 | 51 | /** Init Valorem SDK with clients */ 52 | const valoremSDK = new ValoremSDK({ 53 | publicClient, 54 | walletClient, 55 | authClient, 56 | rfqClient, 57 | }); 58 | 59 | // get the WebTaker instance (essentially a wallet/account/signer, with some utility methods) 60 | const webTaker = valoremSDK.webTaker; 61 | 62 | // Our mock tokens on Arbitrum Sepolia 63 | const USDC = SupportedAsset.fromSymbolAndChainId('USDC', 421614); 64 | const WETH = SupportedAsset.fromSymbolAndChainId('WETH', 421614); 65 | 66 | // contract instances 67 | const clearinghouse = valoremSDK.clearinghouse; 68 | const usdc = new ERC20Contract({ 69 | address: USDC.address, 70 | publicClient, 71 | walletClient, 72 | }); 73 | 74 | /** 75 | * Main Taker Logic 76 | * - Authenticate with Valorem Trade API 77 | * - Initialize an option with Valorem Clearinghouse 78 | * - Send RFQs to Market Makers 79 | * - Accept returned quotes by executing the signed offers on Seaport 80 | */ 81 | 82 | // 1. Authenticate with Valorem Trade API 83 | async function authenticate() { 84 | await webTaker.signIn(); 85 | if (!webTaker.authenticated) { 86 | throw new Error('Sign in failed.'); 87 | } 88 | } 89 | 90 | // 2. Initialize an option with Valorem Clearinghouse 91 | async function createOptionType() { 92 | // Configure your own option type here! 93 | const underlyingAsset = WETH.address; 94 | const exerciseAsset = USDC.address; 95 | const underlyingAmount = 1000000000000n; // 1 WETH, divided by 1e6 96 | const exerciseAmount = 2500n; // 2500 USDC, divided by 1e6 97 | const { exerciseTimestamp, expiryTimestamp } = get24HrTimestamps(); 98 | 99 | const optionInfo = { 100 | underlyingAsset, 101 | underlyingAmount, 102 | exerciseAsset, 103 | exerciseAmount, 104 | exerciseTimestamp, 105 | expiryTimestamp, 106 | }; 107 | 108 | const optionType = await OptionType.fromInfo({ 109 | optionInfo, 110 | clearinghouse, 111 | }); 112 | 113 | // check if option type already exists 114 | if (!optionType.typeExists) { 115 | // if it does not exist, create it 116 | await optionType.createOptionType(webTaker); 117 | } else { 118 | console.log('Option type already created.'); 119 | } 120 | 121 | if (!optionType.optionTypeId) throw new Error('Failed to get OptionTypeId'); 122 | return optionType.optionTypeId; 123 | } 124 | 125 | // 3. Send RFQs 126 | async function sendRfqRequests(optionId: bigint) { 127 | // Create your own quote request logic here! 128 | // for this example: a quote request to buy 1000 options 129 | const quoteRequest = webTaker.createQuoteRequest({ 130 | optionId, 131 | quantityToBuy: 1, 132 | }); 133 | 134 | // quote streams can accept an abort signal to close the stream 135 | // for this example we will close it after a successful quote response 136 | const abortController = new AbortController(); 137 | const abort = () => { 138 | console.log('Closing stream...'); 139 | abortController.abort(); 140 | }; 141 | 142 | // this is the callback that will be called when a quote response is received 143 | const onQuoteResponse = async (quote: ParsedQuoteResponse) => { 144 | abort(); // close the stream 145 | 146 | // create your own quote response handling logic here 147 | // ie: check that the price is within a certain range, add to a queue and accept the best offer after a delay, etc 148 | // for this example we will make sure we have enough balance and allowance to accept the quote 149 | const sufficient = await checkBalanceAndAllowance(quote); 150 | 151 | if (sufficient) { 152 | console.log('Accepting quote...'); 153 | await webTaker.acceptQuote({ quote }); 154 | console.log('Done!'); 155 | } else { 156 | console.log('Not enough balance or allowance to accept quote.'); 157 | } 158 | }; 159 | 160 | // continuously send requests and handle responses 161 | await webTaker.sendRFQ({ 162 | quoteRequest, 163 | onQuoteResponse, 164 | signal: abortController.signal, 165 | }); 166 | } 167 | 168 | /** Check balances and allowances needed to accept quote */ 169 | async function checkBalanceAndAllowance( 170 | quote: ParsedQuoteResponse 171 | ): Promise { 172 | const usdcPremium = quote.order.parameters.consideration[0]!.startAmount; 173 | 174 | const hasEnoughBalance = await webTaker.hasEnoughERC20Balance({ 175 | erc20: usdc, 176 | amount: usdcPremium, 177 | }); 178 | if (!hasEnoughBalance) { 179 | console.log( 180 | `Not enough balance to accept quote. Need ${parseUnits( 181 | usdcPremium.toString(), 182 | usdc.decimals 183 | )} USDC.` 184 | ); 185 | return false; 186 | } 187 | 188 | const hasEnoughAllowance = await webTaker.hasEnoughERC20Allowance({ 189 | erc20: usdc, 190 | amount: usdcPremium, 191 | spender: SEAPORT_ADDRESS, 192 | }); 193 | if (!hasEnoughAllowance) { 194 | await webTaker.approveERC20({ 195 | tokenAddress: usdc.address, 196 | spender: SEAPORT_ADDRESS, 197 | amount: usdcPremium, 198 | }); 199 | } 200 | 201 | return true; 202 | } 203 | 204 | async function main() { 205 | await authenticate(); 206 | const optionTypeId = await createOptionType(); 207 | await sendRfqRequests(optionTypeId); 208 | } 209 | 210 | main(); 211 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "noUncheckedIndexedAccess": true, 19 | "noPropertyAccessFromIndexSignature": false, 20 | "baseUrl": ".", 21 | "types": ["node"] 22 | }, 23 | "ts-node": { 24 | "transpileOnly": true, 25 | "files": true, 26 | "experimentalResolver": true, 27 | "esm": true 28 | }, 29 | "include": ["src"], 30 | "exclude": ["node_modules", "gen"] 31 | } 32 | -------------------------------------------------------------------------------- /img/valorem-trade-api-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valorem-labs-inc/trade-interfaces/d43d44f78cf37b353aab3854abbd5e6beb7509b7/img/valorem-trade-api-banner.png -------------------------------------------------------------------------------- /proto/google/rpc/error_details.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/duration.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "ErrorDetailsProto"; 24 | option java_package = "com.google.rpc"; 25 | option objc_class_prefix = "RPC"; 26 | 27 | // Describes the cause of the error with structured details. 28 | // 29 | // Example of an error when contacting the "pubsub.googleapis.com" API when it 30 | // is not enabled: 31 | // 32 | // { "reason": "API_DISABLED" 33 | // "domain": "googleapis.com" 34 | // "metadata": { 35 | // "resource": "projects/123", 36 | // "service": "pubsub.googleapis.com" 37 | // } 38 | // } 39 | // 40 | // This response indicates that the pubsub.googleapis.com API is not enabled. 41 | // 42 | // Example of an error that is returned when attempting to create a Spanner 43 | // instance in a region that is out of stock: 44 | // 45 | // { "reason": "STOCKOUT" 46 | // "domain": "spanner.googleapis.com", 47 | // "metadata": { 48 | // "availableRegions": "us-central1,us-east2" 49 | // } 50 | // } 51 | message ErrorInfo { 52 | // The reason of the error. This is a constant value that identifies the 53 | // proximate cause of the error. Error reasons are unique within a particular 54 | // domain of errors. This should be at most 63 characters and match a 55 | // regular expression of `[A-Z][A-Z0-9_]+[A-Z0-9]`, which represents 56 | // UPPER_SNAKE_CASE. 57 | string reason = 1; 58 | 59 | // The logical grouping to which the "reason" belongs. The error domain 60 | // is typically the registered service name of the tool or product that 61 | // generates the error. Example: "pubsub.googleapis.com". If the error is 62 | // generated by some common infrastructure, the error domain must be a 63 | // globally unique value that identifies the infrastructure. For Google API 64 | // infrastructure, the error domain is "googleapis.com". 65 | string domain = 2; 66 | 67 | // Additional structured details about this error. 68 | // 69 | // Keys should match /[a-zA-Z0-9-_]/ and be limited to 64 characters in 70 | // length. When identifying the current value of an exceeded limit, the units 71 | // should be contained in the key, not the value. For example, rather than 72 | // {"instanceLimit": "100/request"}, should be returned as, 73 | // {"instanceLimitPerRequest": "100"}, if the client exceeds the number of 74 | // instances that can be created in a single (batch) request. 75 | map metadata = 3; 76 | } 77 | 78 | // Describes when the clients can retry a failed request. Clients could ignore 79 | // the recommendation here or retry when this information is missing from error 80 | // responses. 81 | // 82 | // It's always recommended that clients should use exponential backoff when 83 | // retrying. 84 | // 85 | // Clients should wait until `retry_delay` amount of time has passed since 86 | // receiving the error response before retrying. If retrying requests also 87 | // fail, clients should use an exponential backoff scheme to gradually increase 88 | // the delay between retries based on `retry_delay`, until either a maximum 89 | // number of retries have been reached or a maximum retry delay cap has been 90 | // reached. 91 | message RetryInfo { 92 | // Clients should wait at least this long between retrying the same request. 93 | google.protobuf.Duration retry_delay = 1; 94 | } 95 | 96 | // Describes additional debugging info. 97 | message DebugInfo { 98 | // The stack trace entries indicating where the error occurred. 99 | repeated string stack_entries = 1; 100 | 101 | // Additional debugging information provided by the server. 102 | string detail = 2; 103 | } 104 | 105 | // Describes how a quota check failed. 106 | // 107 | // For example if a daily limit was exceeded for the calling project, 108 | // a service could respond with a QuotaFailure detail containing the project 109 | // id and the description of the quota limit that was exceeded. If the 110 | // calling project hasn't enabled the service in the developer console, then 111 | // a service could respond with the project id and set `service_disabled` 112 | // to true. 113 | // 114 | // Also see RetryInfo and Help types for other details about handling a 115 | // quota failure. 116 | message QuotaFailure { 117 | // A message type used to describe a single quota violation. For example, a 118 | // daily quota or a custom quota that was exceeded. 119 | message Violation { 120 | // The subject on which the quota check failed. 121 | // For example, "clientip:" or "project:". 123 | string subject = 1; 124 | 125 | // A description of how the quota check failed. Clients can use this 126 | // description to find more about the quota configuration in the service's 127 | // public documentation, or find the relevant quota limit to adjust through 128 | // developer console. 129 | // 130 | // For example: "Service disabled" or "Daily Limit for read operations 131 | // exceeded". 132 | string description = 2; 133 | } 134 | 135 | // Describes all quota violations. 136 | repeated Violation violations = 1; 137 | } 138 | 139 | // Describes what preconditions have failed. 140 | // 141 | // For example, if an RPC failed because it required the Terms of Service to be 142 | // acknowledged, it could list the terms of service violation in the 143 | // PreconditionFailure message. 144 | message PreconditionFailure { 145 | // A message type used to describe a single precondition failure. 146 | message Violation { 147 | // The type of PreconditionFailure. We recommend using a service-specific 148 | // enum type to define the supported precondition violation subjects. For 149 | // example, "TOS" for "Terms of Service violation". 150 | string type = 1; 151 | 152 | // The subject, relative to the type, that failed. 153 | // For example, "google.com/cloud" relative to the "TOS" type would indicate 154 | // which terms of service is being referenced. 155 | string subject = 2; 156 | 157 | // A description of how the precondition failed. Developers can use this 158 | // description to understand how to fix the failure. 159 | // 160 | // For example: "Terms of service not accepted". 161 | string description = 3; 162 | } 163 | 164 | // Describes all precondition violations. 165 | repeated Violation violations = 1; 166 | } 167 | 168 | // Describes violations in a client request. This error type focuses on the 169 | // syntactic aspects of the request. 170 | message BadRequest { 171 | // A message type used to describe a single bad request field. 172 | message FieldViolation { 173 | // A path that leads to a field in the request body. The value will be a 174 | // sequence of dot-separated identifiers that identify a protocol buffer 175 | // field. 176 | // 177 | // Consider the following: 178 | // 179 | // message CreateContactRequest { 180 | // message EmailAddress { 181 | // enum Type { 182 | // TYPE_UNSPECIFIED = 0; 183 | // HOME = 1; 184 | // WORK = 2; 185 | // } 186 | // 187 | // optional string email = 1; 188 | // repeated EmailType type = 2; 189 | // } 190 | // 191 | // string full_name = 1; 192 | // repeated EmailAddress email_addresses = 2; 193 | // } 194 | // 195 | // In this example, in proto `field` could take one of the following values: 196 | // 197 | // * `full_name` for a violation in the `full_name` value 198 | // * `email_addresses[1].email` for a violation in the `email` field of the 199 | // first `email_addresses` message 200 | // * `email_addresses[3].type[2]` for a violation in the second `type` 201 | // value in the third `email_addresses` message. 202 | // 203 | // In JSON, the same values are represented as: 204 | // 205 | // * `fullName` for a violation in the `fullName` value 206 | // * `emailAddresses[1].email` for a violation in the `email` field of the 207 | // first `emailAddresses` message 208 | // * `emailAddresses[3].type[2]` for a violation in the second `type` 209 | // value in the third `emailAddresses` message. 210 | string field = 1; 211 | 212 | // A description of why the request element is bad. 213 | string description = 2; 214 | } 215 | 216 | // Describes all violations in a client request. 217 | repeated FieldViolation field_violations = 1; 218 | } 219 | 220 | // Contains metadata about the request that clients can attach when filing a bug 221 | // or providing other forms of feedback. 222 | message RequestInfo { 223 | // An opaque string that should only be interpreted by the service generating 224 | // it. For example, it can be used to identify requests in the service's logs. 225 | string request_id = 1; 226 | 227 | // Any data that was used to serve this request. For example, an encrypted 228 | // stack trace that can be sent back to the service provider for debugging. 229 | string serving_data = 2; 230 | } 231 | 232 | // Describes the resource that is being accessed. 233 | message ResourceInfo { 234 | // A name for the type of resource being accessed, e.g. "sql table", 235 | // "cloud storage bucket", "file", "Google calendar"; or the type URL 236 | // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". 237 | string resource_type = 1; 238 | 239 | // The name of the resource being accessed. For example, a shared calendar 240 | // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current 241 | // error is 242 | // [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. 243 | string resource_name = 2; 244 | 245 | // The owner of the resource (optional). 246 | // For example, "user:" or "project:". 248 | string owner = 3; 249 | 250 | // Describes what error is encountered when accessing this resource. 251 | // For example, updating a cloud project may require the `writer` permission 252 | // on the developer console project. 253 | string description = 4; 254 | } 255 | 256 | // Provides links to documentation or for performing an out of band action. 257 | // 258 | // For example, if a quota check failed with an error indicating the calling 259 | // project hasn't enabled the accessed service, this can contain a URL pointing 260 | // directly to the right place in the developer console to flip the bit. 261 | message Help { 262 | // Describes a URL link. 263 | message Link { 264 | // Describes what the link offers. 265 | string description = 1; 266 | 267 | // The URL of the link. 268 | string url = 2; 269 | } 270 | 271 | // URL(s) pointing to additional information on handling the current error. 272 | repeated Link links = 1; 273 | } 274 | 275 | // Provides a localized error message that is safe to return to the user 276 | // which can be attached to an RPC error. 277 | message LocalizedMessage { 278 | // The locale used following the specification defined at 279 | // https://www.rfc-editor.org/rfc/bcp/bcp47.txt. 280 | // Examples are: "en-US", "fr-CH", "es-MX" 281 | string locale = 1; 282 | 283 | // The localized error message in the above locale. 284 | string message = 2; 285 | } -------------------------------------------------------------------------------- /proto/google/rpc/status.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option cc_enable_arenas = true; 22 | option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "StatusProto"; 25 | option java_package = "com.google.rpc"; 26 | option objc_class_prefix = "RPC"; 27 | 28 | // The `Status` type defines a logical error model that is suitable for 29 | // different programming environments, including REST APIs and RPC APIs. It is 30 | // used by [gRPC](https://github.com/grpc). Each `Status` message contains 31 | // three pieces of data: error code, error message, and error details. 32 | // 33 | // You can find out more about this error model and how to work with it in the 34 | // [API Design Guide](https://cloud.google.com/apis/design/errors). 35 | message Status { 36 | // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. 37 | int32 code = 1; 38 | 39 | // A developer-facing error message, which should be in English. Any 40 | // user-facing error message should be localized and sent in the 41 | // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. 42 | string message = 2; 43 | 44 | // A list of messages that carry the error details. There is a common set of 45 | // message types for APIs to use. 46 | repeated google.protobuf.Any details = 3; 47 | } -------------------------------------------------------------------------------- /proto/grpc/health/v1/health.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The canonical version of this proto can be found at 16 | // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto 17 | 18 | syntax = "proto3"; 19 | 20 | package grpc.health.v1; 21 | 22 | option csharp_namespace = "Grpc.Health.V1"; 23 | option go_package = "google.golang.org/grpc/health/grpc_health_v1"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "HealthProto"; 26 | option java_package = "io.grpc.health.v1"; 27 | 28 | message HealthCheckRequest { 29 | string service = 1; 30 | } 31 | 32 | message HealthCheckResponse { 33 | enum ServingStatus { 34 | UNKNOWN = 0; 35 | SERVING = 1; 36 | NOT_SERVING = 2; 37 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 38 | } 39 | ServingStatus status = 1; 40 | } 41 | 42 | service Health { 43 | // If the requested service is unknown, the call will fail with status 44 | // NOT_FOUND. 45 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 46 | 47 | // Performs a watch for the serving status of the requested service. 48 | // The server will immediately send back a message indicating the current 49 | // serving status. It will then subsequently send a new message whenever 50 | // the service's serving status changes. 51 | // 52 | // If the requested service is unknown when the call is received, the 53 | // server will send a message setting the serving status to 54 | // SERVICE_UNKNOWN but will *not* terminate the call. If at some 55 | // future point, the serving status of the service becomes known, the 56 | // server will send a new message with the service's serving status. 57 | // 58 | // If the call terminates with status UNIMPLEMENTED, then clients 59 | // should assume this method is not supported and should not retry the 60 | // call. If the call terminates with any other status (including OK), 61 | // clients should retry the call with appropriate exponential backoff. 62 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 63 | } -------------------------------------------------------------------------------- /proto/grpc/reflection/v1alpha/reflection.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2016 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Service exported by server reflection 16 | 17 | syntax = "proto3"; 18 | 19 | package grpc.reflection.v1alpha; 20 | 21 | service ServerReflection { 22 | // The reflection service is structured as a bidirectional stream, ensuring 23 | // all related requests go to a single server. 24 | rpc ServerReflectionInfo(stream ServerReflectionRequest) 25 | returns (stream ServerReflectionResponse); 26 | } 27 | 28 | // The message sent by the client when calling ServerReflectionInfo method. 29 | message ServerReflectionRequest { 30 | string host = 1; 31 | // To use reflection service, the client should set one of the following 32 | // fields in message_request. The server distinguishes requests by their 33 | // defined field and then handles them using corresponding methods. 34 | oneof message_request { 35 | // Find a proto file by the file name. 36 | string file_by_filename = 3; 37 | 38 | // Find the proto file that declares the given fully-qualified symbol name. 39 | // This field should be a fully-qualified symbol name 40 | // (e.g. .[.] or .). 41 | string file_containing_symbol = 4; 42 | 43 | // Find the proto file which defines an extension extending the given 44 | // message type with the given field number. 45 | ExtensionRequest file_containing_extension = 5; 46 | 47 | // Finds the tag numbers used by all known extensions of extendee_type, and 48 | // appends them to ExtensionNumberResponse in an undefined order. 49 | // Its corresponding method is best-effort: it's not guaranteed that the 50 | // reflection service will implement this method, and it's not guaranteed 51 | // that this method will provide all extensions. Returns 52 | // StatusCode::UNIMPLEMENTED if it's not implemented. 53 | // This field should be a fully-qualified type name. The format is 54 | // . 55 | string all_extension_numbers_of_type = 6; 56 | 57 | // List the full names of registered services. The content will not be 58 | // checked. 59 | string list_services = 7; 60 | } 61 | } 62 | 63 | // The type name and extension number sent by the client when requesting 64 | // file_containing_extension. 65 | message ExtensionRequest { 66 | // Fully-qualified type name. The format should be . 67 | string containing_type = 1; 68 | int32 extension_number = 2; 69 | } 70 | 71 | // The message sent by the server to answer ServerReflectionInfo method. 72 | message ServerReflectionResponse { 73 | string valid_host = 1; 74 | ServerReflectionRequest original_request = 2; 75 | // The server sets one of the following fields according to the 76 | // message_request in the request. 77 | oneof message_response { 78 | // This message is used to answer file_by_filename, file_containing_symbol, 79 | // file_containing_extension requests with transitive dependencies. 80 | // As the repeated label is not allowed in oneof fields, we use a 81 | // FileDescriptorResponse message to encapsulate the repeated fields. 82 | // The reflection service is allowed to avoid sending FileDescriptorProtos 83 | // that were previously sent in response to earlier requests in the stream. 84 | FileDescriptorResponse file_descriptor_response = 4; 85 | 86 | // This message is used to answer all_extension_numbers_of_type requests. 87 | ExtensionNumberResponse all_extension_numbers_response = 5; 88 | 89 | // This message is used to answer list_services requests. 90 | ListServiceResponse list_services_response = 6; 91 | 92 | // This message is used when an error occurs. 93 | ErrorResponse error_response = 7; 94 | } 95 | } 96 | 97 | // Serialized FileDescriptorProto messages sent by the server answering 98 | // a file_by_filename, file_containing_symbol, or file_containing_extension 99 | // request. 100 | message FileDescriptorResponse { 101 | // Serialized FileDescriptorProto messages. We avoid taking a dependency on 102 | // descriptor.proto, which uses proto2 only features, by making them opaque 103 | // bytes instead. 104 | repeated bytes file_descriptor_proto = 1; 105 | } 106 | 107 | // A list of extension numbers sent by the server answering 108 | // all_extension_numbers_of_type request. 109 | message ExtensionNumberResponse { 110 | // Full name of the base type, including the package name. The format 111 | // is . 112 | string base_type_name = 1; 113 | repeated int32 extension_number = 2; 114 | } 115 | 116 | // A list of ServiceResponse sent by the server answering list_services request. 117 | message ListServiceResponse { 118 | // The information of each service may be expanded in the future, so we use 119 | // ServiceResponse message to encapsulate it. 120 | repeated ServiceResponse service = 1; 121 | } 122 | 123 | // The information of a single service used by ListServiceResponse to answer 124 | // list_services request. 125 | message ServiceResponse { 126 | // Full name of a registered service, including its package name. The format 127 | // is . 128 | string name = 1; 129 | } 130 | 131 | // The error code and error message sent by the server when an error occurs. 132 | message ErrorResponse { 133 | // This field uses the error codes defined in grpc::StatusCode. 134 | int32 error_code = 1; 135 | string error_message = 2; 136 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "types.proto"; 4 | import "google/protobuf/wrappers.proto"; 5 | 6 | package valorem.trade.v1; 7 | 8 | // Services and messages enabling EIP-4361 authentication via gRPC. 9 | // Reference: https://docs.login.xyz/ 10 | 11 | service Auth { 12 | // Returns an EIP-4361 nonce for session and invalidates existing session 13 | rpc Nonce (Empty) returns (NonceText); 14 | 15 | // Verifies the SignedMessage is valid and returns the verified address 16 | rpc Verify (VerifyText) returns (H160); 17 | 18 | // Used to check if a given connection is authenticated, returns the address which is authenticated for a nonce cookie 19 | rpc Authenticate (Empty) returns (H160); 20 | 21 | // Used to check if a given connection is geofenced. If access is restricted, returns true, otherwise false. 22 | rpc Geofenced (Empty) returns (google.protobuf.BoolValue); 23 | 24 | // Returns the SIWE Session for the request's sessionId 25 | rpc Session (Empty) returns (SiweSession); 26 | 27 | // Invalidates the session for the request's sessionId 28 | rpc SignOut (Empty) returns (Empty); 29 | } 30 | 31 | // Nonce response message containing the generated `nonce` string. 32 | message NonceText { 33 | string nonce = 1; 34 | } 35 | 36 | // Verify request message containing an JSON encoded string of the `SignedMessage` structure (src/auth). 37 | // In a future major/breaking version of this API might be renamed to SignedMessage, and yes, it needs to be JSON 38 | // encoded as per the spec for SIWE. 39 | message VerifyText { 40 | string body = 1; 41 | } 42 | 43 | // The object representing a valid/authenticated sid 44 | message SiweSession { 45 | H160 address = 1; 46 | H256 chain_id = 2; 47 | } 48 | -------------------------------------------------------------------------------- /proto/valorem/trade/v1/fees.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "types.proto"; 4 | 5 | package valorem.trade.v1; 6 | 7 | // Service for getting Valorem Clear and Trade fee information. 8 | service Fees { 9 | // Returns the Valorem Fee structure. 10 | rpc getFeeStructure(Empty) returns (FeeStructure); 11 | } 12 | 13 | message FeeStructure { 14 | TradeFees maker = 1; 15 | TradeFees taker = 2; 16 | // A fee or rebate on notional value written via Clear expressed in basis points. 17 | int32 clear_write_notional_bps = 3; 18 | // A fee or rebate on underlying asset notional value redeemed via Clear expressed in basis points. 19 | int32 clear_redeemed_notional_bps = 4; 20 | // A fee or rebate on notional value exercised via Clear expressed in basis points. 21 | int32 clear_exercise_notional_bps = 5; 22 | // The address fees must be paid to or rebates are received from. 23 | H160 address = 6; 24 | } 25 | 26 | message TradeFees { 27 | // A fee or rebate on notional value traded expressed in basis points. 28 | int32 notional_bps = 1; 29 | // A fee or rebate on premia or credit value traded expressed in basis points. 30 | int32 premium_bps = 2; 31 | // A fee or rebate on spot value traded expressed in basis points. 32 | int32 spot_bps = 3; 33 | // A flat relayer fee or rebate expressed in 1e-6 USDC (dust)g - used for non-valued offers/considerations 34 | // such as NFTs. 35 | int32 flat = 4; 36 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/rfq.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "types.proto"; 4 | import "seaport.proto"; 5 | 6 | package valorem.trade.v1; 7 | 8 | 9 | // Request for quote (RFQ) services and related message formats. 10 | 11 | service RFQ { 12 | // Request quotes from makers via a single QuoteRequest message and receive a stream of QuoteResponse messages for use by gRPC-web clients. 13 | rpc WebTaker (QuoteRequest) returns (stream QuoteResponse); 14 | // Request quotes from makers via a stream of QuoteRequest messages and receive a stream of QuoteResponse messages. 15 | rpc Taker (stream QuoteRequest) returns (stream QuoteResponse); 16 | // Send quotes to takers via a stream of QuoteResponse messages and receive a stream of QuoteRequest messages. 17 | rpc Maker (stream QuoteResponse) returns (stream QuoteRequest); 18 | } 19 | 20 | // The Action enum specifies whether the taker is requesting a quote to buy or sell an asset. 21 | enum Action { 22 | BUY = 0; 23 | SELL = 1; 24 | INVALID = 255; 25 | } 26 | 27 | // The fields comprising the quote request message give the maker what they need to provide a quote/signed offer. 28 | message QuoteRequest { 29 | // The unique identifier for the quote request. This is used to match the 30 | // quote response to the quote request. 31 | optional H128 ulid = 1; 32 | 33 | // Ideally the maker would never know who the taker is, and vice-versa. 34 | // However, seaport reveals the makers' address to the taker. 35 | // takerAddress ensures there is no information asymmetry between 36 | // the maker and taker. Thought the trader may not always end up being 37 | // the taker. 38 | optional H160 taker_address = 2; 39 | 40 | ItemType item_type = 3; 41 | 42 | // The token address for which a quote is being requested. 43 | optional H160 token_address = 4; 44 | 45 | // The identifier_or_criteria represents either the ERC721 or ERC1155 46 | // token identifier or, in the case of a criteria-based item type, a 47 | // merkle root composed of the valid set of token identifiers for 48 | // the item. This value will be ignored for Ether and ERC20 item types, 49 | // and can optionally be zero for criteria-based item types to allow 50 | // for any identifier. 51 | optional H256 identifier_or_criteria = 5; 52 | 53 | H256 amount = 6; 54 | 55 | // A request by the Taker to the Maker, i.e. if the request is Buy the Taker wants to buy the option from the 56 | // Maker, whereas Sell is the Taker wanting to sell to the Maker. 57 | Action action = 7; 58 | 59 | // The EIP-155 chain_id for the chain for which the quote is requested. 60 | optional H256 chain_id = 8; 61 | 62 | // The address of the seaport contract for which the quote is requested. 63 | optional H160 seaport_address = 9; 64 | } 65 | 66 | // The quote response message contains the quote/signed offer from the maker. 67 | message QuoteResponse { 68 | // The unique identifier for the quote request. This is used to match the 69 | // quote response to the quote request. 70 | optional H128 ulid = 1; 71 | 72 | // The address of the maker making the offer. 73 | optional H160 maker_address = 2; 74 | 75 | // The order and signature from the maker. 76 | SignedOrder order = 3; 77 | 78 | // The EIP-155 chain_id for the chain for which the offer was signed. 79 | optional H256 chain_id = 4; 80 | 81 | // The address of the seaport contract for which the offer was signed. 82 | optional H160 seaport_address = 5; 83 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/seaport.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "types.proto"; 4 | 5 | package valorem.trade.v1; 6 | 7 | // Messages to support the Seaport smart contract interface via protobuf. 8 | // Reference: https://docs.opensea.io/reference/seaport-overview 9 | 10 | // The ItemType designates the type of item, with valid types being Ether 11 | // (or other native token for the given chain), ERC20, ERC721, ERC1155, 12 | // ERC721 with "criteria" (explained below), and ERC1155 with criteria. 13 | enum ItemType { 14 | NATIVE = 0; 15 | ERC20 = 1; 16 | ERC721 = 2; 17 | ERC1155 = 3; 18 | ERC721_WITH_CRITERIA = 4; 19 | ERC1155_WITH_CRITERIA = 5; 20 | } 21 | 22 | // This is an item required in exchange for an offer. 23 | message ConsiderationItem { 24 | ItemType item_type = 1; 25 | H160 token = 2; 26 | H256 identifier_or_criteria = 3; 27 | H256 start_amount = 4; 28 | H256 end_amount = 5; 29 | H160 recipient = 6; 30 | } 31 | 32 | // This is an item offered in exchange for consideration. 33 | message OfferItem { 34 | ItemType item_type = 1; 35 | 36 | // The token designates the account of the item's token contract 37 | // (with the null address used for Ether or other native tokens). 38 | H160 token = 2; 39 | 40 | // The identifier_or_criteria represents either the ERC721 or ERC1155 41 | // token identifier or, in the case of a criteria-based item type, a 42 | // merkle root composed of the valid set of token identifiers for 43 | // the item. This value will be ignored for Ether and ERC20 item types, 44 | // and can optionally be zero for criteria-based item types to allow 45 | // for any identifier. 46 | H256 identifier_or_criteria = 3; 47 | 48 | // The start_amount represents the amount of the item in question that 49 | // will be required should the order be fulfilled at the moment the 50 | // order becomes active. 51 | H256 start_amount = 4; 52 | 53 | // The end_amount represents the amount of the item in question that 54 | // will be required should the order be fulfilled at the moment the 55 | // order expires. If this value differs from the item's start_amount, 56 | // the realized amount is calculated linearly based on the time elapsed 57 | // since the order became active. 58 | H256 end_amount = 5; 59 | } 60 | 61 | // The OrderType designates one of four types for the order depending on 62 | // two distinct preferences: 63 | // 64 | // FULL indicates that the order does not support partial fills, 65 | // whereas PARTIAL enables filling some fraction of the order, with the 66 | // important caveat that each item must be cleanly divisible by the supplied 67 | // fraction (i.e. no remainder after division). 68 | // 69 | // OPEN indicates that the call to execute the order can be submitted by 70 | // any account, whereas RESTRICTED requires that the order either be executed 71 | // by the offerer or the zone of the order, or that a magic value indicating 72 | // that the order is approved is returned upon calling validateOrder on 73 | // the zone. 74 | enum OrderType { 75 | FULL_OPEN = 0; 76 | PARTIAL_OPEN = 1; 77 | FULL_RESTRICTED = 2; 78 | PARTIAL_RESTRICTED = 3; 79 | } 80 | 81 | // Each order contains ten key components 82 | message Order { 83 | // The offerer of the order supplies all offered items and must either 84 | // fulfill the order personally (i.e. msg.sender == offerer) or approve 85 | // the order via signature (either standard 65-byte EDCSA, 64-byte 86 | // EIP-2098, or an EIP-1271 isValidSignature check) or by listing 87 | // the order on-chain (i.e. calling validate). 88 | H160 offerer = 1; 89 | 90 | // The zone of the order is an optional secondary account attached to the 91 | // order with two additional privileges: 92 | // 93 | // The zone may cancel orders where it is named as the zone by calling 94 | // cancel. (Note that offerers can also cancel their own orders, either 95 | // individually or for all orders signed with their current counter at 96 | // once by calling incrementCounter). 97 | // "Restricted" orders (as specified by the order type) must either be 98 | // executed by the zone or the offerer, or must be approved as indicated 99 | // by a call to an validateOrder on the zone. 100 | H160 zone = 2; 101 | 102 | // The offers array contains an array of items that may be transferred 103 | // from the offerer's account. 104 | repeated OfferItem offer = 3; 105 | 106 | // The consideration contains an array of items that must be received 107 | // in order to fulfill the order. It contains all of the same components 108 | // as an offered item, and additionally includes a recipient that will 109 | // receive each item. This array may be extended by the fulfiller on 110 | // order fulfillment so as to support "tipping" (e.g. relayer or 111 | // referral payments) 112 | repeated ConsiderationItem consideration = 4; 113 | 114 | OrderType order_type = 5; 115 | 116 | // The start_time indicates the block timestamp at which the order 117 | // becomes active. 118 | H256 start_time = 6; 119 | 120 | // The end_time indicates the block timestamp at which the order expires. 121 | // This value and the startTime are used in conjunction with the 122 | // start_amount and end_amount of each item to derive their current amount. 123 | H256 end_time = 7; 124 | 125 | // The zoneHash represents an arbitrary 32-byte value that will be 126 | // supplied to the zone when fulfilling restricted orders that the zone 127 | // can utilize when making a determination on whether to authorize the order. 128 | H256 zone_hash = 8; 129 | 130 | // The salt represents an arbitrary source of entropy for the order. 131 | H256 salt = 9; 132 | 133 | // The conduit_key is a bytes32 value that indicates what conduit, 134 | // if any, should be utilized as a source for token approvals when 135 | // performing transfers. By default (i.e. when conduitKey is set to the 136 | // zero hash), the offerer will grant ERC20, ERC721, and ERC1155 token 137 | // approvals to Seaport directly so that it can perform any transfers 138 | // specified by the order during fulfillment. In contrast, an offerer 139 | // that elects to utilize a conduit will grant token approvals to the 140 | // conduit contract corresponding to the supplied conduit key, and 141 | // Seaport will then instruct that conduit to transfer the respective 142 | // tokens. 143 | H256 conduit_key = 10; 144 | } 145 | 146 | message SignedOrder { 147 | Order parameters = 1; 148 | EthSignature signature = 2; 149 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/soft_quote.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "types.proto"; 4 | import "seaport.proto"; 5 | import "rfq.proto"; 6 | 7 | package valorem.trade.v1; 8 | 9 | // Soft quote services and related message formats. 10 | 11 | service SoftQuote { 12 | // Request soft quotes from makers via a single QuoteRequest message and receive a stream of SoftQuoteResponse messages for use by gRPC-web clients. 13 | rpc WebTaker (QuoteRequest) returns (stream SoftQuoteResponse); 14 | // Request soft quotes from makers via a stream of QuoteRequest messages and receive a stream of SoftQuoteResponse messages. 15 | rpc Taker (stream QuoteRequest) returns (stream SoftQuoteResponse); 16 | // Send quotes to takers via a stream of SoftQuoteResponse messages and receive a stream of QuoteRequest messages. 17 | rpc Maker (stream SoftQuoteResponse) returns (stream QuoteRequest); 18 | } 19 | 20 | // The quote response message contains the quote/signed offer from the maker. 21 | message SoftQuoteResponse { 22 | // The unique identifier for the quote request. This is used to match the 23 | // quote response to the quote request. 24 | optional H128 ulid = 1; 25 | 26 | // The address of the maker making the offer. 27 | optional H160 maker_address = 2; 28 | 29 | // The order from the maker. 30 | Order order = 3; 31 | 32 | // The EIP-155 chain_id for the chain for which the quote is for. 33 | optional H256 chain_id = 4; 34 | 35 | // The address of the seaport contract for which the quote is for. 36 | optional H160 seaport_address = 5; 37 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/spot.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // Import additional EVM data types types. 4 | import "types.proto"; 5 | 6 | package valorem.trade.v1; 7 | 8 | // Spot service offers methods related to the spot tokens 9 | // on various Ethereum-related chains. 10 | service Spot { 11 | 12 | // GetSpotPriceStream is a server-streaming RPC. It provides real-time spot prices 13 | // for a list of tokens across multiple chains. 14 | // 15 | // Parameters: 16 | // SpotPriceRequest: Contains information about which tokens' spot prices 17 | // should be fetched on their respective chains. 18 | // Returns: 19 | // stream of SpotPriceResponse: Continuously streams data about the spot prices 20 | // of the requested tokens on their respective chains 21 | // as updates are available. 22 | rpc GetSpotPrice (SpotPriceRequest) returns (stream SpotPriceInfo); 23 | } 24 | 25 | // SpotPriceRequest encapsulates the details of the tokens for which 26 | // spot prices are desired. 27 | message SpotPriceRequest { 28 | 29 | // spot_price_info contains the details of each token (like its address and the 30 | // chain it's on) for which the spot price should be fetched. 31 | repeated SpotPriceInfo spot_price_info = 1; 32 | } 33 | 34 | // SpotPriceInfo represents the details and the spot price (if available) for a single token 35 | // on a particular blockchain. 36 | message SpotPriceInfo { 37 | 38 | // chain_id denotes the specific chain on which the token is located. 39 | uint64 chain_id = 1; 40 | 41 | // token_address is the Ethereum address of the token, usually in H160 format. 42 | H160 token_address = 2; 43 | 44 | // spot_price, if available, is the current spot price in USD of the token. 45 | // It is represented in a 60x18 (42 integer digits, 18 decimals) H256 format 46 | // for high precision. This field is optional since the spot price might not 47 | // always be available for every token, and because this message is reused 48 | // in the request and response stream. 49 | optional H256 spot_price = 3; 50 | } -------------------------------------------------------------------------------- /proto/valorem/trade/v1/types.proto: -------------------------------------------------------------------------------- 1 | // Partially from: https://github.com/ledgerwatch/interfaces/blob/master/types/types.proto 2 | syntax = "proto3"; 3 | 4 | package valorem.trade.v1; 5 | 6 | message H40 { 7 | uint32 hi = 1; 8 | // Note: lo is really a uint8, however the closest type in Protocol Buffers is uint32. Parsing needs 9 | // to take this into consideration. 10 | uint32 lo = 2; 11 | } 12 | 13 | message H96 { 14 | uint64 hi = 1; 15 | uint32 lo = 2; 16 | } 17 | 18 | message H128 { 19 | uint64 hi = 1; 20 | uint64 lo = 2; 21 | } 22 | 23 | message H160 { 24 | H128 hi = 1; 25 | uint32 lo = 2; 26 | } 27 | 28 | message H256 { 29 | H128 hi = 1; 30 | H128 lo = 2; 31 | } 32 | 33 | // ECDSA signatures in Ethereum consist of three parameters: v, r and s. The signature is always 65-bytes in length. 34 | // r = first 32 bytes of signature 35 | // s = second 32 bytes of signature 36 | // v = final 1 byte of signature 37 | // Since protobuf doesn't support uint8, we use a boolean for v, which is always 8 bits. 38 | message EthSignature { 39 | bytes r = 1; 40 | bytes s = 2; 41 | bytes v = 3; 42 | } 43 | 44 | // The empty message 45 | message Empty {} --------------------------------------------------------------------------------