├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── go.mod ├── go.sum └── pkg ├── cache ├── cache.go └── cache_test.go ├── cluster ├── cluster.go ├── doc.go ├── etcd │ ├── etcd.go │ └── etcd_test.go ├── grpc │ ├── balancer.go │ ├── context.go │ ├── example │ │ ├── cluster.go │ │ └── cluster_test.go │ ├── picker.go │ └── resolver.go ├── memory │ ├── memory.go │ └── memory_test.go ├── registry │ ├── cluster.go │ └── registry.go ├── ring │ ├── ring.go │ └── ring_test.go └── tests │ └── cluster_tests.go ├── code ├── aml │ ├── guard.go │ ├── guard_test.go │ └── metrics.go ├── antispam │ ├── guard.go │ ├── integration.go │ └── metrics.go ├── async │ ├── account │ │ ├── config.go │ │ ├── gift_card.go │ │ ├── gift_card_test.go │ │ ├── metrics.go │ │ ├── service.go │ │ ├── swap.go │ │ ├── swap_test.go │ │ └── testutil.go │ ├── airdrop │ │ ├── config.go │ │ ├── indexer.go │ │ ├── integration.go │ │ ├── nonce.go │ │ ├── service.go │ │ ├── transaction.go │ │ └── worker.go │ ├── currency │ │ └── exchange_rate.go │ ├── geyser │ │ ├── api │ │ │ ├── Makefile │ │ │ ├── gen │ │ │ │ ├── geyser.pb.go │ │ │ │ ├── geyser_grpc.pb.go │ │ │ │ └── solana-storage.pb.go │ │ │ └── proto │ │ │ │ ├── geyser.proto │ │ │ │ └── solana-storage.proto │ │ ├── backup.go │ │ ├── config.go │ │ ├── consumer.go │ │ ├── external_deposit.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── metrics.go │ │ ├── retry.go │ │ ├── service.go │ │ ├── subscription.go │ │ ├── timelock.go │ │ └── util.go │ ├── nonce │ │ ├── allocator.go │ │ ├── config.go │ │ ├── keys.go │ │ ├── metrics.go │ │ ├── pool.go │ │ ├── pool_test.go │ │ ├── service.go │ │ └── util.go │ ├── sequencer │ │ ├── action_handler.go │ │ ├── action_handler_test.go │ │ ├── config.go │ │ ├── fulfillment_handler.go │ │ ├── fulfillment_handler_test.go │ │ ├── intent_handler.go │ │ ├── intent_handler_test.go │ │ ├── metrics.go │ │ ├── scheduler.go │ │ ├── scheduler_test.go │ │ ├── service.go │ │ ├── testutil.go │ │ ├── timelock.go │ │ ├── utils.go │ │ ├── utils_test.go │ │ ├── vm.go │ │ ├── worker.go │ │ └── worker_test.go │ ├── service.go │ ├── vm │ │ ├── service.go │ │ └── storage.go │ └── webhook │ │ ├── config.go │ │ ├── metrics.go │ │ ├── service.go │ │ ├── worker.go │ │ └── worker_test.go ├── auth │ ├── encoding.go │ ├── encoding_test.go │ ├── signature.go │ └── signature_test.go ├── balance │ ├── calculator.go │ └── calculator_test.go ├── common │ ├── account.go │ ├── account_test.go │ ├── key.go │ ├── key_test.go │ ├── mint.go │ ├── mint_test.go │ ├── owner.go │ ├── owner_test.go │ ├── subsidizer.go │ ├── subsidizer_test.go │ ├── testutil.go │ └── vm.go ├── config │ └── config.go ├── data │ ├── account │ │ ├── acccount_info.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── action │ │ ├── action.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── balance │ │ ├── checkpoint.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── blockchain.go │ ├── config.go │ ├── currency │ │ ├── memory │ │ │ └── store.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ ├── tests │ │ │ └── tests.go │ │ └── util │ │ │ └── util.go │ ├── cvm │ │ ├── ram │ │ │ ├── account.go │ │ │ ├── memory │ │ │ │ ├── store.go │ │ │ │ └── store_test.go │ │ │ ├── postgres │ │ │ │ ├── model.go │ │ │ │ ├── store.go │ │ │ │ └── store_test.go │ │ │ ├── store.go │ │ │ ├── tests │ │ │ │ └── tests.go │ │ │ ├── util.go │ │ │ └── util_test.go │ │ └── storage │ │ │ ├── account.go │ │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ │ ├── store.go │ │ │ └── tests │ │ │ └── tests.go │ ├── deposit │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── estimated.go │ ├── estimated_test.go │ ├── external.go │ ├── external_test.go │ ├── fulfillment │ │ ├── fulfillment.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── intent │ │ ├── intent.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── internal.go │ ├── merkletree │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── merkletree.go │ │ ├── model.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── messaging │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── model_test.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── nonce │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── nonce.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── onramp │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── purchase.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── paymentrequest │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── payment_request.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── provider.go │ ├── rendezvous │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ ├── timelock │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ ├── tests │ │ │ └── tests.go │ │ └── timelock.go │ ├── transaction │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── store.go │ │ ├── tests │ │ │ └── tests.go │ │ └── transaction.go │ ├── vault │ │ ├── encryption.go │ │ ├── key.go │ │ ├── key_test.go │ │ ├── memory │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── postgres │ │ │ ├── model.go │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── secret.go │ │ ├── store.go │ │ └── tests │ │ │ └── tests.go │ └── webhook │ │ ├── memory │ │ ├── store.go │ │ └── store_test.go │ │ ├── postgres │ │ ├── model.go │ │ ├── store.go │ │ └── store_test.go │ │ ├── store.go │ │ ├── tests │ │ └── tests.go │ │ └── webhook.go ├── exchangerate │ ├── time.go │ └── validation.go ├── limit │ └── limits.go ├── server │ ├── account │ │ ├── server.go │ │ └── server_test.go │ ├── currency │ │ └── currency.go │ ├── messaging │ │ ├── client.go │ │ ├── config.go │ │ ├── error.go │ │ ├── internal.go │ │ ├── message_handler.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── stream.go │ │ └── testutil.go │ ├── micropayment │ │ ├── server.go │ │ └── server_test.go │ └── transaction │ │ ├── action_handler.go │ │ ├── airdrop.go │ │ ├── airdrop_test.go │ │ ├── config.go │ │ ├── errors.go │ │ ├── intent.go │ │ ├── intent_handler.go │ │ ├── intent_test.go │ │ ├── limits.go │ │ ├── limits_test.go │ │ ├── local_simulation.go │ │ ├── local_simulation_test.go │ │ ├── metrics.go │ │ ├── onramp.go │ │ ├── onramp_test.go │ │ ├── server.go │ │ ├── swap.go │ │ └── testutil.go ├── thirdparty │ ├── domain.go │ ├── domain_test.go │ ├── encryption_test.go │ ├── message.go │ └── message_test.go ├── transaction │ ├── instruction.go │ ├── nonce.go │ ├── nonce_test.go │ ├── transaction.go │ └── transaction_test.go └── webhook │ ├── execution.go │ ├── execution_test.go │ ├── payload.go │ └── testutil.go ├── config ├── config.go ├── env │ ├── config.go │ └── config_test.go ├── memory │ ├── config.go │ └── config_test.go └── wrapper │ ├── wrappers.go │ └── wrappers_test.go ├── currency ├── client.go ├── coingecko │ └── client.go ├── fixer │ └── client.go └── iso.go ├── database ├── postgres │ ├── client.go │ ├── db.go │ ├── errors.go │ └── test │ │ └── util.go └── query │ ├── cursor.go │ ├── filter.go │ ├── interval.go │ ├── ordering.go │ ├── query.go │ └── utils.go ├── etcd ├── persistent_lease.go ├── persistent_lease_test.go ├── wait.go ├── wait_test.go ├── watch.go └── watch_test.go ├── etcdtest ├── container.go └── container_test.go ├── grpc ├── app │ ├── app.go │ ├── config.go │ ├── loader.go │ └── option.go ├── client │ ├── device.go │ ├── device_test.go │ ├── ip.go │ ├── ip_test.go │ ├── logging.go │ ├── user_agent.go │ ├── user_agent_test.go │ ├── version.go │ └── version_test.go ├── headers │ ├── client_interceptor.go │ ├── headers.go │ ├── headers_context.go │ └── server_interceptor.go ├── metrics │ └── new_relic_server_interceptor.go ├── protobuf │ └── validation │ │ ├── client_interceptor.go │ │ ├── server_interceptor.go │ │ └── test │ │ ├── Makefile │ │ ├── test.pb.go │ │ ├── test.pb.validate.go │ │ ├── test.proto │ │ └── validation_test.go ├── util.go └── util_test.go ├── jupiter └── client.go ├── lock ├── etcd │ ├── lock.go │ └── lock_test.go └── lock.go ├── merkletree ├── tree.go └── tree_test.go ├── metrics ├── constants.go ├── events.go ├── logging.go ├── metrics.go └── tracing.go ├── netutil ├── domain.go ├── ip.go ├── port.go └── url.go ├── osutil └── memory.go ├── pointer └── pointer.go ├── rate ├── limiter.go └── limiter_test.go ├── retry ├── backoff │ ├── backoff.go │ └── backoff_test.go ├── retry.go ├── retry_test.go ├── strategy.go └── strategy_test.go ├── solana ├── address.go ├── address_test.go ├── binary │ └── utils.go ├── client.go ├── client_test.go ├── computebudget │ └── program.go ├── config.go ├── cvm │ ├── accounts_code_vm.go │ ├── accounts_memory_account.go │ ├── accounts_relay.go │ ├── accounts_storage.go │ ├── accounts_unlock_state.go │ ├── address.go │ ├── instructions_compress.go │ ├── instructions_decompress.go │ ├── instructions_exec.go │ ├── instructions_init_memory.go │ ├── instructions_init_nonce.go │ ├── instructions_init_relay.go │ ├── instructions_init_storage.go │ ├── instructions_init_timelock.go │ ├── instructions_init_unlock.go │ ├── instructions_init_vm.go │ ├── instructions_resize_memory.go │ ├── instructions_snapshot.go │ ├── instructions_timelock_deposit.go │ ├── program.go │ ├── transaction.go │ ├── types_account_type.go │ ├── types_code_instruction.go │ ├── types_hash.go │ ├── types_item_state.go │ ├── types_memory_allocator.go │ ├── types_memory_layout.go │ ├── types_memory_version.go │ ├── types_merkle_tree.go │ ├── types_message.go │ ├── types_opcode.go │ ├── types_recent_roots.go │ ├── types_signature.go │ ├── types_slice_allocator.go │ ├── types_timelock_state.go │ ├── types_token_pool.go │ ├── types_virtual_account_type.go │ ├── utils.go │ ├── virtual_accounts.go │ ├── virtual_accounts_durable_nonce.go │ ├── virtual_accounts_relay_account.go │ ├── virtual_accounts_timelock_account.go │ ├── virtual_instructions.go │ ├── virtual_instructions_airdrop.go │ ├── virtual_instructions_conditional_transfer.go │ ├── virtual_instructions_external_relay.go │ ├── virtual_instructions_external_transfer.go │ ├── virtual_instructions_external_withdraw.go │ ├── virtual_instructions_relay.go │ ├── virtual_instructions_transfer.go │ └── virtual_instructions_withdraw.go ├── ed25519 │ └── program.go ├── encoding.go ├── errors.go ├── errors_test.go ├── instruction.go ├── memo │ ├── program.go │ └── program_test.go ├── shortvec │ ├── shortvec.go │ └── shortvec_test.go ├── splitter │ ├── accounts_pool.go │ ├── accounts_proof.go │ ├── address.go │ ├── address_test.go │ ├── errors.go │ ├── instructions_close_proof.go │ ├── instructions_close_token_account.go │ ├── instructions_initialize_pool.go │ ├── instructions_initialize_proof.go │ ├── instructions_open_token_account.go │ ├── instructions_save_recent_root.go │ ├── instructions_transfer_with_commitment.go │ ├── instructions_update_proof.go │ ├── instructions_verify_proof.go │ ├── legacy.go │ ├── legacy_test.go │ ├── program.go │ ├── types.go │ ├── types_hash.go │ ├── types_merkletree.go │ └── utils.go ├── swapvalidator │ ├── address.go │ ├── address_test.go │ ├── errors.go │ ├── instructions_post_swap.go │ ├── instructions_pre_swap.go │ ├── legacy.go │ ├── program.go │ └── utils.go ├── system │ ├── accounts.go │ ├── program.go │ ├── program_test.go │ └── sysvar.go ├── timelock │ ├── legacy_2022 │ │ ├── accounts.go │ │ ├── address.go │ │ ├── address_test.go │ │ ├── constants.go │ │ ├── errors.go │ │ ├── instruction_burndustwithauthority.go │ │ ├── instruction_closeaccounts.go │ │ ├── instruction_deactivate.go │ │ ├── instruction_initialize.go │ │ ├── instruction_revokelockwithauthority.go │ │ ├── instruction_withdraw.go │ │ ├── legacy.go │ │ ├── legacy_test.go │ │ ├── program.go │ │ ├── types.go │ │ └── utils.go │ └── v1 │ │ ├── accounts.go │ │ ├── address.go │ │ ├── address_test.go │ │ ├── constants.go │ │ ├── errors.go │ │ ├── instruction_activate.go │ │ ├── instruction_burndustwithauthority.go │ │ ├── instruction_cancellocktimeout.go │ │ ├── instruction_closeAccounts.go │ │ ├── instruction_deactivate.go │ │ ├── instruction_initialize.go │ │ ├── instruction_revokelockwithauthority.go │ │ ├── instruction_revokelockwithtimeout.go │ │ ├── instruction_transferwithauthority.go │ │ ├── instruction_withdraw.go │ │ ├── legacy.go │ │ ├── legacy_test.go │ │ ├── program.go │ │ ├── types.go │ │ └── utils.go ├── token │ ├── associated.go │ ├── associated_test.go │ ├── client.go │ ├── program.go │ ├── program_test.go │ ├── state.go │ └── state_test.go ├── transaction.go └── transaction_test.go ├── sync ├── ring.go ├── ring_test.go ├── striped_channel.go ├── striped_channel_test.go ├── striped_lock.go └── striped_lock_test.go ├── testutil ├── error.go ├── logging.go ├── server.go ├── solana.go ├── subsidizer.go ├── wait.go └── wait_test.go ├── usdc └── usdc.go ├── usdg └── usdg.go └── usdt └── usdt.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # Build output directories 24 | build/ 25 | 26 | # Visual Studio Code 27 | .vscode/ 28 | 29 | # Jetbrains 30 | .idea/ 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Code Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: test 3 | 4 | .PHONY: test 5 | test: 6 | @go test -cover ./... 7 | 8 | 9 | .PHONY: integration-test 10 | integration-test: 11 | @go test -tags integration -cover -timeout=5m ./... 12 | 13 | 14 | .PHONY: clean-integration-containers 15 | clean-integration-containers: 16 | @echo Removing etcd containers... 17 | @docker ps | grep -E "etcd-test-[0-9a-z]{8}-[0-9]+" | awk '{print $$1}' | xargs docker rm -f 2>/dev/null || true 18 | @echo Removing etcd cluster networks... 19 | @docker network ls | grep -E "etcd-test-[0-9a-z]{8}-network" | awk '{print $$1}' | xargs docker network remove 2>/dev/null || true 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security and Issue Disclosures 2 | 3 | In the interest of protecting the security of our users and their funds, we ask 4 | that if you discover any security vulnerabilities in the Code SDK, the clients, 5 | the packages, the on-chain smart contracts, or the mobile app, you report them 6 | using the following proceedure. Our security team will review your report. Your 7 | cooperation in maintaining the security of our products is appreciated. 8 | 9 | ⚠️ **DO NOT CREATE A GITHUB ISSUE** to report a security problem 10 | 11 | # Security Policy 12 | 13 | 1. [Reporting security problems](#reporting) 14 | 1. [Security Bug Bounties](#bounty) 15 | 1. [Incident Response Process](#process) 16 | 17 | 18 | ## Reporting security problems in the Code Program Library 19 | 20 | Please use this [Report a Vulnerability](https://github.com/stickychin/code-server/security/advisories/new) link. 21 | Provide a helpful title and detailed description of the problem. 22 | 23 | If you haven't done so already, please **enable two-factor auth** in your GitHub account. 24 | 25 | Expect a response as fast as possible in the advisory, typically within 72 hours. 26 | 27 | 28 | ## Bounty 29 | 30 | Code Inc may offer bounties for critical security issues. Either a demonstration 31 | or a valid bug report is all that's necessary to submit a bug bounty. A patch to 32 | fix the issue isn't required. 33 | 34 | 35 | ## Process 36 | 37 | If you do not receive a response in the advisory, send an email to 38 | security@getcode.com with the full URL of the advisory you have created. DO NOT 39 | include attachments or provide detail sufficient for exploitation regarding the 40 | security issue in this email. **Only provide such details in the advisory**. 41 | 42 | If you do not receive a response from security@getcode.com please followup with 43 | the team directly. 44 | -------------------------------------------------------------------------------- /pkg/cluster/doc.go: -------------------------------------------------------------------------------- 1 | // Package cluster provides utilities for multi-server clustering. It allows nodes 2 | // to coordinate a shared membership state, which can in turn be used to build 3 | // higher level features, such as Distributed Nonce Pools and RPC routing. 4 | // 5 | // In general, an application will create a Cluster, using CreateMembership() to 6 | // register the current process in the cluster. The data of each Member is 7 | // arbitrary, and can be used by the higher level application to coordinate state. 8 | // 9 | // Multiple clusters can be created in a given server process. However, multiple 10 | // Cluster instances for the same real cluster should be avoided. 11 | package cluster 12 | -------------------------------------------------------------------------------- /pkg/cluster/grpc/context.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey int 8 | 9 | const ( 10 | contextKeyRouting contextKey = iota 11 | contextKeyNode 12 | ) 13 | 14 | func WithRoutingKey(parent context.Context, val []byte) context.Context { 15 | return context.WithValue(parent, contextKeyRouting, val) 16 | } 17 | 18 | func RoutingKey(ctx context.Context) (val []byte, ok bool) { 19 | val, ok = ctx.Value(contextKeyRouting).([]byte) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /pkg/cluster/memory/memory_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | _ "net/http/pprof" 8 | 9 | "github.com/stickychin/code-server/pkg/cluster" 10 | clustertests "github.com/stickychin/code-server/pkg/cluster/tests" 11 | ) 12 | 13 | func TestMemory(t *testing.T) { 14 | go func() { 15 | http.ListenAndServe(":6060", nil) 16 | }() 17 | 18 | clustertests.RunClusterTests(t, func() (cluster.Cluster, error) { 19 | return NewCluster(), nil 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/code/aml/metrics.go: -------------------------------------------------------------------------------- 1 | package aml 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/metrics" 7 | ) 8 | 9 | const ( 10 | metricsStructName = "aml.guard" 11 | 12 | eventName = "AntiMoneyLaunderingGuardDenial" 13 | ) 14 | 15 | func recordDenialEvent(ctx context.Context, reason string) { 16 | kvPairs := map[string]interface{}{ 17 | "reason": reason, 18 | "count": 1, 19 | } 20 | metrics.RecordEvent(ctx, eventName, kvPairs) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/code/antispam/metrics.go: -------------------------------------------------------------------------------- 1 | package antispam 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/metrics" 7 | ) 8 | 9 | const ( 10 | metricsStructName = "antispam.guard" 11 | 12 | eventName = "AntispamGuardDenial" 13 | 14 | actionOpenAccounts = "OpenAccounts" 15 | actionSendPayment = "SendPayment" 16 | actionReceivePayments = "ReceivePayments" 17 | 18 | actionWelcomeBonus = "WelcomeBonus" 19 | 20 | actionSwap = "Swap" 21 | ) 22 | 23 | func recordDenialEvent(ctx context.Context, action, reason string) { 24 | kvPairs := map[string]interface{}{ 25 | "action": action, 26 | "reason": reason, 27 | "count": 1, 28 | } 29 | metrics.RecordEvent(ctx, eventName, kvPairs) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/code/async/account/config.go: -------------------------------------------------------------------------------- 1 | package async_account 2 | 3 | // todo: setup configs 4 | 5 | const ( 6 | envConfigPrefix = "ACCOUNT_SERVICE_" 7 | ) 8 | 9 | type conf struct { 10 | } 11 | 12 | // ConfigProvider defines how config values are pulled 13 | type ConfigProvider func() *conf 14 | 15 | // WithEnvConfigs returns configuration pulled from environment variables 16 | func WithEnvConfigs() ConfigProvider { 17 | return func() *conf { 18 | return &conf{} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/code/async/account/metrics.go: -------------------------------------------------------------------------------- 1 | package async_account 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stickychin/code-server/pkg/metrics" 8 | ) 9 | 10 | const ( 11 | giftCardWorkerEventName = "GiftCardWorkerPollingCheck" 12 | swapRetryWorkerEventName = "SwapRetryWorkerPollingCheck" 13 | ) 14 | 15 | func (p *service) metricsGaugeWorker(ctx context.Context) error { 16 | delay := time.Second 17 | 18 | for { 19 | select { 20 | case <-ctx.Done(): 21 | return ctx.Err() 22 | case <-time.After(delay): 23 | start := time.Now() 24 | 25 | p.recordBackupQueueStatusPollingEvent(ctx) 26 | 27 | delay = time.Second - time.Since(start) 28 | } 29 | } 30 | } 31 | 32 | func (p *service) recordBackupQueueStatusPollingEvent(ctx context.Context) { 33 | count, err := p.data.GetAccountInfoCountRequiringAutoReturnCheck(ctx) 34 | if err == nil { 35 | metrics.RecordEvent(ctx, giftCardWorkerEventName, map[string]interface{}{ 36 | "queue_size": count, 37 | }) 38 | } 39 | 40 | count, err = p.data.GetAccountInfoCountRequiringSwapRetry(ctx) 41 | if err == nil { 42 | metrics.RecordEvent(ctx, swapRetryWorkerEventName, map[string]interface{}{ 43 | "queue_size": count, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/code/async/account/service.go: -------------------------------------------------------------------------------- 1 | package async_account 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/stickychin/code-server/pkg/code/async" 10 | code_data "github.com/stickychin/code-server/pkg/code/data" 11 | ) 12 | 13 | type service struct { 14 | log *logrus.Entry 15 | conf *conf 16 | data code_data.Provider 17 | } 18 | 19 | func New(data code_data.Provider, configProvider ConfigProvider) async.Service { 20 | return &service{ 21 | log: logrus.StandardLogger().WithField("service", "account"), 22 | conf: configProvider(), 23 | data: data, 24 | } 25 | } 26 | 27 | func (p *service) Start(ctx context.Context, interval time.Duration) error { 28 | 29 | go func() { 30 | err := p.giftCardAutoReturnWorker(ctx, interval) 31 | if err != nil && err != context.Canceled { 32 | p.log.WithError(err).Warn("gift card auto-return processing loop terminated unexpectedly") 33 | } 34 | }() 35 | 36 | // todo: the open code protocol needs to get the push token from the implementing app 37 | /* 38 | go func() { 39 | err := p.swapRetryWorker(ctx, interval) 40 | if err != nil && err != context.Canceled { 41 | p.log.WithError(err).Warn("swap retry processing loop terminated unexpectedly") 42 | } 43 | }() 44 | */ 45 | 46 | go func() { 47 | err := p.metricsGaugeWorker(ctx) 48 | if err != nil && err != context.Canceled { 49 | p.log.WithError(err).Warn("account metrics gauge loop terminated unexpectedly") 50 | } 51 | }() 52 | 53 | select { 54 | case <-ctx.Done(): 55 | return ctx.Err() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/code/async/account/swap_test.go: -------------------------------------------------------------------------------- 1 | package async_account 2 | 3 | // todo: add tests 4 | -------------------------------------------------------------------------------- /pkg/code/async/airdrop/config.go: -------------------------------------------------------------------------------- 1 | package async_airdrop 2 | 3 | import ( 4 | "github.com/stickychin/code-server/pkg/config" 5 | "github.com/stickychin/code-server/pkg/config/env" 6 | ) 7 | 8 | // todo: setup configs 9 | 10 | const ( 11 | envConfigPrefix = "AIRDROP_SERVICE_" 12 | 13 | DisableAirdropsConfigEnvName = envConfigPrefix + "DISABLE_AIRDROPS" 14 | defaultDisableAirdrops = false 15 | 16 | AirdropperOwnerConfigEnvName = envConfigPrefix + "AIRDROPPER_OWNER" 17 | defaultAirdropperOwner = "invalid" // ensure something valid is set 18 | 19 | NonceMemoryAccountConfigEnvName = envConfigPrefix + "NONCE_MEMORY_ACCOUNT" 20 | defaultNonceMemoryAccount = "invalid" // ensure something valid is set 21 | ) 22 | 23 | type conf struct { 24 | disableAirdrops config.Bool 25 | airdropperOwner config.String 26 | nonceMemoryAccount config.String 27 | } 28 | 29 | // ConfigProvider defines how config values are pulled 30 | type ConfigProvider func() *conf 31 | 32 | // WithEnvConfigs returns configuration pulled from environment variables 33 | func WithEnvConfigs() ConfigProvider { 34 | return func() *conf { 35 | return &conf{ 36 | disableAirdrops: env.NewBoolConfig(DisableAirdropsConfigEnvName, defaultDisableAirdrops), 37 | airdropperOwner: env.NewStringConfig(AirdropperOwnerConfigEnvName, defaultAirdropperOwner), 38 | nonceMemoryAccount: env.NewStringConfig(NonceMemoryAccountConfigEnvName, defaultNonceMemoryAccount), 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pkg/code/async/airdrop/indexer.go: -------------------------------------------------------------------------------- 1 | package async_airdrop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | 8 | indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" 9 | 10 | "github.com/stickychin/code-server/pkg/code/common" 11 | ) 12 | 13 | var ( 14 | errNotIndexed = errors.New("virtual account is not indexed") 15 | ) 16 | 17 | func (p *service) getVirtualTimelockAccountMemoryLocation(ctx context.Context, vm, owner *common.Account) (*common.Account, uint16, error) { 18 | resp, err := p.vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{ 19 | VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, 20 | Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()}, 21 | }) 22 | if err != nil { 23 | return nil, 0, err 24 | } else if resp.Result == indexerpb.GetVirtualTimelockAccountsResponse_NOT_FOUND { 25 | return nil, 0, errNotIndexed 26 | } else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK { 27 | return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) 28 | } 29 | 30 | if len(resp.Items) > 1 { 31 | return nil, 0, errors.New("multiple results returned") 32 | } else if resp.Items[0].Storage.GetMemory() == nil { 33 | return nil, 0, errors.New("account is compressed") 34 | } 35 | 36 | protoMemory := resp.Items[0].Storage.GetMemory() 37 | memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value) 38 | if err != nil { 39 | return nil, 0, err 40 | } 41 | return memory, uint16(protoMemory.Index), nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/code/async/airdrop/integration.go: -------------------------------------------------------------------------------- 1 | package async_airdrop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/code/common" 7 | ) 8 | 9 | type Integration interface { 10 | // GetOwnersToAirdropNow gets a set of owner accounts to airdrop right now, 11 | // and the amount that should be airdropped. 12 | GetOwnersToAirdropNow(ctx context.Context) ([]*common.Account, uint64, error) 13 | 14 | // OnSuccess is called when an airdrop completes 15 | OnSuccess(ctx context.Context, owners ...*common.Account) error 16 | } 17 | -------------------------------------------------------------------------------- /pkg/code/async/airdrop/nonce.go: -------------------------------------------------------------------------------- 1 | package async_airdrop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | "github.com/stickychin/code-server/pkg/solana/cvm" 8 | ) 9 | 10 | func (p *service) refreshNonceMemoryAccountState(ctx context.Context) error { 11 | p.nonceMu.Lock() 12 | defer p.nonceMu.Unlock() 13 | 14 | ai, err := p.data.GetBlockchainAccountInfo(ctx, p.nonceMemoryAccount.PublicKey().ToBase58(), solana.CommitmentFinalized) 15 | if err != nil { 16 | return err 17 | } 18 | return p.nonceMemoryAccountState.Unmarshal(ai.Data) 19 | } 20 | 21 | func (p *service) getVdn() (*cvm.VirtualDurableNonce, uint16, error) { 22 | p.nonceMu.Lock() 23 | defer p.nonceMu.Unlock() 24 | 25 | vaData, _ := p.nonceMemoryAccountState.Data.Read(int(p.nextNonceIndex)) 26 | var vdn cvm.VirtualDurableNonce 27 | err := vdn.UnmarshalFromMemory(vaData) 28 | if err != nil { 29 | return nil, 0, err 30 | } 31 | index := p.nextNonceIndex 32 | p.nextNonceIndex = (p.nextNonceIndex + 1) % uint16(len(p.nonceMemoryAccountState.Data.State)) 33 | return &vdn, index, nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/code/async/airdrop/worker.go: -------------------------------------------------------------------------------- 1 | package async_airdrop 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/newrelic/go-agent/v3/newrelic" 8 | 9 | "github.com/stickychin/code-server/pkg/metrics" 10 | ) 11 | 12 | func (p *service) airdropWorker(serviceCtx context.Context, interval time.Duration) error { 13 | delay := time.After(interval) 14 | 15 | for { 16 | select { 17 | case <-delay: 18 | nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) 19 | m := nr.StartTransaction("async__airdrop_service__airdrop") 20 | defer m.End() 21 | tracedCtx := newrelic.NewContext(serviceCtx, m) 22 | 23 | start := time.Now() 24 | 25 | err := p.doAirdrop(tracedCtx) 26 | if err != nil { 27 | m.NoticeError(err) 28 | } 29 | 30 | delay = time.After(interval - time.Since(start)) 31 | case <-serviceCtx.Done(): 32 | return serviceCtx.Err() 33 | } 34 | } 35 | } 36 | 37 | func (p *service) doAirdrop(ctx context.Context) error { 38 | log := p.log.WithField("method", "doAirdrop") 39 | 40 | err := p.refreshNonceMemoryAccountState(ctx) 41 | if err != nil { 42 | log.WithError(err).Warn("failed to refresh nonce memory account state") 43 | return err 44 | } 45 | 46 | owners, amount, err := p.integration.GetOwnersToAirdropNow(ctx) 47 | if err != nil { 48 | log.WithError(err).Warn("failed to get owners to airdrop to") 49 | return err 50 | } 51 | 52 | if len(owners) == 0 { 53 | return nil 54 | } 55 | 56 | err = p.airdropToOwners(ctx, amount, owners...) 57 | if err != nil { 58 | log.WithError(err).Warn("failed to airdrop to owners") 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/code/async/geyser/api/Makefile: -------------------------------------------------------------------------------- 1 | all: generate 2 | 3 | generate: 4 | docker run --rm -v $(PWD)/proto:/proto -v $(PWD)/gen:/genproto code-protobuf-api-builder-go 5 | 6 | .PHONY: all generate -------------------------------------------------------------------------------- /pkg/code/async/geyser/handler_test.go: -------------------------------------------------------------------------------- 1 | package async_geyser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // todo: implement me 8 | func TestTokenProgramAccountHandler(t *testing.T) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /pkg/code/async/geyser/retry.go: -------------------------------------------------------------------------------- 1 | package async_geyser 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/stickychin/code-server/pkg/retry" 9 | "github.com/stickychin/code-server/pkg/retry/backoff" 10 | ) 11 | 12 | var ( 13 | errSignatureNotConfirmed = errors.New("signature is not confirmed") 14 | errSignatureNotFinalized = errors.New("signature is not finalized") 15 | ) 16 | 17 | var waitForFinalizationRetryStrategies = []retry.Strategy{ 18 | retry.NonRetriableErrors(context.Canceled), 19 | retry.Limit(30), 20 | retry.Backoff(backoff.Constant(3*time.Second), 3*time.Second), 21 | } 22 | 23 | var waitForConfirmationRetryStrategies = []retry.Strategy{ 24 | retry.NonRetriableErrors(context.Canceled), 25 | retry.Limit(10), 26 | retry.Backoff(backoff.Constant(time.Second), time.Second), 27 | } 28 | -------------------------------------------------------------------------------- /pkg/code/async/geyser/util.go: -------------------------------------------------------------------------------- 1 | package async_geyser 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/stickychin/code-server/pkg/cache" 9 | "github.com/stickychin/code-server/pkg/code/common" 10 | code_data "github.com/stickychin/code-server/pkg/code/data" 11 | "github.com/stickychin/code-server/pkg/code/data/timelock" 12 | ) 13 | 14 | var ( 15 | depositPdaToUserAuthorityCache = cache.NewCache(1_000_000) 16 | ) 17 | 18 | // todo: use a bloom filter, but a caching strategy might be ok for now 19 | func testForKnownUserAuthorityFromDepositPda(ctx context.Context, data code_data.Provider, depositPdaAccount *common.Account) (bool, *common.Account, error) { 20 | cached, ok := depositPdaToUserAuthorityCache.Retrieve(depositPdaAccount.PublicKey().ToBase58()) 21 | if ok { 22 | userAuthorityAccountPublicKeyString := cached.(string) 23 | if len(userAuthorityAccountPublicKeyString) > 0 { 24 | userAuthorityAccount, _ := common.NewAccountFromPublicKeyString(userAuthorityAccountPublicKeyString) 25 | return true, userAuthorityAccount, nil 26 | } 27 | return false, nil, nil 28 | } 29 | 30 | timelockRecord, err := data.GetTimelockByDepositPda(ctx, depositPdaAccount.PublicKey().ToBase58()) 31 | switch err { 32 | case timelock.ErrTimelockNotFound: 33 | depositPdaToUserAuthorityCache.Insert(depositPdaAccount.PublicKey().ToBase58(), "", 1) 34 | return false, nil, nil 35 | case nil: 36 | userAuthorityAccount, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) 37 | if err != nil { 38 | return false, nil, errors.New("invalid vault owner account") 39 | } 40 | depositPdaToUserAuthorityCache.Insert(depositPdaAccount.PublicKey().ToBase58(), userAuthorityAccount.PublicKey().ToBase58(), 1) 41 | return true, userAuthorityAccount, nil 42 | default: 43 | return false, nil, errors.Wrap(err, "error getting timelock record") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/code/async/nonce/config.go: -------------------------------------------------------------------------------- 1 | package async_nonce 2 | 3 | import ( 4 | "github.com/stickychin/code-server/pkg/config" 5 | "github.com/stickychin/code-server/pkg/config/env" 6 | ) 7 | 8 | const ( 9 | envConfigPrefix = "NONCE_SERVICE_" 10 | 11 | solanaMainnetNoncePubkeyPrefixConfigEnvName = envConfigPrefix + "SOLANA_MAINNET_NONCE_PUBKEY_PREFIX" 12 | defaultSolanaMainnetNoncePubkeyPrefix = "non" 13 | 14 | solanaMainnetNoncePoolSizeConfigEnvName = envConfigPrefix + "SOLANA_MAINNET_NONCE_POOL_SIZE" 15 | defaultSolanaMainnetNoncePoolSize = 1000 16 | ) 17 | 18 | type conf struct { 19 | solanaMainnetNoncePubkeyPrefix config.String 20 | solanaMainnetNoncePoolSize config.Uint64 21 | } 22 | 23 | // ConfigProvider defines how config values are pulled 24 | type ConfigProvider func() *conf 25 | 26 | // WithEnvConfigs returns configuration pulled from environment variables 27 | func WithEnvConfigs() ConfigProvider { 28 | return func() *conf { 29 | return &conf{ 30 | solanaMainnetNoncePubkeyPrefix: env.NewStringConfig(solanaMainnetNoncePubkeyPrefixConfigEnvName, defaultSolanaMainnetNoncePubkeyPrefix), 31 | solanaMainnetNoncePoolSize: env.NewUint64Config(solanaMainnetNoncePoolSizeConfigEnvName, defaultSolanaMainnetNoncePoolSize), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/code/async/nonce/pool_test.go: -------------------------------------------------------------------------------- 1 | package async_nonce 2 | -------------------------------------------------------------------------------- /pkg/code/async/sequencer/timelock.go: -------------------------------------------------------------------------------- 1 | package async_sequencer 2 | 3 | import ( 4 | "context" 5 | 6 | code_data "github.com/stickychin/code-server/pkg/code/data" 7 | "github.com/stickychin/code-server/pkg/code/data/timelock" 8 | timelock_token_v1 "github.com/stickychin/code-server/pkg/solana/timelock/v1" 9 | ) 10 | 11 | // The faster we can update timelock state, the better it is to unblock scheduling. 12 | // Particularly, we don't want a missed Geyser account update to block scheduling. 13 | // Generally, these are very safe if they're used when we first create and close 14 | // accounts, because they denote the initial and end states. 15 | 16 | func markTimelockLocked(ctx context.Context, data code_data.Provider, vault string, slot uint64) error { 17 | record, err := data.GetTimelockByVault(ctx, vault) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | record.VaultState = timelock_token_v1.StateLocked 23 | record.Block = slot 24 | 25 | err = data.SaveTimelock(ctx, record) 26 | if err == timelock.ErrStaleTimelockState { 27 | return nil 28 | } 29 | return err 30 | } 31 | 32 | func markTimelockClosed(ctx context.Context, data code_data.Provider, vault string, slot uint64) error { 33 | record, err := data.GetTimelockByVault(ctx, vault) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | record.VaultState = timelock_token_v1.StateClosed 39 | if record.Block > slot { 40 | // Potential conflict with unlock state detection, force a move to close at the next block 41 | // 42 | // todo: Better way of handling this 43 | record.Block += 1 44 | } else { 45 | record.Block = slot 46 | } 47 | 48 | return data.SaveTimelock(ctx, record) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/code/async/service.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Service interface { 9 | Start(ctx context.Context, interval time.Duration) error 10 | } 11 | -------------------------------------------------------------------------------- /pkg/code/async/vm/service.go: -------------------------------------------------------------------------------- 1 | package async_vm 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/stickychin/code-server/pkg/code/async" 10 | code_data "github.com/stickychin/code-server/pkg/code/data" 11 | ) 12 | 13 | type service struct { 14 | log *logrus.Entry 15 | data code_data.Provider 16 | } 17 | 18 | func New(data code_data.Provider) async.Service { 19 | return &service{ 20 | log: logrus.StandardLogger().WithField("service", "vm"), 21 | data: data, 22 | } 23 | } 24 | 25 | func (p *service) Start(ctx context.Context, interval time.Duration) error { 26 | go func() { 27 | err := p.storageInitWorker(ctx, interval) 28 | if err != nil && err != context.Canceled { 29 | p.log.WithError(err).Warn("storage init processing loop terminated unexpectedly") 30 | } 31 | }() 32 | 33 | select { 34 | case <-ctx.Done(): 35 | return ctx.Err() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/code/async/webhook/config.go: -------------------------------------------------------------------------------- 1 | package async_webhook 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stickychin/code-server/pkg/config" 7 | "github.com/stickychin/code-server/pkg/config/env" 8 | "github.com/stickychin/code-server/pkg/config/memory" 9 | "github.com/stickychin/code-server/pkg/config/wrapper" 10 | ) 11 | 12 | const ( 13 | envConfigPrefix = "WEBHOOK_SERVICE_" 14 | 15 | WorkerBatchSizeConfigEnvName = envConfigPrefix + "WORKER_BATCH_SIZE" 16 | defaultWorkerBatchSize = 250 17 | 18 | WebhookTimeoutConfigEnvName = envConfigPrefix + "WEBHOOK_TIMEOUT" 19 | defaultWebhookTimeout = 3 * time.Second 20 | ) 21 | 22 | type conf struct { 23 | workerBatchSize config.Uint64 24 | webhookTimeout config.Duration 25 | } 26 | 27 | // ConfigProvider defines how config values are pulled 28 | type ConfigProvider func() *conf 29 | 30 | // WithEnvConfigs returns configuration pulled from environment variables 31 | func WithEnvConfigs() ConfigProvider { 32 | return func() *conf { 33 | return &conf{ 34 | workerBatchSize: env.NewUint64Config(WorkerBatchSizeConfigEnvName, defaultWorkerBatchSize), 35 | webhookTimeout: env.NewDurationConfig(WebhookTimeoutConfigEnvName, defaultWebhookTimeout), 36 | } 37 | } 38 | } 39 | 40 | type testOverrides struct { 41 | } 42 | 43 | func withManualTestOverrides(overrides *testOverrides) ConfigProvider { 44 | return func() *conf { 45 | return &conf{ 46 | workerBatchSize: wrapper.NewUint64Config(memory.NewConfig(defaultWorkerBatchSize), defaultWorkerBatchSize), 47 | webhookTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultWebhookTimeout), defaultWebhookTimeout), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/code/async/webhook/metrics.go: -------------------------------------------------------------------------------- 1 | package async_webhook 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stickychin/code-server/pkg/metrics" 8 | "github.com/stickychin/code-server/pkg/code/data/webhook" 9 | ) 10 | 11 | const ( 12 | webhookCountEventName = "WebhookCountPollingCheck" 13 | webhookCallsEventName = "WebhookCallsPollingCheck" 14 | ) 15 | 16 | func (p *service) metricsGaugeWorker(ctx context.Context) error { 17 | delay := time.Second 18 | 19 | for { 20 | select { 21 | case <-ctx.Done(): 22 | return ctx.Err() 23 | case <-time.After(delay): 24 | start := time.Now() 25 | 26 | p.recordWebhookCountEvent(ctx, webhook.StatePending) 27 | p.recordWebhookCallsEvents(ctx) 28 | 29 | delay = time.Second - time.Since(start) 30 | } 31 | } 32 | } 33 | 34 | func (p *service) recordWebhookCountEvent(ctx context.Context, state webhook.State) { 35 | count, err := p.data.CountWebhookByState(ctx, state) 36 | if err != nil { 37 | return 38 | } 39 | 40 | metrics.RecordEvent(ctx, webhookCountEventName, map[string]interface{}{ 41 | "count": count, 42 | "state": state.String(), 43 | }) 44 | } 45 | 46 | func (p *service) recordWebhookCallsEvents(ctx context.Context) { 47 | p.metricsMu.Lock() 48 | successfulCalls := p.successfulWebhooks 49 | failedCalls := p.failedWebhooks 50 | p.successfulWebhooks = 0 51 | p.failedWebhooks = 0 52 | p.metricsMu.Unlock() 53 | 54 | metrics.RecordEvent(ctx, webhookCallsEventName, map[string]interface{}{ 55 | "successes": successfulCalls, 56 | "failures": failedCalls, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/code/async/webhook/service.go: -------------------------------------------------------------------------------- 1 | package async_webhook 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | 10 | "github.com/stickychin/code-server/pkg/code/async" 11 | code_data "github.com/stickychin/code-server/pkg/code/data" 12 | "github.com/stickychin/code-server/pkg/code/server/messaging" 13 | sync_util "github.com/stickychin/code-server/pkg/sync" 14 | ) 15 | 16 | type service struct { 17 | log *logrus.Entry 18 | conf *conf 19 | data code_data.Provider 20 | messagingClient messaging.InternalMessageClient 21 | webhookLocks *sync_util.StripedLock // todo: distributed lock 22 | 23 | metricsMu sync.Mutex 24 | successfulWebhooks int 25 | failedWebhooks int 26 | } 27 | 28 | func New(data code_data.Provider, messagingClient messaging.InternalMessageClient, configProvider ConfigProvider) async.Service { 29 | return &service{ 30 | log: logrus.StandardLogger().WithField("service", "webhook"), 31 | conf: configProvider(), 32 | data: data, 33 | messagingClient: messagingClient, 34 | webhookLocks: sync_util.NewStripedLock(1024), 35 | } 36 | } 37 | 38 | func (p *service) Start(ctx context.Context, interval time.Duration) error { 39 | go func() { 40 | err := p.worker(ctx, interval) 41 | if err != nil && err != context.Canceled { 42 | p.log.WithError(err).Warnf("webhook processing loop terminated unexpectedly") 43 | } 44 | }() 45 | 46 | go func() { 47 | err := p.metricsGaugeWorker(ctx) 48 | if err != nil && err != context.Canceled { 49 | p.log.WithError(err).Warn("webhook metrics gauge loop terminated unexpectedly") 50 | } 51 | }() 52 | 53 | select { 54 | case <-ctx.Done(): 55 | return ctx.Err() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/code/common/key.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/mr-tron/base58" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Key struct { 11 | bytesValue []byte 12 | stringValue string 13 | } 14 | 15 | func NewKeyFromBytes(value []byte) (*Key, error) { 16 | k := &Key{ 17 | bytesValue: value, 18 | stringValue: base58.Encode(value), 19 | } 20 | 21 | if err := k.Validate(); err != nil { 22 | return nil, err 23 | } 24 | return k, nil 25 | } 26 | 27 | func NewKeyFromString(value string) (*Key, error) { 28 | bytesValue, err := base58.Decode(value) 29 | if err != nil { 30 | return nil, errors.Wrap(err, "error decoding string as base58") 31 | } 32 | 33 | k := &Key{ 34 | bytesValue: bytesValue, 35 | stringValue: value, 36 | } 37 | 38 | if err := k.Validate(); err != nil { 39 | return nil, err 40 | } 41 | return k, nil 42 | } 43 | 44 | func NewRandomKey() (*Key, error) { 45 | _, privateKeyBytes, err := ed25519.GenerateKey(nil) 46 | if err != nil { 47 | return nil, errors.Wrap(err, "error generating private key") 48 | } 49 | 50 | return NewKeyFromBytes(privateKeyBytes) 51 | } 52 | 53 | func (k *Key) ToBytes() []byte { 54 | return k.bytesValue 55 | } 56 | 57 | func (k *Key) ToBase58() string { 58 | return k.stringValue 59 | } 60 | 61 | func (k *Key) IsPublic() bool { 62 | return len(k.bytesValue) != ed25519.PrivateKeySize 63 | } 64 | 65 | func (k *Key) Validate() error { 66 | if k == nil { 67 | return errors.New("key is nil") 68 | } 69 | 70 | if len(k.bytesValue) != ed25519.PublicKeySize && len(k.bytesValue) != ed25519.PrivateKeySize { 71 | return errors.New("key must be an ed25519 public or private key") 72 | } 73 | 74 | if base58.Encode(k.bytesValue) != k.stringValue { 75 | return errors.New("bytes and string representation don't match") 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/code/common/key_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/mr-tron/base58" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPublicKey(t *testing.T) { 13 | publicKey, _, err := ed25519.GenerateKey(nil) 14 | require.NoError(t, err) 15 | 16 | var keys []*Key 17 | 18 | key, err := NewKeyFromBytes(publicKey) 19 | require.NoError(t, err) 20 | keys = append(keys, key) 21 | 22 | key, err = NewKeyFromString(base58.Encode(publicKey)) 23 | require.NoError(t, err) 24 | keys = append(keys, key) 25 | 26 | for _, key := range keys { 27 | assert.True(t, key.IsPublic()) 28 | assert.EqualValues(t, publicKey, key.ToBytes()) 29 | assert.Equal(t, base58.Encode(publicKey), key.ToBase58()) 30 | } 31 | } 32 | 33 | func TestPrivateKey(t *testing.T) { 34 | _, privateKey, err := ed25519.GenerateKey(nil) 35 | require.NoError(t, err) 36 | 37 | var keys []*Key 38 | 39 | key, err := NewKeyFromBytes(privateKey) 40 | require.NoError(t, err) 41 | keys = append(keys, key) 42 | 43 | key, err = NewKeyFromString(base58.Encode(privateKey)) 44 | require.NoError(t, err) 45 | keys = append(keys, key) 46 | 47 | for _, key := range keys { 48 | assert.False(t, key.IsPublic()) 49 | assert.EqualValues(t, privateKey, key.ToBytes()) 50 | assert.Equal(t, base58.Encode(privateKey), key.ToBase58()) 51 | } 52 | } 53 | 54 | func TestInvalidKey(t *testing.T) { 55 | stringValue := "invalid-key" 56 | bytesValue := []byte(stringValue) 57 | 58 | _, err := NewKeyFromString(stringValue) 59 | assert.Error(t, err) 60 | 61 | _, err = NewKeyFromBytes(bytesValue) 62 | assert.Error(t, err) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/code/common/mint_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStrToQuarks_HappyPath(t *testing.T) { 11 | for _, tc := range []struct { 12 | input string 13 | expected int64 14 | }{ 15 | {"123.456", 123456000}, 16 | {"123", 123000000}, 17 | {"0.456", 456000}, 18 | {"1234567890123.123456", 1234567890123123456}, 19 | } { 20 | quarks, err := StrToQuarks(tc.input) 21 | require.NoError(t, err) 22 | assert.Equal(t, tc.expected, quarks) 23 | } 24 | } 25 | 26 | func TestStrToQuarks_InvalidString(t *testing.T) { 27 | for _, tc := range []string{ 28 | "", 29 | "abc", 30 | "1.1.1", 31 | "0.1234567", 32 | "12345678901234", 33 | } { 34 | _, err := StrToQuarks(tc) 35 | assert.Error(t, err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/code/common/testutil.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // Required because we'd have a dependency loop with the testutil package 10 | func newRandomTestAccount(t *testing.T) *Account { 11 | account, err := NewRandomAccount() 12 | require.NoError(t, err) 13 | return account 14 | } 15 | -------------------------------------------------------------------------------- /pkg/code/common/vm.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/stickychin/code-server/pkg/code/config" 4 | 5 | var ( 6 | // The well-known Code VM instance 7 | CodeVmAccount, _ = NewAccountFromPublicKeyString(config.VmAccountPublicKey) 8 | 9 | // The well-known Code VM instance omnibus account 10 | CodeVmOmnibusAccount, _ = NewAccountFromPublicKeyString(config.VmOmnibusPublicKey) 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/code/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/mr-tron/base58" 5 | 6 | currency_lib "github.com/stickychin/code-server/pkg/currency" 7 | "github.com/stickychin/code-server/pkg/usdc" 8 | ) 9 | 10 | // todo: more things can be pulled into here to configure the open code protocol 11 | // todo: make these environment configs 12 | 13 | const ( 14 | // Random values. Replace with real mint configuration 15 | CoreMintPublicKeyString = "DWYE8SQkpestTvpCxGNTCRjC2E9Kn6TCnu2SxkddrEEU" 16 | CoreMintQuarksPerUnit = uint64(usdc.QuarksPerUsdc) 17 | CoreMintSymbol = currency_lib.USDC 18 | CoreMintDecimals = usdc.Decimals 19 | 20 | // Random value. Replace with real subsidizer public keys 21 | SubsidizerPublicKey = "84ydcM4Yp59W6aZP6eSaKiAMaKidNLfb5k318sT2pm14" 22 | 23 | // Random value. Replace with real VM public keys 24 | VmAccountPublicKey = "BVMGLfRgr3nVFCH5DuW6VR2kfSDxq4EFEopXfwCDpYzb" 25 | VmOmnibusPublicKey = "GNw1t85VH8b1CcwB5933KBC7PboDPJ5EcQdGynbfN1Pb" 26 | ) 27 | 28 | var ( 29 | CoreMintPublicKeyBytes []byte 30 | ) 31 | 32 | func init() { 33 | decoded, err := base58.Decode(CoreMintPublicKeyString) 34 | if err != nil { 35 | panic(err) 36 | } 37 | CoreMintPublicKeyBytes = decoded 38 | } 39 | -------------------------------------------------------------------------------- /pkg/code/data/account/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/account/tests" 7 | ) 8 | 9 | func TestAccountInfoMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/action/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/action/tests" 7 | ) 8 | 9 | func TestActionMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/balance/checkpoint.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Note: Only supports external balances 9 | type Record struct { 10 | Id uint64 11 | 12 | TokenAccount string 13 | Quarks uint64 14 | SlotCheckpoint uint64 15 | 16 | LastUpdatedAt time.Time 17 | } 18 | 19 | func (r *Record) Validate() error { 20 | if len(r.TokenAccount) == 0 { 21 | return errors.New("token account is required") 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (r *Record) Clone() Record { 28 | return Record{ 29 | Id: r.Id, 30 | 31 | TokenAccount: r.TokenAccount, 32 | Quarks: r.Quarks, 33 | SlotCheckpoint: r.SlotCheckpoint, 34 | 35 | LastUpdatedAt: r.LastUpdatedAt, 36 | } 37 | } 38 | 39 | func (r *Record) CopyTo(dst *Record) { 40 | dst.Id = r.Id 41 | 42 | dst.TokenAccount = r.TokenAccount 43 | dst.Quarks = r.Quarks 44 | dst.SlotCheckpoint = r.SlotCheckpoint 45 | 46 | dst.LastUpdatedAt = r.LastUpdatedAt 47 | } 48 | -------------------------------------------------------------------------------- /pkg/code/data/balance/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/balance/tests" 7 | ) 8 | 9 | func TestBalanceMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/balance/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/balance" 10 | ) 11 | 12 | type store struct { 13 | db *sqlx.DB 14 | } 15 | 16 | // New returns a new postgres balance.Store 17 | func New(db *sql.DB) balance.Store { 18 | return &store{ 19 | db: sqlx.NewDb(db, "pgx"), 20 | } 21 | } 22 | 23 | // SaveCheckpoint implements balance.Store.SaveCheckpoint 24 | func (s *store) SaveCheckpoint(ctx context.Context, record *balance.Record) error { 25 | model, err := toModel(record) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | if err := model.dbSave(ctx, s.db); err != nil { 31 | return err 32 | } 33 | 34 | res := fromModel(model) 35 | res.CopyTo(record) 36 | 37 | return nil 38 | } 39 | 40 | // GetCheckpoint implements balance.Store.GetCheckpoint 41 | func (s *store) GetCheckpoint(ctx context.Context, account string) (*balance.Record, error) { 42 | model, err := dbGetCheckpoint(ctx, s.db, account) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return fromModel(model), nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/code/data/balance/store.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrCheckpointNotFound = errors.New("checkpoint not found") 10 | 11 | ErrStaleCheckpoint = errors.New("checkpoint is stale") 12 | ) 13 | 14 | type Store interface { 15 | // SaveCheckpoint saves a balance at a checkpoint. ErrStaleCheckpoint is returned 16 | // if the checkpoint is outdated 17 | SaveCheckpoint(ctx context.Context, record *Record) error 18 | 19 | // GetCheckpoint gets a balance checkpoint for a given account. ErrCheckpointNotFound 20 | // is returend if no DB record exists. 21 | GetCheckpoint(ctx context.Context, account string) (*Record, error) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/code/data/config.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/stickychin/code-server/pkg/config" 5 | "github.com/stickychin/code-server/pkg/config/env" 6 | ) 7 | 8 | const ( 9 | FixerApiKeyConfigEnvName = "FIXER_API_KEY" 10 | defaultFixerApiKey = "" 11 | ) 12 | 13 | // todo: Add other data store configs here (eg. postgres, solana, etc). 14 | type conf struct { 15 | fixerApiKey config.String 16 | } 17 | 18 | // ConfigProvider defines how config values are pulled 19 | // 20 | // todo: Possibly introduce an override config provider, for things like scripts. 21 | type ConfigProvider func() *conf 22 | 23 | // WithEnvConfigs returns configuration pulled from environment variables 24 | func WithEnvConfigs() ConfigProvider { 25 | return func() *conf { 26 | return &conf{ 27 | fixerApiKey: env.NewStringConfig(FixerApiKeyConfigEnvName, defaultFixerApiKey), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/code/data/currency/util/util.go: -------------------------------------------------------------------------------- 1 | package currencyutil 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/stickychin/code-server/pkg/code/data/currency" 8 | ) 9 | 10 | func PrintTable(records []*currency.ExchangeRateRecord) { 11 | fmt.Println("|index|symbol|time|rate|") 12 | for _, item := range records { 13 | fmt.Printf("|%d|%s|%s|%f|\n", 14 | item.Id, 15 | item.Symbol, 16 | item.Time.Format(time.RFC3339), 17 | item.Rate, 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/ram/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/cvm/ram/tests" 7 | ) 8 | 9 | func TestVmRamMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/ram/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/cvm/ram" 10 | "github.com/stickychin/code-server/pkg/solana/cvm" 11 | ) 12 | 13 | type store struct { 14 | db *sqlx.DB 15 | } 16 | 17 | // New returns a new postgres cvm.ram.Store 18 | func New(db *sql.DB) ram.Store { 19 | return &store{ 20 | db: sqlx.NewDb(db, "pgx"), 21 | } 22 | } 23 | 24 | // InitializeMemory implements cvm.ram.Store.InitializeMemory 25 | func (s *store) InitializeMemory(ctx context.Context, record *ram.Record) error { 26 | model, err := toAccountModel(record) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | err = model.dbInitialize(ctx, s.db) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | res := fromAccountModel(model) 37 | res.CopyTo(record) 38 | 39 | return nil 40 | } 41 | 42 | // FreeMemoryByIndex implements cvm.ram.Store.FreeMemoryByIndex 43 | func (s *store) FreeMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error { 44 | return dbFreeMemoryByIndex(ctx, s.db, memoryAccount, index) 45 | } 46 | 47 | // FreeMemoryByAddress implements cvm.ram.Store.FreeMemoryByAddress 48 | func (s *store) FreeMemoryByAddress(ctx context.Context, address string) error { 49 | return dbFreeMemoryByAddress(ctx, s.db, address) 50 | } 51 | 52 | // ReserveMemory implements cvm.ram.Store.ReserveMemory 53 | func (s *store) ReserveMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { 54 | return dbReserveMemory(ctx, s.db, vm, accountType, address) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/ram/store.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/stickychin/code-server/pkg/solana/cvm" 8 | ) 9 | 10 | var ( 11 | ErrAlreadyInitialized = errors.New("memory account already initalized") 12 | ErrNoFreeMemory = errors.New("no available free memory") 13 | ErrNotReserved = errors.New("memory is not reserved") 14 | ErrAddressAlreadyReserved = errors.New("virtual account address already in memory") 15 | ) 16 | 17 | // Store implements a basic construct for managing RAM memory. For simplicity, 18 | // it is assumed that each memory account will store a single account type, 19 | // which eliminates any complexities with parallel transaction execution resulting 20 | // in allocation errors due to free pages across sectors. 21 | // 22 | // Note: A lock outside this implementation is required to resolve any races. 23 | type Store interface { 24 | // Initializes a memory account for management 25 | InitializeMemory(ctx context.Context, record *Record) error 26 | 27 | // FreeMemoryByIndex frees a piece of memory from a memory account at a particual index 28 | FreeMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error 29 | 30 | // FreeMemoryByAddress frees a piece of memory assigned to the virtual account address 31 | FreeMemoryByAddress(ctx context.Context, address string) error 32 | 33 | // ReserveMemory reserves a piece of memory in a VM for the virtual account address 34 | ReserveMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/ram/util.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/stickychin/code-server/pkg/solana/cvm" 7 | ) 8 | 9 | func GetActualCapcity(record *Record) uint16 { 10 | sizeInMemory := int(cvm.GetVirtualAccountSizeInMemory(record.StoredAccountType)) 11 | pagesPerAccount := math.Ceil(1 / (float64(record.PageSize) / float64(sizeInMemory))) 12 | availablePerSector := int(record.NumPages) / int(pagesPerAccount) 13 | maxAvailableAcrossSectors := uint16(record.NumSectors) * uint16(availablePerSector) 14 | if record.Capacity < maxAvailableAcrossSectors { 15 | return record.Capacity 16 | } 17 | return maxAvailableAcrossSectors 18 | } 19 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/ram/util_test.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/stickychin/code-server/pkg/solana/cvm" 9 | ) 10 | 11 | func TestGetActualCapcity(t *testing.T) { 12 | for _, tc := range []struct { 13 | capacity uint16 14 | numSectors uint16 15 | numPages uint16 16 | pageSize uint8 17 | expected uint16 18 | }{ 19 | { 20 | capacity: 1000, 21 | numSectors: 2, 22 | numPages: 50, 23 | pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)), 24 | expected: 100, 25 | }, 26 | { 27 | capacity: 10, 28 | numSectors: 2, 29 | numPages: 50, 30 | pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)), 31 | expected: 10, 32 | }, 33 | { 34 | capacity: 1000, 35 | numSectors: 2, 36 | numPages: 50, 37 | pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)) - 1, 38 | expected: 50, 39 | }, 40 | { 41 | capacity: 1000, 42 | numSectors: 2, 43 | numPages: 50, 44 | pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)) / 3, 45 | expected: 24, 46 | }, 47 | } { 48 | record := &Record{ 49 | Capacity: tc.capacity, 50 | NumSectors: tc.numSectors, 51 | NumPages: tc.numPages, 52 | PageSize: tc.pageSize, 53 | StoredAccountType: cvm.VirtualAccountTypeTimelock, 54 | } 55 | actual := GetActualCapcity(record) 56 | assert.Equal(t, tc.expected, actual) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/storage/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/cvm/storage/tests" 7 | ) 8 | 9 | func TestVmStorageMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/storage/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/cvm/storage" 10 | ) 11 | 12 | type store struct { 13 | db *sqlx.DB 14 | } 15 | 16 | // New returns a new postgres vm.storage.Store 17 | func New(db *sql.DB) storage.Store { 18 | return &store{ 19 | db: sqlx.NewDb(db, "pgx"), 20 | } 21 | } 22 | 23 | // InitializeStorage implements vm.storage.Store.InitializeStorage 24 | func (s *store) InitializeStorage(ctx context.Context, record *storage.Record) error { 25 | if record.AvailableCapacity != storage.GetMaxCapacity(record.Levels) { 26 | return storage.ErrInvalidInitialCapacity 27 | } 28 | 29 | model, err := toAccountModel(record) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = model.dbInitialize(ctx, s.db) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fromAccountModel(model).CopyTo(record) 40 | 41 | return nil 42 | } 43 | 44 | // FindAnyWithAvailableCapacity implements cvm.storage.Store.FindAnyWithAvailableCapacity 45 | func (s *store) FindAnyWithAvailableCapacity(ctx context.Context, vm string, purpose storage.Purpose, minCapacity uint64) (*storage.Record, error) { 46 | model, err := dbFindAnyWithAvailableCapacity(ctx, s.db, vm, purpose, minCapacity) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return fromAccountModel(model), nil 51 | } 52 | 53 | // ReserveStorage implements cvm.storage.Store.ReserveStorage 54 | func (s *store) ReserveStorage(ctx context.Context, vm string, purpose storage.Purpose, address string) (string, error) { 55 | return dbReserveStorage(ctx, s.db, vm, purpose, address) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/code/data/cvm/storage/store.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrAddressAlreadyReserved = errors.New("virtual account address already in storage") 10 | ErrAlreadyInitialized = errors.New("storage account already initalized") 11 | ErrInvalidInitialCapacity = errors.New("available capacity must be maximum when initializing storage") 12 | ErrNoFreeStorage = errors.New("no available free storage") 13 | ErrNotFound = errors.New("no storage accounts found") 14 | ) 15 | 16 | // Store implements a basic construct for managing compression storage. 17 | // 18 | // Note: A lock outside this implementation is required to resolve any races. 19 | type Store interface { 20 | // Initializes a VM storage account for management 21 | InitializeStorage(ctx context.Context, record *Record) error 22 | 23 | // FindAnyWithAvailableCapacity finds a VM storage account with minimum available capcity 24 | FindAnyWithAvailableCapacity(ctx context.Context, vm string, purpose Purpose, minCapacity uint64) (*Record, error) 25 | 26 | // ReserveStorage reserves a piece of storage in a VM for the virtual account address 27 | ReserveStorage(ctx context.Context, vm string, purpose Purpose, address string) (string, error) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/code/data/deposit/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/deposit/tests" 7 | ) 8 | 9 | func TestDepositMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/deposit/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/deposit" 10 | ) 11 | 12 | type store struct { 13 | db *sqlx.DB 14 | } 15 | 16 | // New returns a new postgres deposit.Store 17 | func New(db *sql.DB) deposit.Store { 18 | return &store{ 19 | db: sqlx.NewDb(db, "pgx"), 20 | } 21 | } 22 | 23 | // Save implements deposit.Store.Save 24 | func (s *store) Save(ctx context.Context, record *deposit.Record) error { 25 | model, err := toModel(record) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | err = model.dbSave(ctx, s.db) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | res := fromModel(model) 36 | res.CopyTo(record) 37 | 38 | return nil 39 | } 40 | 41 | // Get implements deposit.Store.Get 42 | func (s *store) Get(ctx context.Context, signature, account string) (*deposit.Record, error) { 43 | model, err := dbGet(ctx, s.db, signature, account) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return fromModel(model), nil 48 | } 49 | 50 | // GetQuarkAmount implements deposit.Store.GetQuarkAmount 51 | func (s *store) GetQuarkAmount(ctx context.Context, account string) (uint64, error) { 52 | return dbGetQuarkAmount(ctx, s.db, account) 53 | } 54 | 55 | // GetQuarkAmountBatch implements deposit.Store.GetQuarkAmountBatch 56 | func (s *store) GetQuarkAmountBatch(ctx context.Context, accounts ...string) (map[string]uint64, error) { 57 | return dbGetQuarkAmountBatch(ctx, s.db, accounts...) 58 | } 59 | 60 | // GetUsdAmount implements deposit.Store.GetUsdAmount 61 | func (s *store) GetUsdAmount(ctx context.Context, account string) (float64, error) { 62 | return dbGetUsdAmount(ctx, s.db, account) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/code/data/estimated_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEstimated_Account(t *testing.T) { 12 | ctx := context.Background() 13 | 14 | provider, err := NewEstimatedProvider() 15 | require.NoError(t, err) 16 | 17 | err = provider.AddKnownAccount(ctx, []byte("account_1")) 18 | require.NoError(t, err) 19 | 20 | err = provider.AddKnownAccount(ctx, []byte("account_2")) 21 | require.NoError(t, err) 22 | 23 | found, err := provider.TestForKnownAccount(ctx, []byte("account_x")) 24 | require.NoError(t, err) 25 | assert.False(t, found) 26 | 27 | found, err = provider.TestForKnownAccount(ctx, []byte("account_1")) 28 | require.NoError(t, err) 29 | assert.True(t, found) 30 | 31 | found, err = provider.TestForKnownAccount(ctx, []byte("account_2")) 32 | require.NoError(t, err) 33 | assert.True(t, found) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/code/data/external_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestComputeAllExchangeRates_HappyPath(t *testing.T) { 11 | coreMintRates := map[string]float64{ 12 | "usd": 0.5, 13 | "cad": 1.0, 14 | } 15 | 16 | usdRates := map[string]float64{ 17 | "usd": 1.0, 18 | "cad": 1.3, 19 | "eur": 1.0, 20 | "aud": 0.66, 21 | } 22 | 23 | rates, err := computeAllExchangeRates(coreMintRates, usdRates) 24 | require.NoError(t, err) 25 | 26 | assert.Equal(t, rates["usd"], 0.5) 27 | assert.Equal(t, rates["cad"], 0.65) 28 | assert.Equal(t, rates["eur"], 0.5) 29 | assert.Equal(t, rates["aud"], 0.33) 30 | } 31 | 32 | func TestComputeAllExchangeRates_UsdRateMissing(t *testing.T) { 33 | kinRates := map[string]float64{ 34 | "cad": 1.0, 35 | } 36 | 37 | usdRates := map[string]float64{ 38 | "usd": 1.0, 39 | "cad": 1.3, 40 | "eur": 1.0, 41 | "aud": 0.66, 42 | } 43 | 44 | _, err := computeAllExchangeRates(kinRates, usdRates) 45 | assert.Error(t, err) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/code/data/fulfillment/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/fulfillment/tests" 7 | ) 8 | 9 | func TestFulfillmentMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/intent/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/intent/tests" 7 | ) 8 | 9 | func TestIntentMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/intent/store.go: -------------------------------------------------------------------------------- 1 | package intent 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/stickychin/code-server/pkg/database/query" 8 | ) 9 | 10 | type Store interface { 11 | // Save creates or updates an intent on the store. 12 | Save(ctx context.Context, record *Record) error 13 | 14 | // Get finds the intent record for a given intent ID. 15 | // 16 | // Returns ErrNotFound if no record is found. 17 | Get(ctx context.Context, intentID string) (*Record, error) 18 | 19 | // GetAllByOwner returns all records for a given owner (as both a source and destination). 20 | // 21 | // Returns ErrNotFound if no records are found. 22 | GetAllByOwner(ctx context.Context, owner string, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) 23 | 24 | // GetLatestByInitiatorAndType gets the latest record by initiating owner and intent type 25 | // 26 | // Returns ErrNotFound if no records are found. 27 | GetLatestByInitiatorAndType(ctx context.Context, intentType Type, owner string) (*Record, error) 28 | 29 | // GetOriginalGiftCardIssuedIntent gets the original intent where a gift card 30 | // was issued by its vault address. 31 | GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*Record, error) 32 | 33 | // GetGiftCardClaimedIntent gets the intent where a gift card was claimed by its 34 | // vault address. 35 | GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*Record, error) 36 | 37 | // GetTransactedAmountForAntiMoneyLaundering gets the total transacted core mint quarks and the 38 | // corresponding USD market value for an owner since a timestamp. 39 | GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, owner string, since time.Time) (uint64, float64, error) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/code/data/merkletree/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/merkletree/tests" 7 | ) 8 | 9 | func TestMerkleTreeMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/messaging/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/messaging/tests" 7 | ) 8 | 9 | func TestMessagingMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | 15 | tests.RunTests(t, testStore, teardown) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/code/data/messaging/postgres/model_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/mr-tron/base58" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "google.golang.org/protobuf/proto" 12 | 13 | commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" 14 | messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" 15 | 16 | "github.com/stickychin/code-server/pkg/code/data/messaging" 17 | ) 18 | 19 | func TestModelConversion(t *testing.T) { 20 | pub, _, err := ed25519.GenerateKey(nil) 21 | require.NoError(t, err) 22 | requestor, _, err := ed25519.GenerateKey(nil) 23 | require.NoError(t, err) 24 | 25 | account := &commonpb.SolanaAccountId{Value: pub} 26 | messageID := uuid.New() 27 | idBytes, _ := messageID.MarshalBinary() 28 | message := &messagingpb.Message{ 29 | Id: &messagingpb.MessageId{ 30 | Value: idBytes, 31 | }, 32 | Kind: &messagingpb.Message_RequestToGrabBill{ 33 | RequestToGrabBill: &messagingpb.RequestToGrabBill{ 34 | RequestorAccount: &commonpb.SolanaAccountId{ 35 | Value: requestor, 36 | }, 37 | }, 38 | }, 39 | } 40 | messageBytes, err := proto.Marshal(message) 41 | require.NoError(t, err) 42 | 43 | record := &messaging.Record{ 44 | Account: base58.Encode(account.Value), 45 | MessageID: messageID, 46 | Message: messageBytes, 47 | } 48 | 49 | model, err := toModel(record) 50 | require.NoError(t, err) 51 | assert.Equal(t, model.Account, base58.Encode(account.Value)) 52 | assert.Equal(t, model.MessageID, messageID.String()) 53 | assert.Equal(t, model.Message, messageBytes) 54 | 55 | actual, err := fromModel(model) 56 | require.NoError(t, err) 57 | assert.Equal(t, actual, record) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/code/data/messaging/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/google/uuid" 8 | "github.com/jmoiron/sqlx" 9 | 10 | "github.com/stickychin/code-server/pkg/code/data/messaging" 11 | ) 12 | 13 | // todo: This doesn't support TTL expiries, which is fine for now. We can 14 | // manually delete old entries while in an invite-only testing phase. 15 | type store struct { 16 | db *sqlx.DB 17 | } 18 | 19 | // New returns a postgres backed messaging.Store. 20 | func New(db *sql.DB) messaging.Store { 21 | return &store{ 22 | db: sqlx.NewDb(db, "pgx"), 23 | } 24 | } 25 | 26 | // Insert implements messaging.Store.Insert. 27 | func (s *store) Insert(ctx context.Context, record *messaging.Record) error { 28 | model, err := toModel(record) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return model.dbSave(ctx, s.db) 34 | } 35 | 36 | // Delete implements messaging.Store.Delete. 37 | func (s *store) Delete(ctx context.Context, account string, messageID uuid.UUID) error { 38 | return dbDelete(ctx, s.db, account, messageID.String()) 39 | } 40 | 41 | // Get implements messaging.Store.Get. 42 | func (s *store) Get(ctx context.Context, account string) ([]*messaging.Record, error) { 43 | models, err := dbGetAllForAccount(ctx, s.db, account) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | records := make([]*messaging.Record, len(models)) 49 | for i, model := range models { 50 | record, err := fromModel(model) 51 | if err != nil { 52 | // todo(safety): this is the equivalent QoS brick case, although should be less problematic. 53 | // we could have a valve to ignore, and also to delete 54 | return nil, err 55 | } 56 | records[i] = record 57 | } 58 | 59 | return records, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/code/data/messaging/store.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var ( 11 | ErrDuplicateMessageID = errors.New("duplicate message id") 12 | ) 13 | 14 | type Record struct { 15 | Account string 16 | MessageID uuid.UUID 17 | Message []byte 18 | } 19 | 20 | // Store stores messages. 21 | type Store interface { 22 | // Insert inserts a message into a specified bin. 23 | // 24 | // ErrDuplicateMessageID is returned if the message's ID already exists in the bin. 25 | Insert(ctx context.Context, record *Record) error 26 | 27 | // Delete deletes a message in the specified bin. 28 | // 29 | // Delete is idempotent. 30 | Delete(ctx context.Context, account string, messageID uuid.UUID) error 31 | 32 | // Get returns the messages in a bin. 33 | Get(ctx context.Context, account string) ([]*Record, error) 34 | } 35 | 36 | func (r *Record) Validate() error { 37 | if len(r.Account) == 0 { 38 | return errors.New("account is required") 39 | } 40 | 41 | var defaultUUID uuid.UUID 42 | if r.MessageID == defaultUUID { 43 | return errors.New("message id is required") 44 | } 45 | 46 | if len(r.Message) == 0 { 47 | return errors.New("message is required") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (r *Record) Clone() Record { 54 | copied := Record{ 55 | Account: r.Account, 56 | Message: make([]byte, len(r.Message)), 57 | } 58 | 59 | copy(copied.MessageID[:], r.MessageID[:]) 60 | copy(copied.Message, r.Message) 61 | 62 | return copied 63 | } 64 | -------------------------------------------------------------------------------- /pkg/code/data/nonce/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/nonce/tests" 7 | ) 8 | 9 | func TestNonceMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/nonce/store.go: -------------------------------------------------------------------------------- 1 | package nonce 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/database/query" 7 | ) 8 | 9 | type Store interface { 10 | // Count returns the total count of nonce accounts within an environment instance 11 | Count(ctx context.Context, env Environment, instance string) (uint64, error) 12 | 13 | // CountByState returns the total count of nonce accounts in the provided state within 14 | // an environment instance 15 | CountByState(ctx context.Context, env Environment, instance string, state State) (uint64, error) 16 | 17 | // CountByStateAndPurpose returns the total count of nonce accounts in the provided 18 | // state and use case within an environment instance 19 | CountByStateAndPurpose(ctx context.Context, env Environment, instance string, state State, purpose Purpose) (uint64, error) 20 | 21 | // Save creates or updates nonce metadata in the store. 22 | Save(ctx context.Context, record *Record) error 23 | 24 | // Get finds the nonce record for a given address. 25 | // 26 | // Returns ErrNotFound if no record is found. 27 | Get(ctx context.Context, address string) (*Record, error) 28 | 29 | // GetAllByState returns nonce records in the store for a given confirmation state 30 | // within an environment intance. 31 | // 32 | // Returns ErrNotFound if no records are found. 33 | GetAllByState(ctx context.Context, env Environment, instance string, state State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) 34 | 35 | // GetRandomAvailableByPurpose gets a random available nonce for a purpose within 36 | // an environment instance. 37 | // 38 | // Returns ErrNotFound if no records are found. 39 | GetRandomAvailableByPurpose(ctx context.Context, env Environment, instance string, purpose Purpose) (*Record, error) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/code/data/onramp/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/onramp/tests" 7 | ) 8 | 9 | func TestOnrampMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/onramp/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/google/uuid" 8 | "github.com/jmoiron/sqlx" 9 | 10 | "github.com/stickychin/code-server/pkg/code/data/onramp" 11 | ) 12 | 13 | type store struct { 14 | db *sqlx.DB 15 | } 16 | 17 | // New returns a new postgres-backed onramp.Store 18 | func New(db *sql.DB) onramp.Store { 19 | return &store{ 20 | db: sqlx.NewDb(db, "pgx"), 21 | } 22 | } 23 | 24 | // Put implements onramp.Store.Put 25 | func (s *store) Put(ctx context.Context, record *onramp.Record) error { 26 | model, err := toModel(record) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if err := model.dbPut(ctx, s.db); err != nil { 32 | return err 33 | } 34 | 35 | res := fromModel(model) 36 | res.CopyTo(record) 37 | 38 | return nil 39 | } 40 | 41 | // Get implements onramp.Store.Get 42 | func (s *store) Get(ctx context.Context, nonce uuid.UUID) (*onramp.Record, error) { 43 | model, err := dbGet(ctx, s.db, nonce) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return fromModel(model), nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/code/data/onramp/purchase.go: -------------------------------------------------------------------------------- 1 | package onramp 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // todo: We can track provider, fulfillment state, time to fulfillment, etc. 12 | type Record struct { 13 | Id uint64 14 | 15 | Owner string 16 | Platform int // todo: can't use client.DeviceType due to import cycle 17 | Currency string 18 | Amount float64 19 | Nonce uuid.UUID 20 | 21 | CreatedAt time.Time 22 | } 23 | 24 | func (r *Record) Validate() error { 25 | if len(r.Owner) == 0 { 26 | return errors.New("owner is required") 27 | } 28 | 29 | if len(r.Currency) == 0 { 30 | return errors.New("currency is required") 31 | } 32 | 33 | if r.Amount <= 0 { 34 | return errors.New("amount must be positive") 35 | } 36 | 37 | if bytes.Equal(r.Nonce[:], uuid.Nil[:]) { 38 | return errors.New("nonce is required") 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (r *Record) Clone() Record { 45 | return Record{ 46 | Id: r.Id, 47 | 48 | Owner: r.Owner, 49 | Platform: r.Platform, 50 | Currency: r.Currency, 51 | Amount: r.Amount, 52 | Nonce: r.Nonce, 53 | 54 | CreatedAt: r.CreatedAt, 55 | } 56 | } 57 | 58 | func (r *Record) CopyTo(dst *Record) { 59 | dst.Id = r.Id 60 | 61 | dst.Owner = r.Owner 62 | dst.Platform = r.Platform 63 | dst.Currency = r.Currency 64 | dst.Amount = r.Amount 65 | dst.Nonce = r.Nonce 66 | 67 | dst.CreatedAt = r.CreatedAt 68 | } 69 | -------------------------------------------------------------------------------- /pkg/code/data/onramp/store.go: -------------------------------------------------------------------------------- 1 | package onramp 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var ( 11 | // ErrPurchaseAlreadyExists indicates an onramp purchase record already exists 12 | ErrPurchaseAlreadyExists = errors.New("onramp purchase record already exists") 13 | 14 | // ErrPurchaseNotFound indicates an onramp purchase record doesn't exists 15 | ErrPurchaseNotFound = errors.New("onramp purchase record not found") 16 | ) 17 | 18 | type Store interface { 19 | // Put creates a new onramp purchase record 20 | Put(ctx context.Context, record *Record) error 21 | 22 | // Get gets an onramp purchase record by nonce 23 | Get(ctx context.Context, nonce uuid.UUID) (*Record, error) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/code/data/paymentrequest/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/paymentrequest/tests" 7 | ) 8 | 9 | func TestPaymentRequestMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/paymentrequest/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/paymentrequest" 10 | ) 11 | 12 | type store struct { 13 | db *sqlx.DB 14 | } 15 | 16 | func New(db *sql.DB) paymentrequest.Store { 17 | return &store{ 18 | db: sqlx.NewDb(db, "pgx"), 19 | } 20 | } 21 | 22 | // Put implements paymentrequest.Store.Put 23 | func (s *store) Put(ctx context.Context, record *paymentrequest.Record) error { 24 | m, err := toRequestModel(record) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = m.dbPut(ctx, s.db) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | res := fromRequestModel(m) 35 | res.CopyTo(record) 36 | 37 | return nil 38 | } 39 | 40 | // Get implements paymentrequest.Store.Get 41 | func (s *store) Get(ctx context.Context, intentId string) (*paymentrequest.Record, error) { 42 | m, err := dbGet(ctx, s.db, intentId) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return fromRequestModel(m), nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/code/data/paymentrequest/store.go: -------------------------------------------------------------------------------- 1 | package paymentrequest 2 | 3 | // todo: Refactor this package to a generic "request" model, similar to intent. 4 | // This package started with the payments use case, but is quickly evolving 5 | // beyond that. Naming is skewed towards payments here until refactor. 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | ) 11 | 12 | var ( 13 | ErrPaymentRequestAlreadyExists = errors.New("payment request record already exists") 14 | ErrPaymentRequestNotFound = errors.New("no payment request records could be found") 15 | ErrInvalidPaymentRequest = errors.New("payment request is invalid") 16 | ) 17 | 18 | type Store interface { 19 | // Put creates a new payment request record 20 | Put(ctx context.Context, record *Record) error 21 | 22 | // Get gets a paymen request record by its intent ID 23 | Get(ctx context.Context, intentId string) (*Record, error) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/code/data/rendezvous/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/rendezvous/tests" 7 | ) 8 | 9 | func TestRendezvousMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | 15 | tests.RunTests(t, testStore, teardown) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/code/data/rendezvous/postgres/store.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | "github.com/stickychin/code-server/pkg/code/data/rendezvous" 10 | ) 11 | 12 | type store struct { 13 | db *sqlx.DB 14 | } 15 | 16 | // New returns a new postgres-backed rendezvous.Store 17 | func New(db *sql.DB) rendezvous.Store { 18 | return &store{ 19 | db: sqlx.NewDb(db, "pgx"), 20 | } 21 | } 22 | 23 | // Save implements rendezvous.Store.Save 24 | func (s *store) Save(ctx context.Context, record *rendezvous.Record) error { 25 | obj, err := toModel(record) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | err = obj.dbSave(ctx, s.db) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | res := fromModel(obj) 36 | res.CopyTo(record) 37 | 38 | return nil 39 | } 40 | 41 | // Get implements rendezvous.Store.Get 42 | func (s *store) Get(ctx context.Context, key string) (*rendezvous.Record, error) { 43 | model, err := dbGetByKey(ctx, s.db, key) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return fromModel(model), nil 49 | } 50 | 51 | // Delete implements rendezvous.Store.Delete 52 | func (s *store) Delete(ctx context.Context, key string) error { 53 | return dbDelete(ctx, s.db, key) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/code/data/rendezvous/store.go: -------------------------------------------------------------------------------- 1 | package rendezvous 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrNotFound = errors.New("rendezvous record not found") 11 | ) 12 | 13 | type Record struct { 14 | Id uint64 15 | Key string 16 | Location string 17 | CreatedAt time.Time 18 | LastUpdatedAt time.Time 19 | } 20 | 21 | type Store interface { 22 | Save(ctx context.Context, record *Record) error 23 | 24 | Get(ctx context.Context, key string) (*Record, error) 25 | 26 | Delete(ctx context.Context, key string) error 27 | } 28 | 29 | func (r *Record) Validate() error { 30 | if len(r.Key) == 0 { 31 | return errors.New("key is required") 32 | } 33 | 34 | if len(r.Location) == 0 { 35 | return errors.New("location is required") 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (r *Record) Clone() Record { 42 | return Record{ 43 | Id: r.Id, 44 | Key: r.Key, 45 | Location: r.Location, 46 | CreatedAt: r.CreatedAt, 47 | LastUpdatedAt: r.LastUpdatedAt, 48 | } 49 | } 50 | 51 | func (r *Record) CopyTo(dst *Record) { 52 | dst.Id = r.Id 53 | dst.Key = r.Key 54 | dst.Location = r.Location 55 | dst.CreatedAt = r.CreatedAt 56 | dst.LastUpdatedAt = r.LastUpdatedAt 57 | } 58 | -------------------------------------------------------------------------------- /pkg/code/data/timelock/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/timelock/tests" 7 | ) 8 | 9 | func TestTimelockMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/timelock/store.go: -------------------------------------------------------------------------------- 1 | package timelock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/database/query" 7 | timelock_token "github.com/stickychin/code-server/pkg/solana/timelock/v1" 8 | ) 9 | 10 | type Store interface { 11 | // Save saves a timelock account's state 12 | Save(ctx context.Context, record *Record) error 13 | 14 | // GetByAddress gets a timelock account's state by the state address 15 | GetByAddress(ctx context.Context, address string) (*Record, error) 16 | 17 | // GetByVault gets a timelock account's state by the vault address it's locking 18 | GetByVault(ctx context.Context, vault string) (*Record, error) 19 | 20 | // GetByDepositPda gets a timelock account's state by the deposit PDA address 21 | GetByDepositPda(ctx context.Context, depositPda string) (*Record, error) 22 | 23 | // GetByVaultBatch is like GetByVault, but for multiple accounts. If any one account 24 | // is missing, ErrTimelockNotFound is returned. 25 | GetByVaultBatch(ctx context.Context, vaults ...string) (map[string]*Record, error) 26 | 27 | // GetAllByState gets all timelock accounts in the provided state 28 | GetAllByState(ctx context.Context, state timelock_token.TimelockState, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) 29 | 30 | // GetCountByState gets the count of records in the provided state 31 | GetCountByState(ctx context.Context, state timelock_token.TimelockState) (uint64, error) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/code/data/transaction/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/transaction/tests" 7 | ) 8 | 9 | func TestTransactionMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/transaction/store.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/database/query" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | var ( 11 | ErrNotFound = errors.New("no records could be found") 12 | ErrExists = errors.New("the transaction already exists") 13 | ) 14 | 15 | type Store interface { 16 | // Get returns a transaction record for the given signature. 17 | // 18 | // ErrNotFound is returned if no record is found. 19 | Get(ctx context.Context, txId string) (*Record, error) 20 | 21 | // Put saves transaction data to the store. 22 | // 23 | // ErrExists is returned if a transaction with the same signature already exists. 24 | Put(ctx context.Context, transaction *Record) error 25 | 26 | // GetAllByAddress returns a list of records that match a given address. 27 | // 28 | // ErrNotFound is returned if no records are found. 29 | GetAllByAddress(ctx context.Context, address string, cursor uint64, limit uint, ordering query.Ordering) ([]*Record, error) 30 | 31 | // GetLatestByState returns the latest record for a given state. 32 | // 33 | // ErrNotFound is returned if no records are found. 34 | GetLatestByState(ctx context.Context, address string, state Confirmation) (*Record, error) 35 | 36 | // GetFirstPending returns the latest record for a given state. 37 | // 38 | // ErrNotFound is returned if no records are found. 39 | GetFirstPending(ctx context.Context, address string) (*Record, error) 40 | 41 | // GetSignaturesByState returns a list of signatures that match given a confirmation state. 42 | // 43 | // ErrNotFound is returned if no records are found. 44 | GetSignaturesByState(ctx context.Context, filter Confirmation, limit uint, ordering query.Ordering) ([]string, error) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/code/data/vault/key_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/vault" 7 | "github.com/mr-tron/base58/base58" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCreateKey(t *testing.T) { 13 | 14 | for i := 0; i < 100; i++ { 15 | actual, err := vault.CreateKey() 16 | require.NoError(t, err) 17 | assert.Equal(t, vault.StateAvailable, actual.State) 18 | 19 | _, err = base58.Decode(actual.PublicKey) 20 | require.NoError(t, err) 21 | 22 | _, err = base58.Decode(actual.PrivateKey) 23 | require.NoError(t, err) 24 | } 25 | 26 | } 27 | 28 | func TestGrindKey(t *testing.T) { 29 | for i := 0; i < 5; i++ { 30 | actual, err := vault.GrindKey("x") 31 | require.NoError(t, err) 32 | assert.Equal(t, vault.StateAvailable, actual.State) 33 | assert.EqualValues(t, 'x', actual.PublicKey[0]) 34 | 35 | _, err = base58.Decode(actual.PublicKey) 36 | require.NoError(t, err) 37 | 38 | _, err = base58.Decode(actual.PrivateKey) 39 | require.NoError(t, err) 40 | } 41 | 42 | for i := 0; i < 5; i++ { 43 | actual, err := vault.GrindKey("y") 44 | require.NoError(t, err) 45 | assert.Equal(t, vault.StateAvailable, actual.State) 46 | assert.EqualValues(t, 'y', actual.PublicKey[0]) 47 | 48 | _, err = base58.Decode(actual.PublicKey) 49 | require.NoError(t, err) 50 | 51 | _, err = base58.Decode(actual.PrivateKey) 52 | require.NoError(t, err) 53 | } 54 | 55 | for i := 0; i < 5; i++ { 56 | actual, err := vault.GrindKey("z") 57 | require.NoError(t, err) 58 | assert.Equal(t, vault.StateAvailable, actual.State) 59 | assert.EqualValues(t, 'z', actual.PublicKey[0]) 60 | 61 | _, err = base58.Decode(actual.PublicKey) 62 | require.NoError(t, err) 63 | 64 | _, err = base58.Decode(actual.PrivateKey) 65 | require.NoError(t, err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/code/data/vault/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/vault/tests" 7 | ) 8 | 9 | func TestFulfillmentMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | tests.RunTests(t, testStore, teardown) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/code/data/vault/secret.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | const ( 8 | vaultSecretKeyEnv = "VAULT_SECRET_KEY" 9 | defaultVaultSecret = "DpmmM8ruhZ5C26USxxEMBfQGAJPzHen6NxNrfKkyaBTB" // for local testing 10 | ) 11 | 12 | func GetSecret() string { 13 | secret := os.Getenv(vaultSecretKeyEnv) 14 | if len(secret) == 0 { 15 | secret = defaultVaultSecret 16 | } 17 | 18 | return secret 19 | } 20 | -------------------------------------------------------------------------------- /pkg/code/data/vault/store.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stickychin/code-server/pkg/database/query" 7 | ) 8 | 9 | // todo: migrate to AWS KMS or similar 10 | 11 | type Store interface { 12 | // Count returns the total count of keys. 13 | Count(ctx context.Context) (uint64, error) 14 | 15 | // CountByState returns the total count of keys by state 16 | CountByState(ctx context.Context, state State) (uint64, error) 17 | 18 | // Save creates or updates the record in the store. 19 | Save(ctx context.Context, record *Record) error 20 | 21 | // Get finds the record for a given public key. 22 | Get(ctx context.Context, pubkey string) (*Record, error) 23 | 24 | // GetAllByState returns all records for a given state. 25 | // 26 | // Returns ErrKeyNotFound if no records are found. 27 | GetAllByState(ctx context.Context, state State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/code/data/webhook/memory/store_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stickychin/code-server/pkg/code/data/webhook/tests" 7 | ) 8 | 9 | func TestWebhookMemoryStore(t *testing.T) { 10 | testStore := New() 11 | teardown := func() { 12 | testStore.(*store).reset() 13 | } 14 | 15 | tests.RunTests(t, testStore, teardown) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/code/data/webhook/store.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | var ( 10 | ErrNotFound = errors.New("webhook record not found") 11 | ErrAlreadyExists = errors.New("webhook record already exists") 12 | ) 13 | 14 | type Store interface { 15 | // Put creates a webhook record 16 | // 17 | // Returns ErrAlreadyExists if a record already exists. 18 | Put(ctx context.Context, record *Record) error 19 | 20 | // Update updates a webhook record 21 | // 22 | // Returns ErrNotFound if no record exists. 23 | Update(ctx context.Context, record *Record) error 24 | 25 | // Get finds the webhook record for a given webhook ID 26 | // 27 | // Returns ErrNotFound if no record is found. 28 | Get(ctx context.Context, webhookId string) (*Record, error) 29 | 30 | // CountByState counts all webhook records in a provided state 31 | CountByState(ctx context.Context, state State) (uint64, error) 32 | 33 | // GetAllPendingReadyToSend gets all webhook records in the pending state 34 | // that have an attempt scheduled to be sent. 35 | // 36 | // Returns ErrNotFound if no record is found. 37 | // 38 | // Note: No traditional pagination since it's expected the next attempt 39 | // timestamp is updated or the state transitions to a terminal value. 40 | GetAllPendingReadyToSend(ctx context.Context, limit uint64) ([]*Record, error) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/code/exchangerate/time.go: -------------------------------------------------------------------------------- 1 | package exchangerate 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // todo: add tests, but generally well tested in server tests since that's where most of this originated 8 | // todo: does this belong in an exchange-specific package? 9 | 10 | // GetLatestExchangeRateTime gets the latest time for fetching an exchange rate. 11 | // By synchronizing on a time, we can eliminate the amount of perceived volatility 12 | // over short time spans. 13 | func GetLatestExchangeRateTime() time.Time { 14 | // Standardize to concrete 15 minute intervals to reduce perceived volatility. 15 | // Notably, don't fall exactly on the 15 minute interval, so we remove 1 second. 16 | // The way our postgres DB query is setup, the start of UTC day is unlikely to 17 | // generate results. 18 | secondsIn15Minutes := int64(15 * time.Minute / time.Second) 19 | queryTimeUnix := time.Now().Unix() 20 | queryTimeUnix = queryTimeUnix - (queryTimeUnix % secondsIn15Minutes) - 1 21 | return time.Unix(queryTimeUnix, 0) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/code/server/messaging/client.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "google.golang.org/grpc/credentials/insecure" 6 | 7 | messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" 8 | "github.com/stickychin/code-server/pkg/grpc/headers" 9 | "github.com/stickychin/code-server/pkg/grpc/protobuf/validation" 10 | ) 11 | 12 | // todo: we can cache and reuse clients 13 | func getInternalMessagingClient(target string) (messagingpb.MessagingClient, func() error, error) { 14 | conn, err := grpc.Dial( 15 | target, 16 | 17 | grpc.WithTransportCredentials(insecure.NewCredentials()), 18 | 19 | grpc.WithUnaryInterceptor(validation.UnaryClientInterceptor()), 20 | grpc.WithUnaryInterceptor(headers.UnaryClientInterceptor()), 21 | 22 | grpc.WithStreamInterceptor(validation.StreamClientInterceptor()), 23 | grpc.WithStreamInterceptor(headers.StreamClientInterceptor()), 24 | ) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | 29 | return messagingpb.NewMessagingClient(conn), conn.Close, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/code/server/messaging/config.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "github.com/stickychin/code-server/pkg/config" 5 | "github.com/stickychin/code-server/pkg/config/env" 6 | "github.com/stickychin/code-server/pkg/config/memory" 7 | "github.com/stickychin/code-server/pkg/config/wrapper" 8 | ) 9 | 10 | const ( 11 | envConfigPrefix = "MESSAGING_SERVICE_" 12 | 13 | DisableBlockchainChecksConfigEnvName = envConfigPrefix + "DISABLE_BLOCKCHAIN_CHECKS" 14 | defaultDisableBlockchainChecks = false 15 | 16 | MaxFeeBasisPointsConfigEnvName = envConfigPrefix + "MAX_FEE_BASIS_POINTS" 17 | defaultMaxFeeBasisPoints = 5000 18 | ) 19 | 20 | type conf struct { 21 | disableBlockchainChecks config.Bool 22 | maxFeeBasisPoints config.Uint64 23 | } 24 | 25 | // ConfigProvider defines how config values are pulled 26 | type ConfigProvider func() *conf 27 | 28 | // WithEnvConfigs returns configuration pulled from environment variables 29 | func WithEnvConfigs() ConfigProvider { 30 | return func() *conf { 31 | return &conf{ 32 | disableBlockchainChecks: env.NewBoolConfig(DisableBlockchainChecksConfigEnvName, defaultDisableBlockchainChecks), 33 | maxFeeBasisPoints: env.NewUint64Config(MaxFeeBasisPointsConfigEnvName, defaultMaxFeeBasisPoints), 34 | } 35 | } 36 | } 37 | 38 | type testOverrides struct { 39 | } 40 | 41 | func withManualTestOverrides(overrides *testOverrides) ConfigProvider { 42 | return func() *conf { 43 | return &conf{ 44 | disableBlockchainChecks: wrapper.NewBoolConfig(memory.NewConfig(true), true), 45 | maxFeeBasisPoints: wrapper.NewUint64Config(memory.NewConfig(defaultMaxFeeBasisPoints), defaultMaxFeeBasisPoints), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/code/server/messaging/error.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import "fmt" 4 | 5 | type MessageValidationError struct { 6 | message string 7 | } 8 | 9 | func newMessageValidationError(message string) MessageValidationError { 10 | return MessageValidationError{ 11 | message: message, 12 | } 13 | } 14 | 15 | func newMessageValidationErrorf(format string, args ...any) MessageValidationError { 16 | return newMessageValidationError(fmt.Sprintf(format, args...)) 17 | } 18 | 19 | func (e MessageValidationError) Error() string { 20 | return e.message 21 | } 22 | 23 | type MessageAuthenticationError struct { 24 | message string 25 | } 26 | 27 | func newMessageAuthenticationError(message string) MessageAuthenticationError { 28 | return MessageAuthenticationError{ 29 | message: message, 30 | } 31 | } 32 | 33 | func newMessageAuthenticationErrorf(format string, args ...any) MessageAuthenticationError { 34 | return newMessageAuthenticationError(fmt.Sprintf(format, args...)) 35 | } 36 | 37 | func (e MessageAuthenticationError) Error() string { 38 | return e.message 39 | } 40 | 41 | type MessageAuthorizationError struct { 42 | message string 43 | } 44 | 45 | func newMessageAuthorizationError(message string) MessageAuthorizationError { 46 | return MessageAuthorizationError{ 47 | message: message, 48 | } 49 | } 50 | 51 | func newMessageAuthorizationErrorf(format string, args ...any) MessageAuthorizationError { 52 | return newMessageAuthorizationError(fmt.Sprintf(format, args...)) 53 | } 54 | 55 | func (e MessageAuthorizationError) Error() string { 56 | return e.message 57 | } 58 | -------------------------------------------------------------------------------- /pkg/code/server/messaging/stream.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "google.golang.org/protobuf/proto" 9 | 10 | messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" 11 | ) 12 | 13 | type messageStream struct { 14 | sync.Mutex 15 | 16 | closed bool 17 | streamCh chan *messagingpb.Message 18 | } 19 | 20 | func newMessageStream(bufferSize int) *messageStream { 21 | return &messageStream{ 22 | streamCh: make(chan *messagingpb.Message, bufferSize), 23 | } 24 | } 25 | 26 | func (s *messageStream) notify(msg *messagingpb.Message, timeout time.Duration) error { 27 | m := proto.Clone(msg).(*messagingpb.Message) 28 | 29 | s.Lock() 30 | 31 | if s.closed { 32 | s.Unlock() 33 | return errors.New("cannot notify closed stream") 34 | } 35 | 36 | select { 37 | case s.streamCh <- m: 38 | case <-time.After(timeout): 39 | s.Unlock() 40 | s.close() 41 | return errors.New("timed out sending message to streamCh") 42 | } 43 | 44 | s.Unlock() 45 | return nil 46 | } 47 | 48 | func (s *messageStream) close() { 49 | s.Lock() 50 | defer s.Unlock() 51 | 52 | if s.closed { 53 | return 54 | } 55 | 56 | s.closed = true 57 | close(s.streamCh) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/code/server/transaction/limits_test.go: -------------------------------------------------------------------------------- 1 | package transaction_v2 2 | 3 | // todo: implement me 4 | -------------------------------------------------------------------------------- /pkg/code/transaction/instruction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "github.com/stickychin/code-server/pkg/code/common" 5 | "github.com/stickychin/code-server/pkg/solana" 6 | "github.com/stickychin/code-server/pkg/solana/system" 7 | ) 8 | 9 | // todo: start moving instruction construction code here 10 | 11 | func makeAdvanceNonceInstruction(nonce *common.Account) (solana.Instruction, error) { 12 | return system.AdvanceNonce( 13 | nonce.PublicKey().ToBytes(), 14 | common.GetSubsidizer().PublicKey().ToBytes(), 15 | ), nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/config/env/config_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/stickychin/code-server/pkg/config" 11 | ) 12 | 13 | func TestConfigDoesntExist(t *testing.T) { 14 | const env = "ENV_CONFIG_TEST_VAR" 15 | os.Setenv(env, "default") 16 | 17 | v, err := NewConfig(env).Get(context.Background()) 18 | assert.Equal(t, []byte("default"), v) 19 | assert.Nil(t, err) 20 | 21 | os.Unsetenv(env) 22 | 23 | v, err = NewConfig(env).Get(context.Background()) 24 | assert.Nil(t, v) 25 | assert.Equal(t, config.ErrNoValue, err) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/config/memory/config_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/stickychin/code-server/pkg/config" 11 | ) 12 | 13 | func TestHappyPath(t *testing.T) { 14 | c := NewConfig(nil) 15 | _, err := c.Get(context.Background()) 16 | assert.Equal(t, config.ErrNoValue, err) 17 | 18 | expected := "value" 19 | c.SetValue(expected) 20 | val, err := c.Get(context.Background()) 21 | require.NoError(t, err) 22 | assert.Equal(t, expected, val) 23 | 24 | c.ClearValue() 25 | _, err = c.Get(context.Background()) 26 | assert.Equal(t, config.ErrNoValue, err) 27 | 28 | c.InduceErrors() 29 | _, err = c.Get(context.Background()) 30 | assert.Equal(t, errDeveloperInduced, err) 31 | 32 | c.StopInducingErrors() 33 | _, err = c.Get(context.Background()) 34 | assert.Equal(t, config.ErrNoValue, err) 35 | 36 | c.Shutdown() 37 | _, err = c.Get(context.Background()) 38 | assert.Equal(t, config.ErrShutdown, err) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/currency/client.go: -------------------------------------------------------------------------------- 1 | package currency 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrInvalidBase = errors.New("invalid base currency") 11 | ) 12 | 13 | type ExchangeData struct { 14 | Base string 15 | Rates map[string]float64 16 | Timestamp time.Time 17 | } 18 | 19 | type Client interface { 20 | // GetCurrentRates gets the current set of exchange rates against a base 21 | // currency. 22 | GetCurrentRates(ctx context.Context, base string) (*ExchangeData, error) 23 | 24 | // GetHistoricalRates gets the historical set of exchange rates against a 25 | // base currency. Granularity of time intervals may be significantly reduced. 26 | // A databse of cached rates from periodic calls to GetCurrentRates is the 27 | // recommended workaround. 28 | GetHistoricalRates(ctx context.Context, base string, timestamp time.Time) (*ExchangeData, error) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/database/postgres/errors.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/jackc/pgconn" 7 | "github.com/jackc/pgerrcode" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func CheckNoRows(inErr, outErr error) error { 12 | if inErr == sql.ErrNoRows { 13 | return outErr 14 | } 15 | return inErr 16 | } 17 | 18 | func CheckUniqueViolation(inErr, outErr error) error { 19 | if inErr != nil { 20 | var pgErr *pgconn.PgError 21 | if errors.As(inErr, &pgErr) { 22 | if pgErr.Code == pgerrcode.UniqueViolation { 23 | return outErr 24 | } 25 | } 26 | } 27 | return inErr 28 | } 29 | 30 | func IsUniqueViolation(err error) bool { 31 | if err == nil { 32 | return false 33 | } 34 | 35 | var pgErr *pgconn.PgError 36 | if errors.As(err, &pgErr) { 37 | if pgErr.Code == pgerrcode.UniqueViolation { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /pkg/database/query/cursor.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/mr-tron/base58" 7 | ) 8 | 9 | type Cursor []byte 10 | 11 | var ( 12 | EmptyCursor Cursor = Cursor([]byte{}) 13 | ) 14 | 15 | func ToCursor(val uint64) Cursor { 16 | b := make([]byte, 8) 17 | binary.BigEndian.PutUint64(b, val) 18 | return b 19 | } 20 | 21 | func FromCursor(val []byte) uint64 { 22 | return binary.BigEndian.Uint64(val) 23 | } 24 | 25 | func (c Cursor) ToUint64() uint64 { 26 | return binary.BigEndian.Uint64(c) 27 | } 28 | 29 | func (c Cursor) ToBase58() string { 30 | return base58.Encode(c) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/database/query/filter.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | type Filter struct { 4 | Value uint64 5 | Valid bool 6 | } 7 | 8 | func NewFilter(value uint64) Filter { 9 | return Filter{ 10 | Value: value, 11 | Valid: true, 12 | } 13 | } 14 | 15 | func (f *Filter) IsValid() bool { 16 | return f.Valid 17 | } 18 | 19 | func (f *Filter) Set(value uint64) { 20 | f.Value = value 21 | f.Valid = true 22 | } 23 | -------------------------------------------------------------------------------- /pkg/database/query/interval.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | type Interval uint8 8 | 9 | const ( 10 | IntervalRaw Interval = iota 11 | IntervalHour 12 | IntervalDay 13 | IntervalWeek 14 | IntervalMonth 15 | ) 16 | 17 | var AllIntervals = []Interval{ 18 | IntervalRaw, 19 | IntervalHour, 20 | IntervalDay, 21 | IntervalWeek, 22 | IntervalMonth, 23 | } 24 | 25 | func ToInterval(val string) (Interval, error) { 26 | switch val { 27 | case "raw": 28 | return IntervalRaw, nil 29 | case "hour": 30 | return IntervalHour, nil 31 | case "day": 32 | return IntervalDay, nil 33 | case "week": 34 | return IntervalWeek, nil 35 | case "month": 36 | return IntervalMonth, nil 37 | default: 38 | return 0, errors.Errorf("unexpected value: %v", val) 39 | } 40 | } 41 | 42 | func FromInterval(val Interval) (string, error) { 43 | switch val { 44 | case IntervalRaw: 45 | return "raw", nil 46 | case IntervalHour: 47 | return "hour", nil 48 | case IntervalDay: 49 | return "day", nil 50 | case IntervalWeek: 51 | return "week", nil 52 | case IntervalMonth: 53 | return "month", nil 54 | default: 55 | return "", errors.Errorf("unexpected value: %v", val) 56 | } 57 | } 58 | 59 | func ToIntervalWithFallback(val string, fallback Interval) Interval { 60 | res, err := ToInterval(val) 61 | if err != nil { 62 | return fallback 63 | } 64 | return res 65 | } 66 | 67 | func FromIntervalWithFallback(val Interval, fallback string) string { 68 | res, err := FromInterval(val) 69 | if err != nil { 70 | return fallback 71 | } 72 | return res 73 | } 74 | -------------------------------------------------------------------------------- /pkg/database/query/ordering.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | // The ordering of a returned set of records 8 | type Ordering uint 9 | 10 | const ( 11 | Ascending Ordering = iota 12 | Descending 13 | ) 14 | 15 | func ToOrdering(val string) (Ordering, error) { 16 | switch val { 17 | case "asc": 18 | return Ascending, nil 19 | case "desc": 20 | return Descending, nil 21 | default: 22 | return 0, errors.Errorf("unexpected value: %v", val) 23 | } 24 | } 25 | 26 | func FromOrdering(val Ordering) (string, error) { 27 | switch val { 28 | case Ascending: 29 | return "asc", nil 30 | case Descending: 31 | return "desc", nil 32 | default: 33 | return "", errors.Errorf("unexpected value: %v", val) 34 | } 35 | } 36 | 37 | func ToOrderingWithFallback(val string, fallback Ordering) Ordering { 38 | res, err := ToOrdering(val) 39 | if err != nil { 40 | return fallback 41 | } 42 | return res 43 | } 44 | 45 | func FromOrderingWithFallback(val Ordering, fallback string) string { 46 | res, err := FromOrdering(val) 47 | if err != nil { 48 | return fallback 49 | } 50 | return res 51 | } 52 | -------------------------------------------------------------------------------- /pkg/etcd/wait.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | 6 | v3 "go.etcd.io/etcd/client/v3" 7 | ) 8 | 9 | func WaitFor(ctx context.Context, client *v3.Client, key string, exists bool) error { 10 | get, err := client.Get(ctx, key) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if len(get.Kvs) == 0 && !exists { 16 | return nil 17 | } else if len(get.Kvs) > 0 && exists { 18 | return nil 19 | } 20 | 21 | ctx, cancel := context.WithCancel(ctx) 22 | defer cancel() 23 | 24 | watch := client.Watch(ctx, key, v3.WithRev(get.Header.Revision+1)) 25 | for w := range watch { 26 | if err = w.Err(); err != nil { 27 | return err 28 | } 29 | 30 | for _, e := range w.Events { 31 | if e.Type == v3.EventTypePut && exists { 32 | return nil 33 | } 34 | if e.Type == v3.EventTypeDelete && !exists { 35 | return nil 36 | } 37 | } 38 | } 39 | 40 | return ctx.Err() 41 | } 42 | -------------------------------------------------------------------------------- /pkg/grpc/app/loader.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "sync" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | ctorMu sync.Mutex 13 | ctors = make(map[string]FileLoaderCtor) 14 | ) 15 | 16 | // RegisterFileLoaderCtor registers a FileLoader for the specified scheme. 17 | func RegisterFileLoaderCtor(scheme string, ctr FileLoaderCtor) { 18 | ctorMu.Lock() 19 | defer ctorMu.Unlock() 20 | 21 | _, exists := ctors[scheme] 22 | if exists { 23 | panic(fmt.Sprintf("FileLoader already registered for scheme '%s'", scheme)) 24 | } 25 | 26 | ctors[scheme] = ctr 27 | } 28 | 29 | // FileLoaderCtor constructs a FileLoader. 30 | type FileLoaderCtor func() (FileLoader, error) 31 | 32 | // FileLoader loads files at a specified URL. 33 | type FileLoader interface { 34 | Load(url *url.URL) ([]byte, error) 35 | } 36 | 37 | // LoadFile loads a file at the specified URL using the corresponding 38 | // registered FileLoader. If no scheme is specified, LocalLoader is used. 39 | func LoadFile(fileURL string) ([]byte, error) { 40 | ctorMu.Lock() 41 | defer ctorMu.Unlock() 42 | 43 | u, err := url.Parse(fileURL) 44 | if err != nil { 45 | return nil, errors.Wrapf(err, "invalid file url %s", fileURL) 46 | } 47 | 48 | ctr, exists := ctors[u.Scheme] 49 | if !exists { 50 | return nil, errors.Errorf("no file loader for %s", u.Scheme) 51 | } 52 | 53 | l, err := ctr() 54 | if err != nil { 55 | return nil, errors.Wrapf(err, "failed get loader for '%s'", fileURL) 56 | } 57 | 58 | return l.Load(u) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/grpc/app/option.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | ) 6 | 7 | // Option configures the environment run by Run(). 8 | type Option func(o *opts) 9 | 10 | type opts struct { 11 | unaryServerInterceptors []grpc.UnaryServerInterceptor 12 | streamServerInterceptors []grpc.StreamServerInterceptor 13 | } 14 | 15 | // WithUnaryServerInterceptor configures the app's gRPC server to use the provided interceptor. 16 | // 17 | // Interceptors are evaluated in addition order, and configured interceptors are executed after 18 | // the app's default interceptors. 19 | func WithUnaryServerInterceptor(interceptor grpc.UnaryServerInterceptor) Option { 20 | return func(o *opts) { 21 | o.unaryServerInterceptors = append(o.unaryServerInterceptors, interceptor) 22 | } 23 | } 24 | 25 | // WithStreamServerInterceptor configures the app's gRPC server to use the provided interceptor. 26 | // 27 | // Interceptors are evaluated in addition order, and configured interceptors are executed after 28 | // the app's default interceptors. 29 | func WithStreamServerInterceptor(interceptor grpc.StreamServerInterceptor) Option { 30 | return func(o *opts) { 31 | o.streamServerInterceptors = append(o.streamServerInterceptors, interceptor) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/grpc/client/device.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "strings" 4 | 5 | type DeviceType uint8 6 | 7 | const ( 8 | DeviceTypeUnknown DeviceType = iota 9 | DeviceTypeIOS 10 | DeviceTypeAndroid 11 | ) 12 | 13 | func (t DeviceType) String() string { 14 | switch t { 15 | case DeviceTypeUnknown: 16 | return "Unknown" 17 | case DeviceTypeIOS: 18 | return "iOS" 19 | case DeviceTypeAndroid: 20 | return "Android" 21 | } 22 | return "Unknown" 23 | } 24 | 25 | func (t DeviceType) IsMobile() bool { 26 | switch t { 27 | case DeviceTypeIOS, DeviceTypeAndroid: 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | func deviceTypeFromString(value string) DeviceType { 34 | switch strings.ToLower(value) { 35 | case "ios": 36 | return DeviceTypeIOS 37 | case "android": 38 | return DeviceTypeAndroid 39 | } 40 | return DeviceTypeUnknown 41 | } 42 | -------------------------------------------------------------------------------- /pkg/grpc/client/device_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDeviceTypeFromString(t *testing.T) { 10 | for _, value := range []string{ 11 | "iOS", 12 | "ios", 13 | } { 14 | assert.Equal(t, DeviceTypeIOS, deviceTypeFromString(value)) 15 | } 16 | 17 | for _, value := range []string{ 18 | "Android", 19 | "android", 20 | } { 21 | assert.Equal(t, DeviceTypeAndroid, deviceTypeFromString(value)) 22 | } 23 | 24 | assert.Equal(t, DeviceTypeUnknown, deviceTypeFromString("windows")) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/grpc/client/ip.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "google.golang.org/grpc/metadata" 8 | ) 9 | 10 | const ( 11 | clientIPHeader = "x-forwarded-for" 12 | ) 13 | 14 | // GetIPAddr gets the client's IP address 15 | func GetIPAddr(ctx context.Context) (string, error) { 16 | mtdt, ok := metadata.FromIncomingContext(ctx) 17 | if !ok { 18 | return "", errors.New("no metdata in context") 19 | } 20 | 21 | ipHeaders := mtdt.Get(clientIPHeader) 22 | if len(ipHeaders) == 0 { 23 | return "", errors.New("x-forwarded-for header not set") 24 | } 25 | 26 | return ipHeaders[0], nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/grpc/client/ip_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "google.golang.org/grpc/metadata" 9 | ) 10 | 11 | func TestGetIPAddr(t *testing.T) { 12 | tt := map[string]struct { 13 | ctx context.Context 14 | hasIP bool 15 | expectedIP string 16 | }{ 17 | "emptyContext": { 18 | ctx: context.Background(), 19 | hasIP: false, 20 | }, 21 | "contextWithoutClientIP": { 22 | ctx: metadata.NewIncomingContext( 23 | context.Background(), 24 | metadata.MD{"key": []string{"value"}}, 25 | ), 26 | hasIP: false, 27 | }, 28 | "contextWithClientIP": { 29 | ctx: metadata.NewIncomingContext( 30 | context.Background(), 31 | metadata.MD{clientIPHeader: []string{"127.0.0.1"}}, 32 | ), 33 | hasIP: true, 34 | expectedIP: "127.0.0.1", 35 | }, 36 | } 37 | 38 | for name, tc := range tt { 39 | t.Run(name, func(t *testing.T) { 40 | actual, err := GetIPAddr(tc.ctx) 41 | assert.Equal(t, tc.hasIP, err == nil) 42 | assert.Equal(t, tc.expectedIP, actual) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/grpc/client/logging.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // InjectLoggingMetadata injects client metadata into a logrus log entry 10 | func InjectLoggingMetadata(ctx context.Context, log *logrus.Entry) *logrus.Entry { 11 | userAgent, err := GetUserAgent(ctx) 12 | if err == nil { 13 | log = log.WithField("user_agent", userAgent.String()) 14 | } 15 | 16 | ip, err := GetIPAddr(ctx) 17 | if err == nil { 18 | log = log.WithField("client_ip", ip) 19 | } 20 | 21 | return log 22 | } 23 | -------------------------------------------------------------------------------- /pkg/grpc/client/user_agent.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/stickychin/code-server/pkg/grpc/headers" 12 | ) 13 | 14 | const ( 15 | UserAgentHeaderName = "user-agent" 16 | ) 17 | 18 | var ( 19 | userAgentPattern = fmt.Sprintf("Code/(iOS|Android)/%s", versionPattern) 20 | userAgentRegex = regexp.MustCompile(userAgentPattern) 21 | ) 22 | 23 | type UserAgent struct { 24 | DeviceType DeviceType 25 | Version Version 26 | } 27 | 28 | func (ua *UserAgent) String() string { 29 | return fmt.Sprintf("Code/%s/%s", ua.DeviceType.String(), ua.Version.String()) 30 | } 31 | 32 | // GetUserAgent gets the Code client user agent value from headers in the provided 33 | // context 34 | func GetUserAgent(ctx context.Context) (*UserAgent, error) { 35 | headerValue, err := headers.GetASCIIHeaderByName(ctx, UserAgentHeaderName) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "user agent header not present") 38 | } 39 | 40 | headerValue = strings.TrimSpace(headerValue) 41 | 42 | matches := userAgentRegex.FindAllStringSubmatch(headerValue, -1) 43 | if len(matches) != 1 { 44 | return nil, errors.New("zero or more than one code version present") 45 | } 46 | 47 | userAgentValue := matches[0][0] 48 | parts := strings.Split(userAgentValue, "/") 49 | 50 | deviceType := deviceTypeFromString(parts[1]) 51 | if deviceType == DeviceTypeUnknown { 52 | return nil, errors.New("unhandled client type") 53 | } 54 | 55 | version, err := ParseVersion(parts[2]) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &UserAgent{ 61 | DeviceType: deviceType, 62 | Version: *version, 63 | }, nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/grpc/headers/headers.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | // Type represents the header type 4 | type Type int 5 | 6 | const ( 7 | // Outbound headers are only sent on the next service call. They are received as Inbound headers 8 | // on the recipients side. 9 | Outbound Type = iota 10 | // Inbound headers are received from the previous service call, and will not be sent to future service calls 11 | Inbound 12 | // Root headers are created from edge layers, and will be passed on to all future service calls 13 | Root 14 | // Propagating headers can be created by anyone, and will be passed on to all future service calls 15 | Propagating 16 | // ASCII headers are similar to Inbound headers, except the header is pure text and not an encoded protobuf 17 | ASCII 18 | ) 19 | 20 | func (h Type) prefix() string { 21 | switch h { 22 | case Root: 23 | return "root-" 24 | case Propagating: 25 | return "prop-" 26 | default: 27 | return "" 28 | } 29 | } 30 | 31 | func (h Type) String() string { 32 | switch h { 33 | case Root: 34 | return "Root" 35 | case Propagating: 36 | return "Propagating" 37 | case Inbound: 38 | return "Inbound" 39 | case Outbound: 40 | return "Outbound" 41 | case ASCII: 42 | return "ASCII" 43 | default: 44 | return "Default" 45 | } 46 | } 47 | 48 | // Headers a custom map that takes a string as a key and either a string or []byte as its value 49 | type Headers map[string]interface{} 50 | 51 | // merge merges the current Headers h with the given merger Headers 52 | func (h Headers) merge(other Headers) { 53 | for k, v := range other { 54 | h[k] = v 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/grpc/protobuf/validation/test/Makefile: -------------------------------------------------------------------------------- 1 | all: generate 2 | 3 | generate: 4 | docker run --platform linux/amd64 -v $(shell pwd)/:/proto -v $(shell pwd)/:/genproto kinecosystem/protoc-gen-go 5 | 6 | .PHONY: all generate 7 | -------------------------------------------------------------------------------- /pkg/grpc/protobuf/validation/test/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package test; 4 | 5 | option go_package=".;test"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | service MyService { 10 | rpc Ping(PingRequest) returns (PingResponse) {} 11 | rpc PingStream(stream PingRequest) returns (stream PingResponse) {} 12 | } 13 | 14 | message PingRequest { 15 | int64 id = 1 [(validate.rules).int64 = { 16 | gt: 0 17 | lt: 100 18 | }]; 19 | } 20 | message PingResponse { 21 | int64 id = 1 [(validate.rules).int64 = { 22 | gt: 0 23 | lt: 100 24 | }]; 25 | } 26 | -------------------------------------------------------------------------------- /pkg/grpc/util_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseFullMethodName_ComponentExtraction(t *testing.T) { 11 | packageName, serviceName, methodName, err := ParseFullMethodName("/internal.v1.test.Service/Method") 12 | require.NoError(t, err) 13 | 14 | assert.Equal(t, "internal.v1.test", packageName) 15 | assert.Equal(t, "Service", serviceName) 16 | assert.Equal(t, "Method", methodName) 17 | } 18 | 19 | func TestParseFullMethodName_Validation(t *testing.T) { 20 | for _, invalidValue := range []string{ 21 | "", 22 | "/internal.v1.test.Service", 23 | "/internal.v1.test.Service/", 24 | "internal.v1.test.Service/Method", 25 | "/internal.v1.test.Service./Method", 26 | "/Service/Method", 27 | } { 28 | _, _, _, err := ParseFullMethodName(invalidValue) 29 | assert.Error(t, err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/lock/lock.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Manager creates and manages locks. Locks produced for a given name 8 | // are re-entrant per Manager. This allows for implementations to 9 | // operate more efficiently (and safely). 10 | // 11 | // As locks are re-entrant per manager, it is strongly advised to use 12 | // a sync.Mutex (or some other state management mechanism) to coordinate 13 | // local concurrency. 14 | type Manager interface { 15 | // Create creates an unlocked DistributedLock for a specific key. 16 | Create(ctx context.Context, name string) (DistributedLock, error) 17 | } 18 | 19 | // DistributedLock is a handle to a distributed lock that spans across multiple 20 | // processes. Two DistributedLock's for the same name produced by the same Manager 21 | // are re-entrant. See Manager for details. 22 | type DistributedLock interface { 23 | // Acquire attempts to acquire the lock, blocking until the lock has been 24 | // successfully acquired. 25 | // 26 | // The returned channel is a channel that will be closed when the lock is lost. 27 | // The lock can be lost when the context is cancelled, Unlock() is called, or 28 | // the underlying implementation detects that the lock _might_ have been lost. 29 | Acquire(ctx context.Context) (<-chan struct{}, error) 30 | 31 | // Unlock unlocks the lock, if the lock is held. 32 | // 33 | // Unlock is idempotent. 34 | Unlock(ctx context.Context) error 35 | 36 | // IsLocked returns whether the lock is held by the process/manager. 37 | IsLocked() bool 38 | } 39 | -------------------------------------------------------------------------------- /pkg/metrics/constants.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const ( 4 | NewRelicContextKey = "newrelic_context" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/metrics/events.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/newrelic/go-agent/v3/newrelic" 7 | ) 8 | 9 | // RecordEvent records a new event with a name and set of key-value pairs 10 | func RecordEvent(ctx context.Context, eventName string, kvPairs map[string]interface{}) { 11 | nr, ok := ctx.Value(NewRelicContextKey).(*newrelic.Application) 12 | if ok { 13 | nr.RecordCustomEvent(eventName, kvPairs) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/newrelic/go-agent/v3/newrelic" 8 | ) 9 | 10 | // RecordCount records a count metric 11 | func RecordCount(ctx context.Context, metricName string, count uint64) { 12 | nr, ok := ctx.Value(NewRelicContextKey).(*newrelic.Application) 13 | if ok { 14 | nr.RecordCustomMetric(metricName, float64(count)) 15 | } 16 | } 17 | 18 | // RecordDuration records a duration metric 19 | func RecordDuration(ctx context.Context, metricName string, duration time.Duration) { 20 | nr, ok := ctx.Value(NewRelicContextKey).(*newrelic.Application) 21 | if ok { 22 | nr.RecordCustomMetric(metricName, float64(duration/time.Millisecond)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/metrics/tracing.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/newrelic/go-agent/v3/newrelic" 8 | ) 9 | 10 | // TraceMethodCall traces a method call with a given struct/package and method names 11 | func TraceMethodCall(ctx context.Context, structOrPackageName, methodName string) *MethodTracer { 12 | txn := newrelic.FromContext(ctx) 13 | if txn == nil { 14 | return nil 15 | } 16 | 17 | seg := txn.StartSegment(fmt.Sprintf("%s %s", structOrPackageName, methodName)) 18 | 19 | return &MethodTracer{ 20 | txn: txn, 21 | seg: seg, 22 | } 23 | } 24 | 25 | // MethodTracer collects analytics for a given method call within an existing 26 | // trace. 27 | type MethodTracer struct { 28 | txn *newrelic.Transaction 29 | seg *newrelic.Segment 30 | } 31 | 32 | // AddAttribute adds a key-value pair metadata to the method trace 33 | func (t *MethodTracer) AddAttribute(key string, value interface{}) { 34 | if t == nil { 35 | return 36 | } 37 | 38 | t.seg.AddAttribute(key, value) 39 | } 40 | 41 | // AddAttributes adds a set of key-value pair metadata to the method trace 42 | func (t *MethodTracer) AddAttributes(attributes map[string]interface{}) { 43 | if t == nil { 44 | return 45 | } 46 | 47 | for key, value := range attributes { 48 | t.seg.AddAttribute(key, value) 49 | } 50 | } 51 | 52 | // OnError observes an error within a method trace 53 | func (t *MethodTracer) OnError(err error) { 54 | if t == nil { 55 | return 56 | } 57 | 58 | if err == nil { 59 | return 60 | } 61 | 62 | t.txn.NoticeError(err) 63 | } 64 | 65 | // End completes the trace for the method call. 66 | func (t *MethodTracer) End() { 67 | if t == nil { 68 | return 69 | } 70 | 71 | t.seg.End() 72 | } 73 | -------------------------------------------------------------------------------- /pkg/netutil/domain.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "golang.org/x/net/idna" 6 | ) 7 | 8 | const ( 9 | maxDomainNameSize = 253 10 | ) 11 | 12 | // ValidateDomainName validates the string value as a domain name 13 | func ValidateDomainName(value string) error { 14 | if len(value) == 0 { 15 | return errors.New("domain name is empty") 16 | } 17 | if len(value) > maxDomainNameSize { 18 | return errors.New("domain name length exceeds limit") 19 | } 20 | if _, err := idna.Registration.ToASCII(value); err != nil { 21 | return errors.Wrap(err, "domain name is invalid") 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/netutil/ip.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // GetOutboundIP gets the locally preferred outbound IP address 8 | // 9 | // From https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go 10 | func GetOutboundIP() net.IP { 11 | conn, err := net.Dial("udp", "8.8.8.8:80") 12 | if err != nil { 13 | panic(err) 14 | } 15 | defer conn.Close() 16 | 17 | localAddr := conn.LocalAddr().(*net.UDPAddr) 18 | 19 | return localAddr.IP 20 | } 21 | -------------------------------------------------------------------------------- /pkg/netutil/port.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | ) 8 | 9 | // GetAvailablePortForAddress returns an open port on the specified address 10 | func GetAvailablePortForAddress(address string) (int32, error) { 11 | server, err := net.Listen("tcp", fmt.Sprintf("%s:0", address)) 12 | if err != nil { 13 | return 0, err 14 | } 15 | defer server.Close() 16 | _, portString, err := net.SplitHostPort(server.Addr().String()) 17 | if err != nil { 18 | return 0, err 19 | } 20 | port, err := strconv.Atoi(portString) 21 | return int32(port), err 22 | } 23 | -------------------------------------------------------------------------------- /pkg/osutil/memory.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | import ( 4 | "io/ioutil" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/pbnjay/memory" 9 | ) 10 | 11 | const ( 12 | // This is the default value for cgroup's limit_in_bytes. This is not a 13 | // valid value and indicates that the memory is not restricted. 14 | // See https://unix.stackexchange.com/questions/420906/what-is-the-value-for-the-cgroups-limit-in-bytes-if-the-memory-is-not-restricte 15 | unrestrictedMemoryLimit = 9223372036854771712 16 | ) 17 | 18 | const ( 19 | dockerMemoryLimitLocation = "/sys/fs/cgroup/memory/memory.limit_in_bytes" 20 | ) 21 | 22 | // GetTotalMemory returns the total available memory size. The call is 23 | // container-aware. 24 | func GetTotalMemory() uint64 { 25 | totalMemory := memory.TotalMemory() 26 | 27 | cgroupLimit, err := ioutil.ReadFile(dockerMemoryLimitLocation) 28 | if err == nil { 29 | dockerMemoryLimit, err := strconv.ParseUint(strings.Replace(string(cgroupLimit), "\n", "", 1), 10, 64) 30 | if err == nil && dockerMemoryLimit != unrestrictedMemoryLimit { 31 | totalMemory = dockerMemoryLimit 32 | } 33 | } 34 | return totalMemory 35 | } 36 | -------------------------------------------------------------------------------- /pkg/rate/limiter.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "sync" 5 | 6 | "golang.org/x/time/rate" 7 | ) 8 | 9 | // Limiter limits operations based on a provided key. 10 | type Limiter interface { 11 | Allow(key string) (bool, error) 12 | } 13 | 14 | // LimiterCtor allows the creation of a Limiter using a provided rate. 15 | type LimiterCtor func(rate float64) Limiter 16 | 17 | type localRateLimiter struct { 18 | limit rate.Limit 19 | 20 | sync.Mutex 21 | limiters map[string]*rate.Limiter 22 | } 23 | 24 | // NewRedisRateLimiter returns an in memory limiter. 25 | func NewLocalRateLimiter(limit rate.Limit) Limiter { 26 | return &localRateLimiter{ 27 | limit: limit, 28 | limiters: make(map[string]*rate.Limiter), 29 | } 30 | } 31 | 32 | // Allow implements limiter.Allow. 33 | func (l *localRateLimiter) Allow(key string) (bool, error) { 34 | l.Lock() 35 | limiter, ok := l.limiters[key] 36 | if !ok { 37 | limiter = rate.NewLimiter(l.limit, int(l.limit)) 38 | l.limiters[key] = limiter 39 | } 40 | l.Unlock() 41 | 42 | return limiter.Allow(), nil 43 | } 44 | 45 | // NoLimiter never limits operations 46 | type NoLimiter struct { 47 | } 48 | 49 | // Allow implements limiter.Allow. 50 | func (n *NoLimiter) Allow(key string) (bool, error) { 51 | return true, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/rate/limiter_test.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/time/rate" 8 | ) 9 | 10 | func TestNoLimiter(t *testing.T) { 11 | l := &NoLimiter{} 12 | for i := 0; i < 10000; i++ { 13 | allowed, err := l.Allow("") 14 | assert.NoError(t, err) 15 | assert.True(t, allowed) 16 | } 17 | } 18 | 19 | func TestLocalRateLimiter(t *testing.T) { 20 | l := NewLocalRateLimiter(rate.Limit(2)) 21 | 22 | for i := 0; i < 2; i++ { 23 | allowed, err := l.Allow("a") 24 | assert.NoError(t, err) 25 | assert.True(t, allowed) 26 | } 27 | 28 | allowed, err := l.Allow("a") 29 | assert.NoError(t, err) 30 | assert.False(t, allowed) 31 | 32 | // Ensure key partitioning is valid 33 | for i := 0; i < 2; i++ { 34 | allowed, err := l.Allow("b") 35 | assert.NoError(t, err) 36 | assert.True(t, allowed) 37 | } 38 | 39 | allowed, err = l.Allow("b") 40 | assert.NoError(t, err) 41 | assert.False(t, allowed) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/retry/backoff/backoff.go: -------------------------------------------------------------------------------- 1 | // Package backoff provides varies backoff strategies for retry. 2 | package backoff 3 | 4 | import ( 5 | "math" 6 | "time" 7 | ) 8 | 9 | // Strategy is a function that provides the amount of time to wait before trying 10 | // again. Note: attempts starts at 1 11 | type Strategy func(attempts uint) time.Duration 12 | 13 | // Constant returns a strategy that always returns the provided duration. 14 | func Constant(interval time.Duration) Strategy { 15 | return func(attempts uint) time.Duration { 16 | return interval 17 | } 18 | } 19 | 20 | // Linear returns a strategy that linearly increases based off of the number of 21 | // attempts. 22 | // 23 | // delay = baseDelay * attempts 24 | // Ex. Linear(2*time.Seconds) = 2s, 4s, 6s, 8s, ... 25 | func Linear(baseDelay time.Duration) Strategy { 26 | return func(attempts uint) time.Duration { 27 | if delay := baseDelay * time.Duration(attempts); delay >= 0 { 28 | return delay 29 | } 30 | 31 | return math.MaxInt64 32 | } 33 | } 34 | 35 | // Exponential returns a strategy that exponentially increases based off of the 36 | // number of attempts. 37 | // 38 | // delay = baseDelay * base^(attempts - 1) 39 | // Ex. Exponential(2*time.Seconds, 3) = 2s, 6s, 18s, 54s, ... 40 | func Exponential(baseDelay time.Duration, base float64) Strategy { 41 | return func(attempts uint) time.Duration { 42 | if delay := baseDelay * time.Duration(math.Pow(base, float64(attempts-1))); delay >= 0 { 43 | return delay 44 | } 45 | 46 | return math.MaxInt64 47 | } 48 | } 49 | 50 | // BinaryExponential returns an Exponential strategy with a base of 2.0 51 | // 52 | // delay = baseDelay * 2^(attempts - 1) 53 | // Ex. BinaryExponential(2*time.Seconds) = 2s, 4s, 8s, 16s, ... 54 | func BinaryExponential(baseDelay time.Duration) Strategy { 55 | return Exponential(baseDelay, 2) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/retry/backoff/backoff_test.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConstant(t *testing.T) { 11 | s := Constant(100 * time.Millisecond) 12 | 13 | for i := uint(1); i < 10; i++ { 14 | assert.Equal(t, 100*time.Millisecond, s(i)) 15 | } 16 | } 17 | 18 | func TestLinear(t *testing.T) { 19 | s := Linear(500 * time.Millisecond) 20 | 21 | assert.Equal(t, 500*time.Millisecond, s(1)) 22 | assert.Equal(t, 1*time.Second, s(2)) 23 | assert.Equal(t, 1*time.Second+500*time.Millisecond, s(3)) 24 | assert.Equal(t, 2*time.Second, s(4)) 25 | assert.Equal(t, 2*time.Second+500*time.Millisecond, s(5)) 26 | } 27 | 28 | func TestExponential(t *testing.T) { 29 | s := Exponential(2*time.Second, 3.0) 30 | 31 | assert.Equal(t, 2*time.Second, s(1)) // 2*3^0 32 | assert.Equal(t, 6*time.Second, s(2)) // 2*3^1 33 | assert.Equal(t, 18*time.Second, s(3)) // 2*3^2 34 | assert.Equal(t, 54*time.Second, s(4)) // 2*3^3 35 | } 36 | 37 | func TestBinaryExponential(t *testing.T) { 38 | exp := Exponential(2*time.Second, 2) 39 | binExp := BinaryExponential(2 * time.Second) 40 | 41 | for i := uint(1); i < 10; i++ { 42 | assert.Equal(t, exp(i), binExp(i)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/solana/binary/utils.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/binary" 6 | ) 7 | 8 | func PutKey32(dst []byte, src []byte, offset *int) { 9 | copy(dst, src) 10 | *offset += ed25519.PublicKeySize 11 | } 12 | 13 | func PutOptionalKey32(dst []byte, src []byte, offset *int) { 14 | if len(src) > 0 { 15 | dst[0] = 1 16 | copy(dst[4:], src) 17 | } 18 | 19 | *offset += 4 + ed25519.PublicKeySize 20 | } 21 | 22 | func PutUint64(dst []byte, v uint64, offset *int) { 23 | binary.LittleEndian.PutUint64(dst, v) 24 | *offset += 8 25 | } 26 | 27 | func PutUint32(dst []byte, v uint32, offset *int) { 28 | binary.LittleEndian.PutUint32(dst, v) 29 | *offset += 4 30 | } 31 | 32 | func PutOptionalUint64(dst []byte, v *uint64, offset *int) { 33 | if v != nil { 34 | dst[0] = 1 35 | binary.LittleEndian.PutUint64(dst[4:], *v) 36 | } 37 | *offset += 4 + 8 38 | } 39 | 40 | func GetKey32(src []byte, dst *ed25519.PublicKey, offset *int) { 41 | *dst = make([]byte, ed25519.PublicKeySize) 42 | copy(*dst, src) 43 | *offset += ed25519.PublicKeySize 44 | } 45 | 46 | func GetOptionalKey32(src []byte, dst *ed25519.PublicKey, offset *int) { 47 | if src[0] == 1 { 48 | *dst = make([]byte, ed25519.PublicKeySize) 49 | copy(*dst, src[4:]) 50 | } 51 | *offset += 4 + ed25519.PublicKeySize 52 | } 53 | 54 | func GetUint64(src []byte, dst *uint64, offset *int) { 55 | *dst = binary.LittleEndian.Uint64(src) 56 | *offset += 8 57 | } 58 | 59 | func GetUint32(src []byte, dst *uint32, offset *int) { 60 | *dst = binary.LittleEndian.Uint32(src) 61 | *offset += 4 62 | } 63 | 64 | func GetOptionalUint64(src []byte, dst **uint64, offset *int) { 65 | if src[0] == 1 { 66 | val := binary.LittleEndian.Uint64(src[4:]) 67 | *dst = &val 68 | } 69 | *offset += 4 + 8 70 | } 71 | -------------------------------------------------------------------------------- /pkg/solana/client_test.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSignatureStatus(t *testing.T) { 10 | zero, one := 0, 1 11 | 12 | testCases := []struct { 13 | s SignatureStatus 14 | confirmed bool 15 | finalized bool 16 | }{ 17 | { 18 | s: SignatureStatus{ 19 | Slot: 10, 20 | ErrorResult: nil, 21 | Confirmations: &zero, 22 | ConfirmationStatus: "", 23 | }, 24 | }, 25 | { 26 | s: SignatureStatus{ 27 | Slot: 10, 28 | ErrorResult: nil, 29 | Confirmations: &zero, 30 | ConfirmationStatus: "random", 31 | }, 32 | }, 33 | { 34 | s: SignatureStatus{ 35 | Slot: 10, 36 | ErrorResult: nil, 37 | Confirmations: &zero, 38 | ConfirmationStatus: confirmationStatusProcessed, 39 | }, 40 | }, 41 | { 42 | s: SignatureStatus{ 43 | Slot: 10, 44 | ErrorResult: nil, 45 | Confirmations: &one, 46 | ConfirmationStatus: "", 47 | }, 48 | confirmed: true, 49 | }, 50 | { 51 | s: SignatureStatus{ 52 | Slot: 10, 53 | ErrorResult: nil, 54 | Confirmations: &zero, 55 | ConfirmationStatus: confirmationStatusConfirmed, 56 | }, 57 | confirmed: true, 58 | }, 59 | { 60 | s: SignatureStatus{ 61 | Slot: 10, 62 | ErrorResult: nil, 63 | Confirmations: &zero, 64 | ConfirmationStatus: confirmationStatusFinalized, 65 | }, 66 | confirmed: true, 67 | finalized: true, 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | assert.Equal(t, tc.confirmed, tc.s.Confirmed()) 73 | assert.Equal(t, tc.finalized, tc.s.Finalized()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/solana/computebudget/program.go: -------------------------------------------------------------------------------- 1 | package compute_budget 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/binary" 6 | "errors" 7 | 8 | "github.com/stickychin/code-server/pkg/solana" 9 | ) 10 | 11 | // ComputeBudget111111111111111111111111111111 12 | var ProgramKey = ed25519.PublicKey{3, 6, 70, 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0} 13 | 14 | const ( 15 | commandRequestUnits uint8 = iota 16 | commandRequestHeapFrame 17 | commandSetComputeUnitLimit 18 | commandSetComputeUnitPrice 19 | ) 20 | 21 | func SetComputeUnitLimit(computeUnitLimit uint32) solana.Instruction { 22 | data := make([]byte, 1+4) 23 | data[0] = commandSetComputeUnitLimit 24 | binary.LittleEndian.PutUint32(data[1:], computeUnitLimit) 25 | 26 | return solana.NewInstruction( 27 | ProgramKey[:], 28 | data, 29 | ) 30 | } 31 | 32 | func SetComputeUnitPrice(computeUnitPrice uint64) solana.Instruction { 33 | data := make([]byte, 1+8) 34 | data[0] = commandSetComputeUnitPrice 35 | binary.LittleEndian.PutUint64(data[1:], computeUnitPrice) 36 | 37 | return solana.NewInstruction( 38 | ProgramKey[:], 39 | data, 40 | ) 41 | } 42 | 43 | func DecompileSetComputeUnitLimitIxnData(data []byte) (uint32, error) { 44 | if len(data) != 5 { 45 | return 0, errors.New("invalid length") 46 | } 47 | 48 | if data[0] != commandSetComputeUnitLimit { 49 | return 0, errors.New("invalid instruction") 50 | } 51 | 52 | return binary.LittleEndian.Uint32(data[1:]), nil 53 | } 54 | 55 | func DecompileSetComputeUnitPriceIxnData(data []byte) (uint64, error) { 56 | if len(data) != 9 { 57 | return 0, errors.New("invalid length") 58 | } 59 | 60 | if data[0] != commandSetComputeUnitPrice { 61 | return 0, errors.New("invalid instruction") 62 | } 63 | 64 | return binary.LittleEndian.Uint64(data[1:]), nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/solana/config.go: -------------------------------------------------------------------------------- 1 | package solana 2 | 3 | type Environment string 4 | 5 | const ( 6 | EnvironmentDev Environment = "https://api.devnet.solana.com" 7 | EnvironmentTest Environment = "https://api.testnet.solana.com" 8 | EnvironmentProd Environment = "https://api.mainnet-beta.solana.com" 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/solana/cvm/accounts_code_vm.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "fmt" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | const ( 12 | CodeVmAccountSize = (8 + //discriminator 13 | 32 + // authority 14 | 32 + // mint 15 | 8 + // slot 16 | HashSize + // poh 17 | TokenPoolSize + // omnibus 18 | 1 + // lock_duration 19 | 1 + // bump 20 | 5) // padding 21 | ) 22 | 23 | var CodeVmAccountDiscriminator = []byte{byte(AccountTypeCodeVm), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 24 | 25 | type CodeVmAccount struct { 26 | Authority ed25519.PublicKey 27 | Mint ed25519.PublicKey 28 | Slot uint64 29 | Poh Hash 30 | Omnibus TokenPool 31 | LockDuration uint8 32 | Bump uint8 33 | } 34 | 35 | func (obj *CodeVmAccount) Unmarshal(data []byte) error { 36 | if len(data) < CodeVmAccountSize { 37 | return ErrInvalidAccountData 38 | } 39 | 40 | var offset int 41 | 42 | var discriminator []byte 43 | getDiscriminator(data, &discriminator, &offset) 44 | if !bytes.Equal(discriminator, CodeVmAccountDiscriminator) { 45 | return ErrInvalidAccountData 46 | } 47 | 48 | getKey(data, &obj.Authority, &offset) 49 | getKey(data, &obj.Mint, &offset) 50 | getUint64(data, &obj.Slot, &offset) 51 | getHash(data, &obj.Poh, &offset) 52 | getTokenPool(data, &obj.Omnibus, &offset) 53 | getUint8(data, &obj.LockDuration, &offset) 54 | getUint8(data, &obj.Bump, &offset) 55 | offset += 5 // padding 56 | 57 | return nil 58 | } 59 | 60 | func (obj *CodeVmAccount) String() string { 61 | return fmt.Sprintf( 62 | "CodeVmAccount{authority=%s,mint=%s,slot=%d,poh=%s,omnibus=%s,lock_duration=%d,bump=%d}", 63 | base58.Encode(obj.Authority), 64 | base58.Encode(obj.Mint), 65 | obj.Slot, 66 | obj.Poh.String(), 67 | obj.Omnibus.String(), 68 | obj.LockDuration, 69 | obj.Bump, 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/solana/cvm/accounts_storage.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | DefaultCompressedStateDepth = 20 5 | 6 | MaxStorageAccountNameSize = 32 7 | ) 8 | 9 | // todo: Define account struct 10 | -------------------------------------------------------------------------------- /pkg/solana/cvm/accounts_unlock_state.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "fmt" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | const ( 12 | UnlockStateAccountSize = (8 + //discriminator 13 | 32 + // vm 14 | 32 + // owner 15 | 32 + // address 16 | 1 + // bump 17 | 1 + // state 18 | 6) // padding 19 | ) 20 | 21 | var UnlockStateAccountDiscriminator = []byte{byte(AccountTypeUnlockState), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 22 | 23 | type UnlockStateAccount struct { 24 | Vm ed25519.PublicKey 25 | Owner ed25519.PublicKey 26 | Address ed25519.PublicKey 27 | UnlockAt int64 28 | Bump uint8 29 | State TimelockState 30 | } 31 | 32 | func (obj *UnlockStateAccount) Unmarshal(data []byte) error { 33 | if len(data) < UnlockStateAccountSize { 34 | return ErrInvalidAccountData 35 | } 36 | 37 | var offset int 38 | 39 | var discriminator []byte 40 | getDiscriminator(data, &discriminator, &offset) 41 | if !bytes.Equal(discriminator, UnlockStateAccountDiscriminator) { 42 | return ErrInvalidAccountData 43 | } 44 | 45 | getKey(data, &obj.Vm, &offset) 46 | getKey(data, &obj.Owner, &offset) 47 | getKey(data, &obj.Address, &offset) 48 | getInt64(data, &obj.UnlockAt, &offset) 49 | getUint8(data, &obj.Bump, &offset) 50 | getTimelockState(data, &obj.State, &offset) 51 | offset += 6 // padding 52 | 53 | return nil 54 | } 55 | 56 | func (obj *UnlockStateAccount) IsUnlocked() bool { 57 | return obj.State == TimelockStateUnlocked 58 | } 59 | 60 | func (obj *UnlockStateAccount) String() string { 61 | return fmt.Sprintf( 62 | "UnlockStateAccount{vm=%s,owner=%s,address=%s,unlock_at=%d,bump=%d,state=%s", 63 | base58.Encode(obj.Vm), 64 | base58.Encode(obj.Owner), 65 | base58.Encode(obj.Address), 66 | obj.UnlockAt, 67 | obj.Bump, 68 | obj.State.String(), 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_compress.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | CompressInstructionArgsSize = (2 + // account_index 11 | SignatureSize) // signature 12 | ) 13 | 14 | type CompressInstructionArgs struct { 15 | AccountIndex uint16 16 | Signature Signature 17 | } 18 | 19 | type CompressInstructionAccounts struct { 20 | VmAuthority ed25519.PublicKey 21 | Vm ed25519.PublicKey 22 | VmMemory ed25519.PublicKey 23 | VmStorage ed25519.PublicKey 24 | } 25 | 26 | func NewCompressInstruction( 27 | accounts *CompressInstructionAccounts, 28 | args *CompressInstructionArgs, 29 | ) solana.Instruction { 30 | var offset int 31 | 32 | // Serialize instruction arguments 33 | data := make([]byte, 1+CompressInstructionArgsSize) 34 | 35 | putCodeInstruction(data, CodeInstructionCompress, &offset) 36 | putUint16(data, args.AccountIndex, &offset) 37 | putSignature(data, args.Signature, &offset) 38 | 39 | return solana.Instruction{ 40 | Program: PROGRAM_ADDRESS, 41 | 42 | // Instruction args 43 | Data: data, 44 | 45 | // Instruction accounts 46 | Accounts: []solana.AccountMeta{ 47 | { 48 | PublicKey: accounts.VmAuthority, 49 | IsWritable: true, 50 | IsSigner: true, 51 | }, 52 | { 53 | PublicKey: accounts.Vm, 54 | IsWritable: true, 55 | IsSigner: false, 56 | }, 57 | { 58 | PublicKey: accounts.VmMemory, 59 | IsWritable: true, 60 | IsSigner: false, 61 | }, 62 | { 63 | PublicKey: accounts.VmStorage, 64 | IsWritable: true, 65 | IsSigner: false, 66 | }, 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_init_nonce.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | InitNonceInstructionArgsSize = 2 // account_index 11 | ) 12 | 13 | type InitNonceInstructionArgs struct { 14 | AccountIndex uint16 15 | } 16 | 17 | type InitNonceInstructionAccounts struct { 18 | VmAuthority ed25519.PublicKey 19 | Vm ed25519.PublicKey 20 | VmMemory ed25519.PublicKey 21 | VirtualAccountOwner ed25519.PublicKey 22 | } 23 | 24 | func NewInitNonceInstruction( 25 | accounts *InitNonceInstructionAccounts, 26 | args *InitNonceInstructionArgs, 27 | ) solana.Instruction { 28 | var offset int 29 | 30 | // Serialize instruction arguments 31 | data := make([]byte, 1+InitNonceInstructionArgsSize) 32 | 33 | putCodeInstruction(data, CodeInstructionInitNonce, &offset) 34 | putUint16(data, args.AccountIndex, &offset) 35 | 36 | return solana.Instruction{ 37 | Program: PROGRAM_ADDRESS, 38 | 39 | // Instruction args 40 | Data: data, 41 | 42 | // Instruction accounts 43 | Accounts: []solana.AccountMeta{ 44 | { 45 | PublicKey: accounts.VmAuthority, 46 | IsWritable: true, 47 | IsSigner: true, 48 | }, 49 | { 50 | PublicKey: accounts.Vm, 51 | IsWritable: true, 52 | IsSigner: false, 53 | }, 54 | { 55 | PublicKey: accounts.VmMemory, 56 | IsWritable: true, 57 | IsSigner: false, 58 | }, 59 | { 60 | PublicKey: accounts.VirtualAccountOwner, 61 | IsWritable: false, 62 | IsSigner: false, 63 | }, 64 | }, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_init_storage.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | MaxStorageAccountNameLength = 32 11 | ) 12 | 13 | const ( 14 | InitStorageInstructionArgsSize = (MaxStorageAccountNameLength + // name 15 | 1) // vm_storage_bump 16 | ) 17 | 18 | type InitStorageInstructionArgs struct { 19 | Name string 20 | VmStorageBump uint8 21 | } 22 | 23 | type InitStorageInstructionAccounts struct { 24 | VmAuthority ed25519.PublicKey 25 | Vm ed25519.PublicKey 26 | VmStorage ed25519.PublicKey 27 | } 28 | 29 | func NewInitStorageInstruction( 30 | accounts *InitStorageInstructionAccounts, 31 | args *InitStorageInstructionArgs, 32 | ) solana.Instruction { 33 | var offset int 34 | 35 | // Serialize instruction arguments 36 | data := make([]byte, 1+InitStorageInstructionArgsSize) 37 | 38 | putCodeInstruction(data, CodeInstructionInitStorage, &offset) 39 | putFixedString(data, args.Name, MaxStorageAccountNameLength, &offset) 40 | putUint8(data, args.VmStorageBump, &offset) 41 | 42 | return solana.Instruction{ 43 | Program: PROGRAM_ADDRESS, 44 | 45 | // Instruction args 46 | Data: data, 47 | 48 | // Instruction accounts 49 | Accounts: []solana.AccountMeta{ 50 | { 51 | PublicKey: accounts.VmAuthority, 52 | IsWritable: true, 53 | IsSigner: true, 54 | }, 55 | { 56 | PublicKey: accounts.Vm, 57 | IsWritable: true, 58 | IsSigner: false, 59 | }, 60 | { 61 | PublicKey: accounts.VmStorage, 62 | IsWritable: true, 63 | IsSigner: false, 64 | }, 65 | { 66 | PublicKey: SYSTEM_PROGRAM_ID, 67 | IsWritable: false, 68 | IsSigner: false, 69 | }, 70 | { 71 | PublicKey: SYSVAR_RENT_PUBKEY, 72 | IsWritable: false, 73 | IsSigner: false, 74 | }, 75 | }, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_init_unlock.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | InitUnlockInstructionArgsSize = 0 11 | ) 12 | 13 | type InitUnlockInstructionArgs struct { 14 | } 15 | 16 | type InitUnlockInstructionAccounts struct { 17 | AccountOwner ed25519.PublicKey 18 | Payer ed25519.PublicKey 19 | Vm ed25519.PublicKey 20 | UnlockState ed25519.PublicKey 21 | } 22 | 23 | func NewInitUnlockInstruction( 24 | accounts *InitUnlockInstructionAccounts, 25 | args *InitUnlockInstructionArgs, 26 | ) solana.Instruction { 27 | var offset int 28 | 29 | // Serialize instruction arguments 30 | data := make([]byte, 1+InitUnlockInstructionArgsSize) 31 | 32 | putCodeInstruction(data, CodeInstructionInitUnlock, &offset) 33 | 34 | return solana.Instruction{ 35 | Program: PROGRAM_ADDRESS, 36 | 37 | // Instruction args 38 | Data: data, 39 | 40 | // Instruction accounts 41 | Accounts: []solana.AccountMeta{ 42 | { 43 | PublicKey: accounts.AccountOwner, 44 | IsWritable: true, 45 | IsSigner: true, 46 | }, 47 | { 48 | PublicKey: accounts.Payer, 49 | IsWritable: true, 50 | IsSigner: true, 51 | }, 52 | { 53 | PublicKey: accounts.Vm, 54 | IsWritable: true, 55 | IsSigner: false, 56 | }, 57 | { 58 | PublicKey: accounts.UnlockState, 59 | IsWritable: true, 60 | IsSigner: false, 61 | }, 62 | { 63 | PublicKey: SYSTEM_PROGRAM_ID, 64 | IsWritable: false, 65 | IsSigner: false, 66 | }, 67 | { 68 | PublicKey: SYSVAR_RENT_PUBKEY, 69 | IsWritable: false, 70 | IsSigner: false, 71 | }, 72 | }, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_resize_memory.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | ResizeMemoryInstructionArgsSize = 4 // account_size 11 | ) 12 | 13 | type ResizeMemoryInstructionArgs struct { 14 | AccountSize uint32 15 | } 16 | 17 | type ResizeMemoryInstructionAccounts struct { 18 | VmAuthority ed25519.PublicKey 19 | Vm ed25519.PublicKey 20 | VmMemory ed25519.PublicKey 21 | } 22 | 23 | func NewResizeMemoryInstruction( 24 | accounts *ResizeMemoryInstructionAccounts, 25 | args *ResizeMemoryInstructionArgs, 26 | ) solana.Instruction { 27 | var offset int 28 | 29 | // Serialize instruction arguments 30 | data := make([]byte, 1+ResizeMemoryInstructionArgsSize) 31 | 32 | putCodeInstruction(data, CodeInstructionResizeMemory, &offset) 33 | putUint32(data, args.AccountSize, &offset) 34 | 35 | return solana.Instruction{ 36 | Program: PROGRAM_ADDRESS, 37 | 38 | // Instruction args 39 | Data: data, 40 | 41 | // Instruction accounts 42 | Accounts: []solana.AccountMeta{ 43 | { 44 | PublicKey: accounts.VmAuthority, 45 | IsWritable: true, 46 | IsSigner: true, 47 | }, 48 | { 49 | PublicKey: accounts.Vm, 50 | IsWritable: true, 51 | IsSigner: false, 52 | }, 53 | { 54 | PublicKey: accounts.VmMemory, 55 | IsWritable: true, 56 | IsSigner: false, 57 | }, 58 | { 59 | PublicKey: SYSTEM_PROGRAM_ID, 60 | IsWritable: false, 61 | IsSigner: false, 62 | }, 63 | { 64 | PublicKey: SYSVAR_RENT_PUBKEY, 65 | IsWritable: false, 66 | IsSigner: false, 67 | }, 68 | }, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/solana/cvm/instructions_snapshot.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | const ( 10 | RelaySaveRecentRootInstructionArgsSize = 0 11 | ) 12 | 13 | type RelaySaveRecentRootInstructionArgs struct { 14 | } 15 | 16 | type RelaySaveRecentRootInstructionAccounts struct { 17 | VmAuthority ed25519.PublicKey 18 | Vm ed25519.PublicKey 19 | Relay ed25519.PublicKey 20 | } 21 | 22 | func NewRelaySaveRecentRootInstruction( 23 | accounts *RelaySaveRecentRootInstructionAccounts, 24 | args *RelaySaveRecentRootInstructionArgs, 25 | ) solana.Instruction { 26 | var offset int 27 | 28 | // Serialize instruction arguments 29 | data := make([]byte, 1+RelaySaveRecentRootInstructionArgsSize) 30 | 31 | putCodeInstruction(data, CodeInstructionSnapshot, &offset) 32 | 33 | return solana.Instruction{ 34 | Program: PROGRAM_ADDRESS, 35 | 36 | // Instruction args 37 | Data: data, 38 | 39 | // Instruction accounts 40 | Accounts: []solana.AccountMeta{ 41 | { 42 | PublicKey: accounts.VmAuthority, 43 | IsWritable: true, 44 | IsSigner: true, 45 | }, 46 | { 47 | PublicKey: accounts.Vm, 48 | IsWritable: true, 49 | IsSigner: false, 50 | }, 51 | { 52 | PublicKey: accounts.Relay, 53 | IsWritable: true, 54 | IsSigner: false, 55 | }, 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/solana/cvm/program.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrInvalidProgram = errors.New("invalid program id") 10 | ErrInvalidAccountData = errors.New("unexpected account data") 11 | ErrInvalidVirtualAccountData = errors.New("unexpected virtual account data") 12 | ErrInvalidVirtualAccountType = errors.New("unexpected virtual account type") 13 | ErrInvalidInstructionData = errors.New("unexpected instruction data") 14 | ) 15 | 16 | var ( 17 | PROGRAM_ADDRESS = mustBase58Decode("vmZ1WUq8SxjBWcaeTCvgJRZbS84R61uniFsQy5YMRTJ") 18 | PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) 19 | ) 20 | 21 | var ( 22 | SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) 23 | SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) 24 | TIMELOCK_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("time2Z2SCnn3qYg3ULKVtdkh8YmZ5jFdKicnA1W2YnJ")) 25 | SPLITTER_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("spLit2eb13Tz93if6aJM136nUWki5PVUsoEjcUjwpwW")) 26 | 27 | SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/solana/cvm/transaction.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | ) 6 | 7 | func getOptionalAccountMetaAddress(account *ed25519.PublicKey) ed25519.PublicKey { 8 | if account != nil { 9 | return *account 10 | } 11 | return PROGRAM_ID 12 | } 13 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_account_type.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type AccountType uint8 4 | 5 | const ( 6 | AccountTypeUnknown AccountType = iota 7 | AccountTypeCodeVm 8 | AccountTypeMemory 9 | AccountTypeStorage 10 | AccountTypeRelay 11 | AccountTypeUnlockState 12 | AccountTypeWithdrawReceipt 13 | ) 14 | 15 | func putAccountType(dst []byte, v AccountType, offset *int) { 16 | dst[*offset] = uint8(v) 17 | *offset += 1 18 | } 19 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_code_instruction.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type CodeInstruction uint8 4 | 5 | const ( 6 | Unknown CodeInstruction = iota 7 | 8 | CodeInstructionInitVm 9 | CodeInstructionInitMemory 10 | CodeInstructionInitStorage 11 | CodeInstructionInitRelay 12 | CodeInstructionInitNonce 13 | CodeInstructionInitTimelock 14 | CodeInstructionInitUnlock 15 | 16 | CodeInstructionExec 17 | CodeInstructionCompress 18 | CodeInstructionDecompress 19 | 20 | CodeInstructionResizeMemory 21 | CodeInstructionSnapshot 22 | 23 | CodeInstructionDeposit 24 | CodeInstructionWithdraw 25 | CodeInstructionUnlockI 26 | ) 27 | 28 | func putCodeInstruction(dst []byte, v CodeInstruction, offset *int) { 29 | dst[*offset] = uint8(v) 30 | *offset += 1 31 | } 32 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_hash.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | const HashSize = 32 11 | 12 | type Hash [HashSize]byte 13 | 14 | func (h Hash) String() string { 15 | return hex.EncodeToString(h[:]) 16 | } 17 | 18 | func putHash(dst []byte, v Hash, offset *int) { 19 | copy(dst[*offset:], v[:]) 20 | *offset += HashSize 21 | } 22 | func getHash(src []byte, dst *Hash, offset *int) { 23 | copy(dst[:], src[*offset:]) 24 | *offset += HashSize 25 | } 26 | 27 | type HashArray []Hash 28 | 29 | func getStaticHashArray(src []byte, dst *HashArray, length int, offset *int) { 30 | *dst = make([]Hash, length) 31 | for i := 0; i < int(length); i++ { 32 | getHash(src, &(*dst)[i], offset) 33 | } 34 | } 35 | 36 | func putHashArray(dst []byte, v HashArray, offset *int) { 37 | binary.LittleEndian.PutUint32(dst[*offset:], uint32(len(v))) 38 | *offset += 4 39 | 40 | for _, hash := range v { 41 | copy(dst[*offset:], hash[:]) 42 | *offset += HashSize 43 | } 44 | } 45 | 46 | func (h HashArray) String() string { 47 | stringValues := make([]string, len(h)) 48 | for i := 0; i < len(h); i++ { 49 | stringValues[i] = h[i].String() 50 | } 51 | return fmt.Sprintf("[%s]", strings.Join(stringValues, ",")) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_item_state.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ItemState uint8 9 | 10 | const ( 11 | ItemStateEmpty ItemState = iota 12 | ItemStateAllocated 13 | ) 14 | 15 | const ( 16 | ItemStateSize = 1 17 | ) 18 | 19 | func getItemState(src []byte, dst *ItemState, offset *int) { 20 | *dst = ItemState(src[*offset]) 21 | *offset += 1 22 | } 23 | 24 | func (obj ItemState) String() string { 25 | switch obj { 26 | case ItemStateEmpty: 27 | return "empty" 28 | case ItemStateAllocated: 29 | return "allocated" 30 | } 31 | return "empty" 32 | } 33 | 34 | type ItemStateArray []ItemState 35 | 36 | func getStaticItemStateArray(src []byte, dst *ItemStateArray, length int, offset *int) { 37 | *dst = make([]ItemState, length) 38 | for i := 0; i < int(length); i++ { 39 | getItemState(src, &(*dst)[i], offset) 40 | } 41 | } 42 | 43 | func (obj ItemStateArray) String() string { 44 | stringValues := make([]string, len(obj)) 45 | for i := 0; i < len(obj); i++ { 46 | stringValues[i] = obj[i].String() 47 | } 48 | return fmt.Sprintf("[%s]", strings.Join(stringValues, ",")) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_memory_allocator.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | MemoryV0NumAccounts = 32_000 5 | ) 6 | 7 | type MemoryAllocator interface { 8 | IsAllocated(index int) bool 9 | 10 | Read(index int) ([]byte, bool) 11 | 12 | String() string 13 | } 14 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_memory_layout.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type MemoryLayout uint8 4 | 5 | const ( 6 | MemoryLayoutUnknown MemoryLayout = iota 7 | MemoryLayoutTimelock 8 | MemoryLayoutNonce 9 | MemoryLayoutRelay 10 | ) 11 | 12 | func putMemoryLayout(dst []byte, v MemoryLayout, offset *int) { 13 | dst[*offset] = uint8(v) 14 | *offset += 1 15 | } 16 | func getMemoryLayout(src []byte, dst *MemoryLayout, offset *int) { 17 | *dst = MemoryLayout(src[*offset]) 18 | *offset += 1 19 | } 20 | 21 | func GetPageSizeFromMemoryLayout(layout MemoryLayout) uint32 { 22 | switch layout { 23 | case MemoryLayoutNonce: 24 | return GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce) 25 | case MemoryLayoutTimelock: 26 | return GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock) 27 | case MemoryLayoutRelay: 28 | return GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay) 29 | } 30 | return 0 31 | } 32 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_memory_version.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type MemoryVersion uint8 4 | 5 | const ( 6 | MemoryVersionV0 MemoryVersion = iota 7 | MemoryVersionV1 8 | ) 9 | 10 | func putMemoryVersion(dst []byte, v MemoryVersion, offset *int) { 11 | dst[*offset] = uint8(v) 12 | *offset += 1 13 | } 14 | func getMemoryVersion(src []byte, dst *MemoryVersion, offset *int) { 15 | *dst = MemoryVersion(src[*offset]) 16 | *offset += 1 17 | } 18 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_merkle_tree.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import "fmt" 4 | 5 | const ( 6 | minMerkleTreeSize = (HashSize + // root 7 | 8) // next_index 8 | ) 9 | 10 | var ( 11 | MerkleTreePrefix = []byte("merkletree") 12 | ) 13 | 14 | type MerkleTree struct { 15 | Root Hash 16 | FilledSubtrees HashArray 17 | ZeroValues HashArray 18 | NextIndex uint64 19 | } 20 | 21 | func (obj *MerkleTree) Unmarshal(data []byte, levels int) error { 22 | if len(data) < GetMerkleTreeSize(levels) { 23 | return ErrInvalidAccountData 24 | } 25 | 26 | var offset int 27 | 28 | getHash(data, &obj.Root, &offset) 29 | getStaticHashArray(data, &obj.FilledSubtrees, levels, &offset) 30 | getStaticHashArray(data, &obj.ZeroValues, levels, &offset) 31 | getUint64(data, &obj.NextIndex, &offset) 32 | 33 | return nil 34 | } 35 | 36 | func (obj *MerkleTree) String() string { 37 | return fmt.Sprintf( 38 | "MerkleTree{root=%s,filled_subtrees=%s,zero_values=%s,next_index=%d}", 39 | obj.Root.String(), 40 | obj.FilledSubtrees.String(), 41 | obj.ZeroValues.String(), 42 | obj.NextIndex, 43 | ) 44 | } 45 | 46 | func getMerkleTree(src []byte, dst *MerkleTree, levels int, offset *int) { 47 | dst.Unmarshal(src[*offset:], levels) 48 | *offset += GetMerkleTreeSize(int(levels)) 49 | } 50 | 51 | func GetMerkleTreeSize(levels int) int { 52 | return (minMerkleTreeSize + 53 | levels*HashSize + // filled_subtrees 54 | levels*HashSize) // zero_values 55 | } 56 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_opcode.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type Opcode uint8 4 | 5 | const ( 6 | OpcodeUnknown Opcode = 0 7 | 8 | OpcodeTransfer Opcode = 11 9 | OpcodeWithdraw Opcode = 14 10 | OpcodeRelay Opcode = 21 11 | 12 | OpcodeExternalTransfer Opcode = 10 13 | OpcodeExternalWithdraw Opcode = 13 14 | OpcodeExternalRelay Opcode = 20 15 | 16 | OpcodeConditionalTransfer Opcode = 12 17 | 18 | OpcodeAirdrop Opcode = 30 19 | ) 20 | 21 | func putOpcode(dst []byte, v Opcode, offset *int) { 22 | dst[*offset] = uint8(v) 23 | *offset += 1 24 | } 25 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_recent_roots.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | minRecentRootsSize = (1 + // offset 9 | 1 + // num_items 10 | 6) // padding 11 | ) 12 | 13 | type RecentRoots struct { 14 | Items HashArray 15 | Offset uint8 16 | NumItems uint8 17 | } 18 | 19 | func (obj *RecentRoots) Unmarshal(data []byte, length int) error { 20 | if len(data) < 1 { 21 | return ErrInvalidAccountData 22 | } 23 | 24 | var offset int 25 | 26 | getStaticHashArray(data, &obj.Items, length, &offset) 27 | getUint8(data, &obj.Offset, &offset) 28 | getUint8(data, &obj.NumItems, &offset) 29 | offset += 6 // padding 30 | 31 | return nil 32 | } 33 | 34 | func (obj *RecentRoots) String() string { 35 | return fmt.Sprintf( 36 | "RecentRoots{items=%s,offset=%d,num_items=%d}", 37 | obj.Items.String(), 38 | obj.Offset, 39 | obj.NumItems, 40 | ) 41 | } 42 | 43 | func getRecentRoots(src []byte, dst *RecentRoots, length int, offset *int) { 44 | dst.Unmarshal(src[*offset:], length) 45 | *offset += GetRecentRootsSize(len(dst.Items)) 46 | } 47 | 48 | func GetRecentRootsSize(length int) int { 49 | return (minRecentRootsSize + 50 | length*HashSize) // items 51 | } 52 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_signature.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "github.com/mr-tron/base58" 5 | ) 6 | 7 | const SignatureSize = 64 8 | 9 | type Signature [SignatureSize]byte 10 | 11 | func (s Signature) String() string { 12 | return base58.Encode(s[:]) 13 | } 14 | 15 | func putSignature(dst []byte, v Signature, offset *int) { 16 | copy(dst[*offset:], v[:]) 17 | *offset += SignatureSize 18 | } 19 | func getSignature(src []byte, dst *Signature, offset *int) { 20 | copy(dst[:], src[*offset:]) 21 | *offset += SignatureSize 22 | } 23 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_timelock_state.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type TimelockState uint8 4 | 5 | const ( 6 | TimelockStateUnknown TimelockState = iota 7 | TimelockStateUnlocked 8 | TimelockStateWaitingForTimeout 9 | ) 10 | 11 | func getTimelockState(src []byte, dst *TimelockState, offset *int) { 12 | *dst = TimelockState(src[*offset]) 13 | *offset += 1 14 | } 15 | 16 | func (s TimelockState) String() string { 17 | switch s { 18 | case TimelockStateUnlocked: 19 | return "unlocked" 20 | case TimelockStateWaitingForTimeout: 21 | return "waiting_for_timeout" 22 | } 23 | return "unknown" 24 | } 25 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_token_pool.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | 7 | "github.com/mr-tron/base58" 8 | ) 9 | 10 | const ( 11 | TokenPoolSize = (32 + // vault 12 | 1) // vault_bump 13 | ) 14 | 15 | type TokenPool struct { 16 | Vault ed25519.PublicKey 17 | VaultBump uint8 18 | } 19 | 20 | func (obj *TokenPool) Unmarshal(data []byte) error { 21 | if len(data) < TokenPoolSize { 22 | return ErrInvalidAccountData 23 | } 24 | 25 | var offset int 26 | 27 | getKey(data, &obj.Vault, &offset) 28 | getUint8(data, &obj.VaultBump, &offset) 29 | 30 | return nil 31 | } 32 | 33 | func (obj *TokenPool) String() string { 34 | return fmt.Sprintf( 35 | "TokenPool{vault=%s,vault_bump=%d}", 36 | base58.Encode(obj.Vault), 37 | obj.VaultBump, 38 | ) 39 | } 40 | 41 | func getTokenPool(src []byte, dst *TokenPool, offset *int) { 42 | dst.Unmarshal(src[*offset:]) 43 | *offset += TokenPoolSize 44 | } 45 | -------------------------------------------------------------------------------- /pkg/solana/cvm/types_virtual_account_type.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import "errors" 4 | 5 | type VirtualAccountType uint8 6 | 7 | const ( 8 | VirtualAccountTypeDurableNonce VirtualAccountType = iota 9 | VirtualAccountTypeTimelock 10 | VirtualAccountTypeRelay 11 | ) 12 | 13 | func GetVirtualAccountTypeFromMemoryLayout(layout MemoryLayout) (VirtualAccountType, error) { 14 | switch layout { 15 | case MemoryLayoutNonce: 16 | return VirtualAccountTypeDurableNonce, nil 17 | case MemoryLayoutTimelock: 18 | return VirtualAccountTypeTimelock, nil 19 | case MemoryLayoutRelay: 20 | return VirtualAccountTypeRelay, nil 21 | default: 22 | return 0, errors.New("memory layout doesn't have a defined virtual account type") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_accounts.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | func GetVirtualAccountSize(accountType VirtualAccountType) uint32 { 4 | switch accountType { 5 | case VirtualAccountTypeDurableNonce: 6 | return VirtualDurableNonceSize 7 | case VirtualAccountTypeRelay: 8 | return VirtualRelayAccountSize 9 | case VirtualAccountTypeTimelock: 10 | return VirtualTimelockAccountSize 11 | default: 12 | return 0 13 | } 14 | } 15 | 16 | func GetVirtualAccountSizeInMemory(accountType VirtualAccountType) uint32 { 17 | return GetVirtualAccountSize(accountType) + 1 18 | } 19 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_accounts_durable_nonce.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | 7 | "github.com/mr-tron/base58" 8 | ) 9 | 10 | const VirtualDurableNonceSize = (32 + // address 11 | 32) // hash 12 | 13 | type VirtualDurableNonce struct { 14 | Address ed25519.PublicKey 15 | Value Hash 16 | } 17 | 18 | func (obj *VirtualDurableNonce) Marshal() []byte { 19 | data := make([]byte, VirtualDurableNonceSize) 20 | 21 | var offset int 22 | 23 | putKey(data, obj.Address, &offset) 24 | putHash(data, obj.Value, &offset) 25 | 26 | return data 27 | } 28 | 29 | func (obj *VirtualDurableNonce) UnmarshalDirectly(data []byte) error { 30 | if len(data) < VirtualDurableNonceSize { 31 | return ErrInvalidVirtualAccountData 32 | } 33 | 34 | var offset int 35 | 36 | getKey(data, &obj.Address, &offset) 37 | getHash(data, &obj.Value, &offset) 38 | 39 | return nil 40 | } 41 | 42 | func (obj *VirtualDurableNonce) UnmarshalFromMemory(data []byte) error { 43 | if len(data) == 0 { 44 | return ErrInvalidVirtualAccountData 45 | } 46 | 47 | if data[0] != uint8(VirtualAccountTypeDurableNonce) { 48 | return ErrInvalidVirtualAccountType 49 | } 50 | 51 | return obj.UnmarshalDirectly(data[1:]) 52 | } 53 | 54 | func (obj *VirtualDurableNonce) String() string { 55 | return fmt.Sprintf( 56 | "VirtualDurableNonce{address=%s,value=%s}", 57 | base58.Encode(obj.Address), 58 | obj.Value.String(), 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_accounts_relay_account.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | 7 | "github.com/mr-tron/base58" 8 | ) 9 | 10 | const VirtualRelayAccountSize = (32 + // target 11 | 32) // destination 12 | 13 | type VirtualRelayAccount struct { 14 | Target ed25519.PublicKey 15 | Destination ed25519.PublicKey 16 | } 17 | 18 | func (obj *VirtualRelayAccount) Marshal() []byte { 19 | data := make([]byte, VirtualRelayAccountSize) 20 | 21 | var offset int 22 | 23 | putKey(data, obj.Target, &offset) 24 | putKey(data, obj.Destination, &offset) 25 | 26 | return data 27 | } 28 | 29 | func (obj *VirtualRelayAccount) UnmarshalDirectly(data []byte) error { 30 | if len(data) < VirtualRelayAccountSize { 31 | return ErrInvalidVirtualAccountData 32 | } 33 | 34 | var offset int 35 | 36 | getKey(data, &obj.Target, &offset) 37 | getKey(data, &obj.Destination, &offset) 38 | 39 | return nil 40 | } 41 | 42 | func (obj *VirtualRelayAccount) UnmarshalFromMemory(data []byte) error { 43 | if len(data) == 0 { 44 | return ErrInvalidVirtualAccountData 45 | } 46 | 47 | if data[0] != uint8(VirtualAccountTypeRelay) { 48 | return ErrInvalidVirtualAccountType 49 | } 50 | 51 | return obj.UnmarshalDirectly(data[1:]) 52 | } 53 | 54 | func (obj *VirtualRelayAccount) String() string { 55 | return fmt.Sprintf( 56 | "VirtualRelayAccount{target=%s,destination=%s}", 57 | base58.Encode(obj.Target), 58 | base58.Encode(obj.Destination), 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | type VirtualInstruction struct { 4 | Opcode Opcode 5 | Data []byte 6 | } 7 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_airdrop.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | AirdropVirtrualInstructionDataSize = (SignatureSize + // signature 5 | 8 + // amount 6 | 1) // count 7 | ) 8 | 9 | type AirdropVirtualInstructionArgs struct { 10 | Amount uint64 11 | Count uint8 12 | Signature Signature 13 | } 14 | 15 | func NewAirdropVirtualInstruction( 16 | args *AirdropVirtualInstructionArgs, 17 | ) VirtualInstruction { 18 | var offset int 19 | data := make([]byte, AirdropVirtrualInstructionDataSize) 20 | putSignature(data, args.Signature, &offset) 21 | putUint64(data, args.Amount, &offset) 22 | putUint8(data, args.Count, &offset) 23 | 24 | return VirtualInstruction{ 25 | Opcode: OpcodeAirdrop, 26 | Data: data, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_conditional_transfer.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | ConditionalTransferVirtrualInstructionDataSize = (SignatureSize + // signature 5 | 8) // amount 6 | ) 7 | 8 | type ConditionalTransferVirtualInstructionArgs struct { 9 | Amount uint64 10 | Signature Signature 11 | } 12 | 13 | func NewConditionalTransferVirtualInstruction( 14 | args *ConditionalTransferVirtualInstructionArgs, 15 | ) VirtualInstruction { 16 | var offset int 17 | data := make([]byte, ConditionalTransferVirtrualInstructionDataSize) 18 | putSignature(data, args.Signature, &offset) 19 | putUint64(data, args.Amount, &offset) 20 | 21 | return VirtualInstruction{ 22 | Opcode: OpcodeConditionalTransfer, 23 | Data: data, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_external_relay.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | ExternalRelayVirtrualInstructionDataSize = (8 + // amount 5 | HashSize + // transcript 6 | HashSize + // recent_root 7 | HashSize) // commitment 8 | ) 9 | 10 | type ExternalRelayVirtualInstructionArgs struct { 11 | Amount uint64 12 | Transcript Hash 13 | RecentRoot Hash 14 | Commitment Hash 15 | } 16 | 17 | func NewExternalRelayVirtualInstruction( 18 | args *ExternalRelayVirtualInstructionArgs, 19 | ) VirtualInstruction { 20 | var offset int 21 | data := make([]byte, ExternalRelayVirtrualInstructionDataSize) 22 | 23 | putUint64(data, args.Amount, &offset) 24 | putHash(data, args.Transcript, &offset) 25 | putHash(data, args.RecentRoot, &offset) 26 | putHash(data, args.Commitment, &offset) 27 | 28 | return VirtualInstruction{ 29 | Opcode: OpcodeExternalRelay, 30 | Data: data, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_external_transfer.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | ExternalTransferVirtrualInstructionDataSize = (SignatureSize + // signature 5 | 8) // amount 6 | ) 7 | 8 | type ExternalTransferVirtualInstructionArgs struct { 9 | Amount uint64 10 | Signature Signature 11 | } 12 | 13 | func NewExternalTransferVirtualInstruction( 14 | args *TransferVirtualInstructionArgs, 15 | ) VirtualInstruction { 16 | var offset int 17 | data := make([]byte, ExternalTransferVirtrualInstructionDataSize) 18 | putSignature(data, args.Signature, &offset) 19 | putUint64(data, args.Amount, &offset) 20 | 21 | return VirtualInstruction{ 22 | Opcode: OpcodeExternalTransfer, 23 | Data: data, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_external_withdraw.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | ExternalWithdrawVirtrualInstructionDataSize = SignatureSize // signature 5 | ) 6 | 7 | type ExternalWithdrawVirtualInstructionArgs struct { 8 | Signature Signature 9 | } 10 | 11 | func NewExternalWithdrawVirtualInstruction( 12 | args *ExternalWithdrawVirtualInstructionArgs, 13 | ) VirtualInstruction { 14 | var offset int 15 | data := make([]byte, ExternalWithdrawVirtrualInstructionDataSize) 16 | putSignature(data, args.Signature, &offset) 17 | 18 | return VirtualInstruction{ 19 | Opcode: OpcodeExternalWithdraw, 20 | Data: data, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_relay.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | RelayVirtrualInstructionDataSize = (8 + // amount 5 | HashSize + // transcript 6 | HashSize + // recent_root 7 | HashSize) // commitment 8 | ) 9 | 10 | type RelayVirtualInstructionArgs struct { 11 | Amount uint64 12 | Transcript Hash 13 | RecentRoot Hash 14 | Commitment Hash 15 | } 16 | 17 | func NewRelayVirtualInstruction( 18 | args *RelayVirtualInstructionArgs, 19 | ) VirtualInstruction { 20 | var offset int 21 | data := make([]byte, RelayVirtrualInstructionDataSize) 22 | 23 | putUint64(data, args.Amount, &offset) 24 | putHash(data, args.Transcript, &offset) 25 | putHash(data, args.RecentRoot, &offset) 26 | putHash(data, args.Commitment, &offset) 27 | 28 | return VirtualInstruction{ 29 | Opcode: OpcodeRelay, 30 | Data: data, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_transfer.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | TransferVirtrualInstructionDataSize = (SignatureSize + // signature 5 | 8) // amount 6 | ) 7 | 8 | type TransferVirtualInstructionArgs struct { 9 | Amount uint64 10 | Signature Signature 11 | } 12 | 13 | func NewTransferVirtualInstruction( 14 | args *TransferVirtualInstructionArgs, 15 | ) VirtualInstruction { 16 | var offset int 17 | data := make([]byte, TransferVirtrualInstructionDataSize) 18 | putSignature(data, args.Signature, &offset) 19 | putUint64(data, args.Amount, &offset) 20 | 21 | return VirtualInstruction{ 22 | Opcode: OpcodeTransfer, 23 | Data: data, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/solana/cvm/virtual_instructions_withdraw.go: -------------------------------------------------------------------------------- 1 | package cvm 2 | 3 | const ( 4 | WithdrawVirtrualInstructionDataSize = SignatureSize // signature 5 | ) 6 | 7 | type WithdrawVirtualInstructionArgs struct { 8 | Signature Signature 9 | } 10 | 11 | func NewWithdrawVirtualInstruction( 12 | args *WithdrawVirtualInstructionArgs, 13 | ) VirtualInstruction { 14 | var offset int 15 | data := make([]byte, WithdrawVirtrualInstructionDataSize) 16 | putSignature(data, args.Signature, &offset) 17 | 18 | return VirtualInstruction{ 19 | Opcode: OpcodeWithdraw, 20 | Data: data, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/solana/memo/program.go: -------------------------------------------------------------------------------- 1 | package memo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/stickychin/code-server/pkg/solana" 10 | ) 11 | 12 | // ProgramKey is the address of the memo program that should be used. 13 | // 14 | // Current key: Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo 15 | // 16 | // todo: lock this in, or make configurable 17 | var ProgramKey = ed25519.PublicKey{5, 74, 83, 80, 248, 93, 200, 130, 214, 20, 165, 86, 114, 120, 138, 41, 109, 223, 30, 171, 171, 208, 166, 6, 120, 136, 73, 50, 244, 238, 246, 160} 18 | 19 | // Reference: https://github.com/solana-labs/solana-program-library/blob/master/memo/program/src/entrypoint.rs 20 | func Instruction(data string) solana.Instruction { 21 | return solana.NewInstruction( 22 | ProgramKey, 23 | []byte(data), 24 | ) 25 | } 26 | 27 | type DecompiledMemo struct { 28 | Data []byte 29 | } 30 | 31 | func DecompileMemo(m solana.Message, index int) (*DecompiledMemo, error) { 32 | if index >= len(m.Instructions) { 33 | return nil, errors.Errorf("instruction doesn't exist at %d", index) 34 | } 35 | 36 | i := m.Instructions[index] 37 | 38 | if !bytes.Equal(m.Accounts[i.ProgramIndex], ProgramKey) { 39 | return nil, solana.ErrIncorrectProgram 40 | } 41 | 42 | return &DecompiledMemo{Data: i.Data}, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/solana/memo/program_test.go: -------------------------------------------------------------------------------- 1 | package memo 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/stickychin/code-server/pkg/solana" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestInstruction(t *testing.T) { 13 | i := Instruction("hello, world!") 14 | assert.Equal(t, ProgramKey, i.Program) 15 | assert.Empty(t, i.Accounts) 16 | assert.Equal(t, "hello, world!", string(i.Data)) 17 | } 18 | 19 | func TestDecompile(t *testing.T) { 20 | tx := solana.NewTransaction( 21 | make([]byte, 32), 22 | Instruction("hello, world"), 23 | ) 24 | 25 | i, err := DecompileMemo(tx.Message, 0) 26 | assert.NoError(t, err) 27 | assert.Equal(t, "hello, world", string(i.Data)) 28 | 29 | _, err = DecompileMemo(tx.Message, 1) 30 | assert.Error(t, err) 31 | assert.Contains(t, err.Error(), "instruction doesn't exist") 32 | 33 | tx.Message.Accounts[1], _, err = ed25519.GenerateKey(nil) 34 | require.NoError(t, err) 35 | _, err = DecompileMemo(tx.Message, 0) 36 | assert.Error(t, err) 37 | assert.Equal(t, solana.ErrIncorrectProgram, err) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/solana/shortvec/shortvec.go: -------------------------------------------------------------------------------- 1 | package shortvec 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | ) 8 | 9 | // EncodeLen encodes the specified len into the writer. 10 | // 11 | // If len > math.MaxUint16, an error is returned. 12 | func EncodeLen(w io.Writer, len int) (n int, err error) { 13 | if len > math.MaxUint16 { 14 | return 0, fmt.Errorf("len exceeds %d", math.MaxUint16) 15 | } 16 | 17 | written := 0 18 | valBuf := make([]byte, 1) 19 | 20 | for { 21 | valBuf[0] = byte(len & 0x7f) 22 | len >>= 7 23 | if len == 0 { 24 | n, err := w.Write(valBuf) 25 | written += n 26 | 27 | return written, err 28 | } 29 | 30 | valBuf[0] |= 0x80 31 | n, err := w.Write(valBuf) 32 | written += n 33 | if err != nil { 34 | return written, err 35 | } 36 | } 37 | } 38 | 39 | // DecodeLen decodes a shortvec encoded len from the reader. 40 | func DecodeLen(r io.Reader) (val int, err error) { 41 | var offset int 42 | valBuf := make([]byte, 1) 43 | 44 | for { 45 | if _, err := r.Read(valBuf); err != nil { 46 | return 0, err 47 | } 48 | 49 | val |= int(valBuf[0]&0x7f) << (offset * 7) 50 | offset++ 51 | 52 | if valBuf[0]&0x80 == 0 { 53 | break 54 | } 55 | } 56 | 57 | if offset > 3 { 58 | return 0, fmt.Errorf("invalid size: %d (max 3)", offset) 59 | } 60 | 61 | return val, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/solana/shortvec/shortvec_test.go: -------------------------------------------------------------------------------- 1 | package shortvec 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestShortVec_Valid(t *testing.T) { 13 | for i := 0; i < math.MaxUint16; i++ { 14 | buf := &bytes.Buffer{} 15 | _, err := EncodeLen(buf, i) 16 | require.NoError(t, err) 17 | 18 | actual, err := DecodeLen(buf) 19 | require.NoError(t, err) 20 | require.Equal(t, i, actual) 21 | } 22 | } 23 | 24 | func TestShortVec_CrossImpl(t *testing.T) { 25 | for _, tc := range []struct { 26 | val int 27 | encoded []byte 28 | }{ 29 | {0x0, []byte{0x0}}, 30 | {0x7f, []byte{0x7f}}, 31 | {0x80, []byte{0x80, 0x01}}, 32 | {0xff, []byte{0xff, 0x01}}, 33 | {0x100, []byte{0x80, 0x02}}, 34 | {0x7fff, []byte{0xff, 0xff, 0x01}}, 35 | {0xffff, []byte{0xff, 0xff, 0x03}}, 36 | } { 37 | buf := &bytes.Buffer{} 38 | n, err := EncodeLen(buf, tc.val) 39 | require.NoError(t, err) 40 | assert.Equal(t, len(tc.encoded), n) 41 | assert.Equal(t, tc.encoded, buf.Bytes()) 42 | } 43 | } 44 | 45 | func TestShortVec_Invalid(t *testing.T) { 46 | _, err := EncodeLen(&bytes.Buffer{}, math.MaxUint16+1) 47 | require.NotNil(t, err) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/solana/splitter/errors.go: -------------------------------------------------------------------------------- 1 | package splitter_token 2 | 3 | type SplitterTokenError uint32 4 | 5 | const ( 6 | // Invalid pool state for this instruction 7 | ErrInvalidPoolState SplitterTokenError = iota + 0x1770 8 | 9 | //Invalid commitment state for this instruction 10 | ErrInvalidCommitmentState 11 | 12 | //Invalid recent root value 13 | ErrInvalidRecentRoot 14 | 15 | //Invalid token account 16 | ErrInvalidVaultAccount 17 | 18 | //Insufficient vault funds 19 | ErrInsufficientVaultBalance 20 | 21 | //Invalid authority 22 | ErrInvalidAuthority 23 | 24 | //Invalid vault owner 25 | ErrInvalidVaultOwner 26 | 27 | //Merkle tree full 28 | ErrMerkleTreeFull 29 | 30 | //Invalid merkle tree depth 31 | ErrInvalidMerkleTreeDepth 32 | 33 | //Proof already verified 34 | ErrProofAlreadyVerified 35 | 36 | //Proof not verified 37 | ErrProofNotVerified 38 | 39 | //Invalid proof size 40 | ErrInvalidProofSize 41 | 42 | //Invalid proof 43 | ErrInvalidProof 44 | ) 45 | -------------------------------------------------------------------------------- /pkg/solana/splitter/legacy_test.go: -------------------------------------------------------------------------------- 1 | package splitter_token 2 | 3 | // todo: updated set of tests from production data 4 | -------------------------------------------------------------------------------- /pkg/solana/splitter/program.go: -------------------------------------------------------------------------------- 1 | package splitter_token 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrInvalidProgram = errors.New("invalid program id") 10 | ErrInvalidAccountData = errors.New("unexpected account data") 11 | ErrInvalidInstructionData = errors.New("unexpected instruction data") 12 | ) 13 | 14 | var ( 15 | PROGRAM_ADDRESS = mustBase58Decode("spLit2eb13Tz93if6aJM136nUWki5PVUsoEjcUjwpwW") 16 | PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) 17 | ) 18 | 19 | var ( 20 | SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) 21 | SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) 22 | SPL_ASSOCIATED_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")) 23 | 24 | SYSVAR_CLOCK_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarC1ock11111111111111111111111111111111")) 25 | SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) 26 | ) 27 | 28 | // AccountMeta represents the account information required 29 | // for building transactions. 30 | type AccountMeta struct { 31 | PublicKey ed25519.PublicKey 32 | IsWritable bool 33 | IsSigner bool 34 | } 35 | 36 | // Instruction represents a transaction instruction. 37 | type Instruction struct { 38 | Program ed25519.PublicKey 39 | Accounts []AccountMeta 40 | Data []byte 41 | } 42 | -------------------------------------------------------------------------------- /pkg/solana/splitter/types.go: -------------------------------------------------------------------------------- 1 | package splitter_token 2 | 3 | type DataVersion uint8 4 | 5 | const ( 6 | UnknownDataVersion DataVersion = iota 7 | DataVersion1 8 | ) 9 | 10 | func putDataVersion(dst []byte, v DataVersion, offset *int) { 11 | putUint8(dst, uint8(v), offset) 12 | } 13 | 14 | func getDataVersion(src []byte, dst *DataVersion, offset *int) { 15 | var v uint8 16 | getUint8(src, &v, offset) 17 | *dst = DataVersion(v) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/solana/splitter/types_hash.go: -------------------------------------------------------------------------------- 1 | package splitter_token 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // HashSize is the size, in bytes, of SHA256 hashes as used in this package. 7 | HashSize = 32 8 | ) 9 | 10 | type Hash []byte 11 | 12 | func (h Hash) ToString() string { 13 | return fmt.Sprintf("%x", h) 14 | } 15 | 16 | func putHash(dst []byte, src []byte, offset *int) { 17 | copy(dst[*offset:], src) 18 | *offset += HashSize 19 | } 20 | 21 | func getHash(src []byte, dst *Hash, offset *int) { 22 | *dst = make([]byte, HashSize) 23 | copy(*dst, src[*offset:]) 24 | *offset += HashSize 25 | } 26 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/address.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | var ( 10 | preSwapStatePrefix = []byte("pre_swap_state") 11 | ) 12 | 13 | type GetPreSwapStateAddressArgs struct { 14 | Source ed25519.PublicKey 15 | Destination ed25519.PublicKey 16 | Nonce ed25519.PublicKey 17 | } 18 | 19 | func GetPreSwapStateAddress(args *GetPreSwapStateAddressArgs) (ed25519.PublicKey, uint8, error) { 20 | return solana.FindProgramAddressAndBump( 21 | PROGRAM_ID, 22 | preSwapStatePrefix, 23 | args.Source, 24 | args.Destination, 25 | args.Nonce, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/address_test.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mr-tron/base58" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // Tests based on account created here https://solscan.io/tx/4kXywqQRqJXRnm8mzAVRYkL1YVwyAEi8rVVNzRYG3AAu73xqjxNdHKX6LgoFhon66ZWHJ8HU3QkAmiot7AGzXPzd 12 | 13 | func TestPreSwapStateAddress(t *testing.T) { 14 | address, _, err := GetPreSwapStateAddress(&GetPreSwapStateAddressArgs{ 15 | Source: mustBase58Decode("5nNBW1KhzHVbR4NMPLYPRYj3UN5vgiw5GrtpdK6eGoce"), 16 | Destination: mustBase58Decode("9Rgx4kjnYZBbeXXgbbYLT2FfgzrNHFUShDtp8dpHHjd2"), 17 | Nonce: mustBase58Decode("3SVPEF5HDcKLhVfKeAnbH5Azpyeuk2yyVjEjZbz4VhrL"), 18 | }) 19 | require.NoError(t, err) 20 | assert.Equal(t, "Hh338LHJhkzPbDisGt5Lge8qkgc3RExvH7BdmKgnRQw9", base58.Encode(address)) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/errors.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | type SwapValidatorError uint32 4 | 5 | const ( 6 | // Invalid input token amount sent 7 | InvalidInputTokenAmountSent SwapValidatorError = iota + 0x1770 8 | 9 | // Invalid output token amount received 10 | InvalidOutputTokenAmountReceived 11 | 12 | // Unexpected writable user account 13 | UnexpectedWritableUserAccount 14 | 15 | // Unexpected update to user token account 16 | UnexpectedUserTokenAccountUpdate 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/instructions_post_swap.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import ( 4 | "crypto/ed25519" 5 | ) 6 | 7 | var postSwapInstructionDiscriminator = []byte{ 8 | 0x9f, 0xd5, 0xb7, 0x39, 0xb3, 0x8a, 0x75, 0xa1, 9 | } 10 | 11 | const ( 12 | PostSwapInstructionArgsSize = (1 + // StateBump 13 | 8 + // MaxToSend 14 | 8) // MinToReceive 15 | ) 16 | 17 | type PostSwapInstructionArgs struct { 18 | StateBump uint8 19 | MaxToSend uint64 20 | MinToReceive uint64 21 | } 22 | 23 | type PostSwapInstructionAccounts struct { 24 | PreSwapState ed25519.PublicKey 25 | Source ed25519.PublicKey 26 | Destination ed25519.PublicKey 27 | Payer ed25519.PublicKey 28 | } 29 | 30 | func NewPostSwapInstruction( 31 | accounts *PostSwapInstructionAccounts, 32 | args *PostSwapInstructionArgs, 33 | ) Instruction { 34 | var offset int 35 | 36 | // Serialize instruction arguments 37 | data := make([]byte, 38 | len(postSwapInstructionDiscriminator)+ 39 | PostSwapInstructionArgsSize) 40 | 41 | putDiscriminator(data, postSwapInstructionDiscriminator, &offset) 42 | putUint8(data, args.StateBump, &offset) 43 | putUint64(data, args.MaxToSend, &offset) 44 | putUint64(data, args.MinToReceive, &offset) 45 | 46 | return Instruction{ 47 | Program: PROGRAM_ADDRESS, 48 | 49 | // Instruction args 50 | Data: data, 51 | 52 | // Instruction accounts 53 | Accounts: []AccountMeta{ 54 | { 55 | PublicKey: accounts.PreSwapState, 56 | IsWritable: true, 57 | IsSigner: false, 58 | }, 59 | { 60 | PublicKey: accounts.Source, 61 | IsWritable: false, 62 | IsSigner: false, 63 | }, 64 | { 65 | PublicKey: accounts.Destination, 66 | IsWritable: false, 67 | IsSigner: false, 68 | }, 69 | { 70 | PublicKey: accounts.Payer, 71 | IsWritable: true, 72 | IsSigner: true, 73 | }, 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/legacy.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import "github.com/stickychin/code-server/pkg/solana" 4 | 5 | func (i Instruction) ToLegacyInstruction() solana.Instruction { 6 | legacyAccountMeta := make([]solana.AccountMeta, len(i.Accounts)) 7 | for i, accountMeta := range i.Accounts { 8 | legacyAccountMeta[i] = solana.AccountMeta{ 9 | PublicKey: accountMeta.PublicKey, 10 | IsSigner: accountMeta.IsSigner, 11 | IsWritable: accountMeta.IsWritable, 12 | } 13 | } 14 | 15 | return solana.Instruction{ 16 | Program: PROGRAM_ID, 17 | Accounts: legacyAccountMeta, 18 | Data: i.Data, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/program.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrInvalidProgram = errors.New("invalid program id") 10 | ErrInvalidAccountData = errors.New("unexpected account data") 11 | ErrInvalidInstructionData = errors.New("unexpected instruction data") 12 | ) 13 | 14 | var ( 15 | PROGRAM_ADDRESS = mustBase58Decode("sWvA66HNNvgamibZe88v3NN5nQwE8tp3KitfViFjukA") 16 | PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) 17 | ) 18 | 19 | var ( 20 | SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) 21 | 22 | SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) 23 | ) 24 | 25 | // AccountMeta represents the account information required 26 | // for building transactions. 27 | type AccountMeta struct { 28 | PublicKey ed25519.PublicKey 29 | IsWritable bool 30 | IsSigner bool 31 | } 32 | 33 | // Instruction represents a transaction instruction. 34 | type Instruction struct { 35 | Program ed25519.PublicKey 36 | Accounts []AccountMeta 37 | Data []byte 38 | } 39 | -------------------------------------------------------------------------------- /pkg/solana/swapvalidator/utils.go: -------------------------------------------------------------------------------- 1 | package swap_validator 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/binary" 6 | 7 | "github.com/mr-tron/base58" 8 | ) 9 | 10 | // This file should be replaced with something more robust. Modified version of what the original agora and code pkgs do. 11 | 12 | func putDiscriminator(dst []byte, src []byte, offset *int) { 13 | copy(dst[*offset:], src) 14 | *offset += 8 15 | } 16 | func getDiscriminator(src []byte, dst *[]byte, offset *int) { 17 | *dst = make([]byte, 8) 18 | copy(*dst, src[*offset:]) 19 | *offset += 8 20 | } 21 | 22 | func putKey(dst []byte, src []byte, offset *int) { 23 | copy(dst[*offset:], src) 24 | *offset += ed25519.PublicKeySize 25 | } 26 | func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { 27 | *dst = make([]byte, ed25519.PublicKeySize) 28 | copy(*dst, src[*offset:]) 29 | *offset += ed25519.PublicKeySize 30 | } 31 | 32 | func putUint8(dst []byte, v uint8, offset *int) { 33 | dst[*offset] = v 34 | *offset += 1 35 | } 36 | func getUint8(src []byte, dst *uint8, offset *int) { 37 | *dst = src[*offset] 38 | *offset += 1 39 | } 40 | 41 | func putUint64(dst []byte, v uint64, offset *int) { 42 | binary.LittleEndian.PutUint64(dst[*offset:], v) 43 | *offset += 8 44 | } 45 | func getUint64(src []byte, dst *uint64, offset *int) { 46 | *dst = binary.LittleEndian.Uint64(src[*offset:]) 47 | *offset += 8 48 | } 49 | 50 | func mustBase58Decode(value string) []byte { 51 | decoded, err := base58.Decode(value) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return decoded 56 | } 57 | -------------------------------------------------------------------------------- /pkg/solana/system/sysvar.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/mr-tron/base58/base58" 7 | ) 8 | 9 | // https://explorer.solana.com/address/11111111111111111111111111111111 10 | var SystemAccount ed25519.PublicKey 11 | 12 | // RentSysVar points to the system variable "Rent" 13 | // 14 | // Source: https://github.com/solana-labs/solana/blob/f02a78d8fff2dd7297dc6ce6eb5a68a3002f5359/sdk/src/sysvar/rent.rs#L11 15 | var RentSysVar ed25519.PublicKey 16 | 17 | // RecentBlockhashesSysVar points to the system variable "Recent Blockhashes" 18 | // 19 | // Source: https://github.com/solana-labs/solana/blob/f02a78d8fff2dd7297dc6ce6eb5a68a3002f5359/sdk/src/sysvar/recent_blockhashes.rs#L12-L15 20 | var RecentBlockhashesSysVar ed25519.PublicKey 21 | 22 | func init() { 23 | var err error 24 | 25 | RentSysVar, err = base58.Decode("SysvarRent111111111111111111111111111111111") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | RecentBlockhashesSysVar, err = base58.Decode("SysvarRecentB1ockHashes11111111111111111111") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | SystemAccount, err = base58.Decode("11111111111111111111111111111111") 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/address.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/binary" 6 | 7 | "github.com/stickychin/code-server/pkg/solana" 8 | ) 9 | 10 | const ( 11 | expectedInitOffset = 0 12 | pdaVersion1 = 1 13 | ) 14 | 15 | var ( 16 | statePrefix = []byte("timelock_state") 17 | vaultPrefix = []byte("timelock_vault") 18 | 19 | pdaPadding = make([]byte, ed25519.PublicKeySize) 20 | ) 21 | 22 | type GetStateAddressArgs struct { 23 | Mint ed25519.PublicKey 24 | TimeAuthority ed25519.PublicKey 25 | Nonce ed25519.PublicKey 26 | VaultOwner ed25519.PublicKey 27 | UnlockDuration uint64 28 | } 29 | 30 | type GetVaultAddressArgs struct { 31 | State ed25519.PublicKey 32 | } 33 | 34 | func GetStateAddress(args *GetStateAddressArgs) (ed25519.PublicKey, uint8, error) { 35 | unlockDurationBytes := make([]byte, 8) 36 | binary.LittleEndian.PutUint64(unlockDurationBytes, args.UnlockDuration) 37 | 38 | return solana.FindProgramAddressAndBump( 39 | PROGRAM_ID, 40 | statePrefix, 41 | []byte{pdaVersion1}, 42 | args.Mint, 43 | args.TimeAuthority, 44 | args.Nonce, 45 | args.VaultOwner, 46 | unlockDurationBytes, 47 | pdaPadding, 48 | ) 49 | } 50 | 51 | func GetVaultAddress(args *GetVaultAddressArgs) (ed25519.PublicKey, uint8, error) { 52 | return solana.FindProgramAddressAndBump( 53 | PROGRAM_ID, 54 | vaultPrefix, 55 | args.State, 56 | []byte{expectedInitOffset}, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/address_test.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mr-tron/base58" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // todo: Base these on real-world examples from mainnet 12 | 13 | func TestGetStateAddress(t *testing.T) { 14 | address, _, err := GetStateAddress(&GetStateAddressArgs{ 15 | Mint: mustBase58Decode("2U7X7s9CjKzkB9AqtJ4Am6hqiFhzPzisdEo3uMwuoPrq"), 16 | TimeAuthority: mustBase58Decode("DpnXnwD9DcBnMxpbNM36cko3m41qgrsopqTNsUesMuyd"), 17 | Nonce: mustBase58Decode("11111111111111111111111111111111"), 18 | VaultOwner: mustBase58Decode("9N3Ypqi8FwNBK21bFbpysVqTupiH13JRQg9wQ4xnjYxK"), 19 | UnlockDuration: 1814400, 20 | }) 21 | require.NoError(t, err) 22 | assert.Equal(t, "7BJrsePGaFkkYWuzy63VGy7N5ZA9Gou1BZHETCPRydiY", base58.Encode(address)) 23 | } 24 | 25 | func TestGetVaultAddress(t *testing.T) { 26 | address, _, err := GetVaultAddress(&GetVaultAddressArgs{ 27 | State: mustBase58Decode("7BJrsePGaFkkYWuzy63VGy7N5ZA9Gou1BZHETCPRydiY"), 28 | }) 29 | require.NoError(t, err) 30 | assert.Equal(t, "9e6MBQRfQLQmaheuvixLvsTtZfPMw4L4BX2x9LTj2kf3", base58.Encode(address)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/constants.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | import "time" 4 | 5 | const ( 6 | // Need to be very careful changing this value, as it's used in the state 7 | // address PDA. 8 | DefaultUnlockDuration = uint64(3 * 7 * 24 * time.Hour / time.Second) // 3 weeks in seconds 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/errors.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | type TimelockTokenError uint32 4 | 5 | const ( 6 | // Invalid timelock state for this instruction 7 | ErrInvalidTimelockState TimelockTokenError = iota + 0x1770 8 | 9 | // Invalid timelock duration provided 10 | ErrInvalidTimelockDuration 11 | 12 | // Invalid vault account 13 | ErrInvalidVaultAccount 14 | 15 | // The timelock period has not yet been reached 16 | ErrInsufficientTimeElapsed 17 | 18 | // Insufficient vault funds 19 | ErrInsufficientVaultBalance 20 | 21 | // Invalid time authority 22 | ErrInvalidTimeAuthority 23 | 24 | // Invalid vault owner 25 | ErrInvalidVaultOwner 26 | 27 | // Invalid close authority 28 | ErrInvalidCloseAuthority 29 | 30 | // Invalid token balance. Token balance must be zero. 31 | ErrNonZeroTokenBalance 32 | ) 33 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/legacy_test.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | // todo: updated set of tests from production data 4 | -------------------------------------------------------------------------------- /pkg/solana/timelock/legacy_2022/types.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | type TimelockState uint8 4 | 5 | const ( 6 | Unknown TimelockState = iota 7 | Unlocked 8 | WaitingForTimeout 9 | Locked 10 | Closed 11 | ) 12 | 13 | func putTimelockAccountState(dst []byte, v TimelockState, offset *int) { 14 | putUint8(dst, uint8(v), offset) 15 | } 16 | 17 | func getTimelockAccountState(src []byte, dst *TimelockState, offset *int) { 18 | var v uint8 19 | getUint8(src, &v, offset) 20 | *dst = TimelockState(v) 21 | } 22 | 23 | func (s TimelockState) String() string { 24 | switch s { 25 | case Unknown: 26 | return "unknown" 27 | case Locked: 28 | return "locked" 29 | case WaitingForTimeout: 30 | return "waiting_for_timeout" 31 | case Unlocked: 32 | return "unlocked" 33 | case Closed: 34 | return "closed" 35 | default: 36 | return "unknown" 37 | } 38 | } 39 | 40 | type TimelockDataVersion uint8 41 | 42 | const ( 43 | UnknownDataVersion TimelockDataVersion = iota 44 | DataVersion1 45 | ) 46 | 47 | func putTimelockAccountDataVersion(dst []byte, v TimelockDataVersion, offset *int) { 48 | putUint8(dst, uint8(v), offset) 49 | } 50 | 51 | func getTimelockAccountDataVersion(src []byte, dst *TimelockDataVersion, offset *int) { 52 | var v uint8 53 | getUint8(src, &v, offset) 54 | *dst = TimelockDataVersion(v) 55 | } 56 | 57 | type TimelockPdaVersion uint8 58 | 59 | const ( 60 | UnknownPdaVersion TimelockPdaVersion = iota 61 | PdaVersion1 62 | ) 63 | 64 | func putTimelockAccountPdaVersion(dst []byte, v TimelockPdaVersion, offset *int) { 65 | putUint8(dst, uint8(v), offset) 66 | } 67 | 68 | func getTimelockAccountPdaVersion(src []byte, dst *TimelockPdaVersion, offset *int) { 69 | var v uint8 70 | getUint8(src, &v, offset) 71 | *dst = TimelockPdaVersion(v) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/address.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/stickychin/code-server/pkg/solana" 7 | ) 8 | 9 | var ( 10 | statePrefix = []byte("timelock_state") 11 | vaultPrefix = []byte("timelock_vault") 12 | ) 13 | 14 | type GetStateAddressArgs struct { 15 | Mint ed25519.PublicKey 16 | TimeAuthority ed25519.PublicKey 17 | VaultOwner ed25519.PublicKey 18 | NumDaysLocked uint8 19 | } 20 | 21 | type GetVaultAddressArgs struct { 22 | State ed25519.PublicKey 23 | DataVersion TimelockDataVersion 24 | } 25 | 26 | func GetStateAddress(args *GetStateAddressArgs) (ed25519.PublicKey, uint8, error) { 27 | return solana.FindProgramAddressAndBump( 28 | PROGRAM_ID, 29 | statePrefix, 30 | args.Mint, 31 | args.TimeAuthority, 32 | args.VaultOwner, 33 | []byte{args.NumDaysLocked}, 34 | ) 35 | } 36 | 37 | func GetVaultAddress(args *GetVaultAddressArgs) (ed25519.PublicKey, uint8, error) { 38 | return solana.FindProgramAddressAndBump( 39 | PROGRAM_ID, 40 | vaultPrefix, 41 | args.State, 42 | []byte{byte(args.DataVersion)}, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/address_test.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mr-tron/base58/base58" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // Tests based on account created here https://explorer.solana.com/tx/2TTGdkrxuzQXe227CukjRMbppELELWyip9HQB2swuttgtfQovVp7bAXxSpBQjZpMpEWh17UFrwU1EYwiqYX5wK3 12 | 13 | func TestGetStateAddress(t *testing.T) { 14 | address, _, err := GetStateAddress(&GetStateAddressArgs{ 15 | Mint: mustBase58Decode("kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6"), 16 | TimeAuthority: mustBase58Decode("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR"), 17 | VaultOwner: mustBase58Decode("BuAprBZugjXG6QRbRQN8QKF8EzbW5SigkDuyR9KtqN5z"), 18 | NumDaysLocked: DefaultNumDaysLocked, 19 | }) 20 | require.NoError(t, err) 21 | assert.Equal(t, "7Ema8Z4gAUWegampp2AuX4cvaTRy3VMwJUq8LMJshQTV", base58.Encode(address)) 22 | } 23 | 24 | func TestGetVaultAddress(t *testing.T) { 25 | address, _, err := GetVaultAddress(&GetVaultAddressArgs{ 26 | State: mustBase58Decode("7Ema8Z4gAUWegampp2AuX4cvaTRy3VMwJUq8LMJshQTV"), 27 | DataVersion: DataVersion1, 28 | }) 29 | require.NoError(t, err) 30 | assert.Equal(t, "3538bYdWoRXUgBbyAyvG3Zemmawh75nmCQEvWc9DfKFR", base58.Encode(address)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/constants.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | const ( 4 | // Need to be very careful changing this value, as it's used in the state 5 | // address PDA. 6 | DefaultNumDaysLocked = uint8(21) 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/errors.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | type TimeLockTokenError uint32 4 | 5 | const ( 6 | // Invalid time-lock state for this instruction 7 | ErrInvalidTimeLockState TimeLockTokenError = iota + 0x1770 8 | 9 | // Invalid time-lock duration provided 10 | ErrInvalidTimeLockDuration 11 | 12 | // Invalid vault account 13 | ErrInvalidVaultAccount 14 | 15 | // The time-lock period has not yet been reached 16 | ErrInsufficientTimeElapsed 17 | 18 | // Insufficient vault funds 19 | ErrInsufficientVaultBalance 20 | 21 | // Invalid time authority 22 | ErrInvalidTimeAuthority 23 | 24 | // Invalid vault owner 25 | ErrInvalidVaultOwner 26 | 27 | // Invalid close authority 28 | ErrInvalidCloseAuthority 29 | 30 | // Invalid token balance. Token balance must be zero. 31 | ErrNonZeroTokenBalance 32 | 33 | // Invalid dust burn 34 | ErrInvalidDustBurn 35 | 36 | // Invalid token mint 37 | ErrInvalidTokenMint 38 | ) 39 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/legacy_test.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | // todo: updated set of tests from production data 4 | -------------------------------------------------------------------------------- /pkg/solana/timelock/v1/types.go: -------------------------------------------------------------------------------- 1 | package timelock_token 2 | 3 | type TimelockState uint8 4 | 5 | const ( 6 | StateUnknown TimelockState = iota 7 | StateUnlocked 8 | StateWaitingForTimeout 9 | StateLocked 10 | StateClosed 11 | ) 12 | 13 | func putTimelockAccountState(dst []byte, v TimelockState, offset *int) { 14 | putUint8(dst, uint8(v), offset) 15 | } 16 | 17 | func getTimelockAccountState(src []byte, dst *TimelockState, offset *int) { 18 | var v uint8 19 | getUint8(src, &v, offset) 20 | *dst = TimelockState(v) 21 | } 22 | 23 | type TimelockDataVersion uint8 24 | 25 | const ( 26 | UnknownDataVersion TimelockDataVersion = iota 27 | DataVersionLegacy 28 | DataVersionClosed 29 | DataVersion1 30 | ) 31 | 32 | func putTimelockAccountDataVersion(dst []byte, v TimelockDataVersion, offset *int) { 33 | putUint8(dst, uint8(v), offset) 34 | } 35 | 36 | func getTimelockAccountDataVersion(src []byte, dst *TimelockDataVersion, offset *int) { 37 | var v uint8 38 | getUint8(src, &v, offset) 39 | *dst = TimelockDataVersion(v) 40 | } 41 | 42 | func (s TimelockState) String() string { 43 | switch s { 44 | case StateUnknown: 45 | return "unknown" 46 | case StateUnlocked: 47 | return "unlocked" 48 | case StateWaitingForTimeout: 49 | return "waiting_for_timeout" 50 | case StateLocked: 51 | return "locked" 52 | case StateClosed: 53 | return "closed" 54 | } 55 | 56 | return "unknown" 57 | } 58 | -------------------------------------------------------------------------------- /pkg/sync/ring.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/emirpasic/gods/maps/treemap" 7 | "github.com/emirpasic/gods/utils" 8 | "github.com/spaolacci/murmur3" 9 | ) 10 | 11 | // ring is a consistent hash ring 12 | // 13 | // todo: extract into it's own package if there becomes more uses with more 14 | // options like hash func 15 | type ring struct { 16 | hashRing *treemap.Map 17 | 18 | // minEntryValue is used to cache the value of min entry in hashRing. 19 | // Using treemap.Map.Min() is O(log n). 20 | minEntryValue interface{} 21 | } 22 | 23 | // newRing returns a new consistent hash ring with the set of entries that have 24 | // replicationFactor entries in the ring 25 | func newRing(entries map[string]interface{}, replicationFactor uint) *ring { 26 | hashRing := treemap.NewWith(utils.Int64Comparator) 27 | for k, v := range entries { 28 | keyHash, _ := murmur3.Sum128([]byte(k)) 29 | keyHashBytes := make([]byte, 8) 30 | binary.LittleEndian.PutUint64(keyHashBytes, keyHash) 31 | for i := 0; i < int(replicationFactor); i++ { 32 | hasher := murmur3.New128() 33 | hasher.Write(keyHashBytes) 34 | indexBytes := make([]byte, 4) 35 | binary.LittleEndian.PutUint32(indexBytes, uint32(i)) 36 | hasher.Write(indexBytes) 37 | hash, _ := hasher.Sum128() 38 | hashRing.Put(int64(hash), v) 39 | } 40 | } 41 | 42 | _, minEntryValue := hashRing.Min() 43 | 44 | return &ring{ 45 | hashRing: hashRing, 46 | minEntryValue: minEntryValue, 47 | } 48 | } 49 | 50 | // shard consistently hashes the key and returns the sharded entry value 51 | func (r *ring) shard(key []byte) interface{} { 52 | hasher := murmur3.New128() 53 | hasher.Write(key) 54 | raw, _ := hasher.Sum128() 55 | hash := int64(raw) 56 | _, shard := r.hashRing.Ceiling(hash) 57 | if shard != nil { 58 | return shard 59 | } 60 | return r.minEntryValue 61 | } 62 | -------------------------------------------------------------------------------- /pkg/sync/ring_test.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRing_Consistency(t *testing.T) { 12 | entries := make(map[string]interface{}) 13 | for i := 0; i < 64; i++ { 14 | entries[fmt.Sprintf("entry%d", i)] = i 15 | } 16 | 17 | r := newRing(entries, 200) 18 | 19 | for i := 0; i < 256; i++ { 20 | key := []byte(fmt.Sprintf("key%d", i)) 21 | val := r.shard(key) 22 | 23 | for j := 0; j < 256; j++ { 24 | assert.EqualValues(t, val, r.shard(key)) 25 | } 26 | } 27 | } 28 | 29 | func TestRing_Distribution(t *testing.T) { 30 | entryCount := 5 31 | iterations := 500000 32 | marginOfError := 0.1 33 | expectedFrequency := iterations / entryCount 34 | 35 | entries := make(map[string]interface{}) 36 | for i := 0; i < entryCount; i++ { 37 | entries[fmt.Sprintf("entry%d", i)] = i 38 | } 39 | 40 | r := newRing(entries, 200) 41 | 42 | hits := make(map[int]int) 43 | for i := 0; i < iterations; i++ { 44 | key := []byte(fmt.Sprintf("key%d", i)) 45 | val := r.shard(key).(int) 46 | hits[val]++ 47 | } 48 | 49 | assert.EqualValues(t, entryCount, len(hits)) 50 | for _, hitCount := range hits { 51 | assert.True(t, math.Abs(float64(hitCount-expectedFrequency)) <= marginOfError*float64(expectedFrequency)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/sync/striped_channel_test.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestStripedChannel_HappyPath(t *testing.T) { 12 | c := NewStripedChannel(32, 256) 13 | 14 | channels := c.GetChannels() 15 | require.Equal(t, 32, len(channels)) 16 | 17 | results := make([]map[int]int, len(channels)) 18 | 19 | var wg sync.WaitGroup 20 | worker := func(id int, c <-chan interface{}) { 21 | defer func() { 22 | wg.Done() 23 | }() 24 | 25 | for { 26 | select { 27 | case val, ok := <-c: 28 | if !ok { 29 | return 30 | } 31 | 32 | results[id][val.(int)]++ 33 | } 34 | } 35 | } 36 | 37 | for i, channel := range channels { 38 | wg.Add(1) 39 | results[i] = make(map[int]int) 40 | go worker(i, channel) 41 | } 42 | 43 | for i := 0; i < 256; i++ { 44 | for j := 0; j < 10; j++ { 45 | assert.True(t, c.Send([]byte{byte(i)}, i)) 46 | } 47 | } 48 | 49 | c.Close() 50 | 51 | wg.Wait() 52 | 53 | aggregated := make(map[int]int) 54 | for _, result := range results { 55 | for k, v := range result { 56 | aggregated[k] = v 57 | } 58 | } 59 | 60 | assert.Equal(t, 256, len(aggregated)) 61 | for _, v := range aggregated { 62 | assert.Equal(t, 10, v) 63 | } 64 | } 65 | 66 | func TestStripedChannel_FullQueue(t *testing.T) { 67 | c := NewStripedChannel(32, 256) 68 | 69 | for i := 0; i < 256; i++ { 70 | assert.True(t, c.Send([]byte{byte(1)}, 1)) 71 | } 72 | 73 | assert.False(t, c.Send([]byte{byte(1)}, 1)) 74 | assert.True(t, c.Send([]byte{byte(2)}, 2)) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/sync/striped_lock.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "fmt" 5 | base "sync" 6 | ) 7 | 8 | const ( 9 | hashEntriesPerLock = 200 10 | ) 11 | 12 | // StripedLock is a partitioned locking mechanism that consistently maps a key 13 | // space to a set of locks. This provides concurrent data access while also 14 | // limiting the total memory footprint. 15 | type StripedLock struct { 16 | locks []base.RWMutex 17 | hashRing *ring 18 | } 19 | 20 | // NewStripedLock returns a new StripedLock with a static number of stripes. 21 | func NewStripedLock(stripes uint) *StripedLock { 22 | return newStripedLockGroup(stripes, 1)[0] 23 | } 24 | 25 | // newStripedLockGroup returns a new group of StripedLocks which share a common 26 | // hash ring. This should be used in cases where multiple striped locks are 27 | // needed which share the same sharding domain in order to avoid allocating 28 | // entire hash rings for each. 29 | // 30 | // todo: Add tests, then expose publicly 31 | func newStripedLockGroup(stripes, groupSize uint) []*StripedLock { 32 | stripedLocks := make([]*StripedLock, 0, groupSize) 33 | 34 | ringEntries := make(map[string]interface{}) 35 | for i := 0; i < int(stripes); i++ { 36 | ringEntries[fmt.Sprintf("lock%d", i)] = i 37 | } 38 | 39 | ring := newRing(ringEntries, hashEntriesPerLock) 40 | 41 | for i := 0; i < int(groupSize); i++ { 42 | stripedLocks = append(stripedLocks, &StripedLock{ 43 | locks: make([]base.RWMutex, stripes), 44 | hashRing: ring, 45 | }) 46 | } 47 | 48 | return stripedLocks 49 | } 50 | 51 | // Get gets the lock for a key 52 | func (l *StripedLock) Get(key []byte) *base.RWMutex { 53 | sharded := l.hashRing.shard(key).(int) 54 | return &l.locks[sharded] 55 | } 56 | -------------------------------------------------------------------------------- /pkg/sync/striped_lock_test.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | base "sync" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestStripedLock_HappyPath(t *testing.T) { 13 | workerCount := 256 14 | operationCount := 100000 15 | 16 | l := NewStripedLock(4) 17 | 18 | var workerWg base.WaitGroup 19 | startChan := make(chan struct{}, 0) 20 | data := make([]int, workerCount) 21 | 22 | for i := 0; i < workerCount; i++ { 23 | workerWg.Add(1) 24 | 25 | go func(workerID int) { 26 | defer workerWg.Done() 27 | 28 | var opWg sync.WaitGroup 29 | key := []byte(fmt.Sprintf("worker%d", workerID)) 30 | for j := 0; j < operationCount; j++ { 31 | opWg.Add(1) 32 | 33 | go func() { 34 | defer opWg.Done() 35 | 36 | select { 37 | case <-startChan: 38 | } 39 | 40 | mu := l.Get([]byte(key)) 41 | mu.Lock() 42 | data[workerID]++ 43 | mu.Unlock() 44 | }() 45 | } 46 | opWg.Wait() 47 | }(i) 48 | } 49 | 50 | close(startChan) 51 | workerWg.Wait() 52 | 53 | for _, val := range data { 54 | assert.EqualValues(t, operationCount, val) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/testutil/error.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | // AssertStatusErrorWithCode verifies that the provided error is a gRPC status 13 | // error of the provided status code. 14 | func AssertStatusErrorWithCode(t *testing.T, err error, code codes.Code) { 15 | require.Error(t, err) 16 | status, ok := status.FromError(err) 17 | require.True(t, ok) 18 | assert.Equal(t, code, status.Code()) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/testutil/logging.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func init() { 11 | var isVerbose bool 12 | for _, arg := range os.Args { 13 | if arg == "-test.v=true" { 14 | isVerbose = true 15 | } 16 | } 17 | 18 | logrus.SetLevel(logrus.TraceLevel) 19 | 20 | if !isVerbose { 21 | logrus.StandardLogger().Out = ioutil.Discard 22 | } 23 | } 24 | 25 | func DisableLogging() (reset func()) { 26 | originalLogOutput := logrus.StandardLogger().Out 27 | logrus.StandardLogger().Out = ioutil.Discard 28 | return func() { 29 | logrus.StandardLogger().Out = originalLogOutput 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/testutil/solana.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/stickychin/code-server/pkg/code/common" 10 | ) 11 | 12 | func GenerateSolanaKeypair(t *testing.T) ed25519.PrivateKey { 13 | _, p, err := ed25519.GenerateKey(nil) 14 | require.NoError(t, err) 15 | return p 16 | } 17 | 18 | func GenerateSolanaKeys(t *testing.T, n int) []ed25519.PublicKey { 19 | keys := make([]ed25519.PublicKey, n) 20 | for i := 0; i < n; i++ { 21 | p, _, err := ed25519.GenerateKey(nil) 22 | require.NoError(t, err) 23 | keys[i] = p 24 | } 25 | return keys 26 | } 27 | 28 | func NewRandomAccount(t *testing.T) *common.Account { 29 | account, err := common.NewRandomAccount() 30 | require.NoError(t, err) 31 | 32 | return account 33 | } 34 | -------------------------------------------------------------------------------- /pkg/testutil/subsidizer.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/stickychin/code-server/pkg/code/common" 10 | code_data "github.com/stickychin/code-server/pkg/code/data" 11 | ) 12 | 13 | func SetupRandomSubsidizer(t *testing.T, data code_data.Provider) *common.Account { 14 | account := NewRandomAccount(t) 15 | require.NoError(t, common.InjectTestSubsidizer(context.Background(), data, account)) 16 | return account 17 | } 18 | -------------------------------------------------------------------------------- /pkg/testutil/wait.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // WaitFor waits for a condition to be met before the specified timeout 10 | func WaitFor(timeout, interval time.Duration, condition func() bool) error { 11 | if timeout < interval { 12 | return errors.New("timeout must be greater than interval") 13 | } 14 | start := time.Now() 15 | for { 16 | if condition() { 17 | return nil 18 | } 19 | if time.Since(start) >= timeout { 20 | return errors.Errorf("condition not met withind %v", timeout) 21 | } 22 | 23 | time.Sleep(interval) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/testutil/wait_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWaitFor(t *testing.T) { 11 | require.NoError(t, WaitFor(50*time.Millisecond, 25*time.Millisecond, func() bool { 12 | return true 13 | })) 14 | 15 | require.Error(t, WaitFor(50*time.Millisecond, 25*time.Millisecond, func() bool { 16 | return false 17 | })) 18 | 19 | require.Error(t, WaitFor(50*time.Millisecond, 100*time.Millisecond, func() bool { 20 | return true 21 | })) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/usdc/usdc.go: -------------------------------------------------------------------------------- 1 | package usdc 2 | 3 | import ( 4 | "crypto/ed25519" 5 | ) 6 | 7 | const ( 8 | Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" 9 | QuarksPerUsdc = 1000000 10 | Decimals = 6 11 | ) 12 | 13 | var ( 14 | TokenMint = ed25519.PublicKey{198, 250, 122, 243, 190, 219, 173, 58, 61, 101, 243, 106, 171, 201, 116, 49, 177, 187, 228, 194, 210, 246, 224, 228, 124, 166, 2, 3, 69, 47, 93, 97} 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/usdg/usdg.go: -------------------------------------------------------------------------------- 1 | package usdg 2 | 3 | const ( 4 | Mint = "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH" 5 | QuarksPerUsdg = 1000000 6 | Decimals = 6 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/usdt/usdt.go: -------------------------------------------------------------------------------- 1 | package usdt 2 | 3 | const ( 4 | Mint = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" 5 | QuarksPerUsdt = 1000000 6 | Decimals = 6 7 | ) 8 | --------------------------------------------------------------------------------