From c90ca1ebd7eacc7eb57af1cc2cf90fcc5dd385db Mon Sep 17 00:00:00 2001 From: Duy Do Date: Fri, 3 Nov 2023 19:41:24 +0700 Subject: [PATCH] Flow Deployment --- .cargo/config.toml | 3 + .github/assert_cargo_lock_unchanged.bash | 4 + .github/workflows/deps.yml | 26 + .github/workflows/docker-main.yaml | 28 + .github/workflows/docker.yaml | 27 + .github/workflows/rust.yml | 52 + .gitignore | 8 + @space-operator/client/LICENSE | 668 + @space-operator/client/deno.json | 5 + @space-operator/client/deno.lock | 403 + .../client/notebook/Untitled.ipynb | 157 + @space-operator/client/src/client.ts | 376 + @space-operator/client/src/deps.ts | 7 + @space-operator/client/src/mod.ts | 58 + @space-operator/client/src/supabase.ts | 1286 ++ @space-operator/client/src/types/common.ts | 9 + @space-operator/client/src/types/rest.ts | 125 + @space-operator/client/src/types/ws.ts | 246 + @space-operator/client/src/ws.ts | 235 + @space-operator/client/tests/auth.ts | 52 + @space-operator/client/tests/deploy.ts | 98 + @space-operator/client/tests/flow.ts | 51 + @space-operator/client/tests/upsert.ts | 47 + @space-operator/deno-command-rpc/README.md | 1 + @space-operator/deno-command-rpc/deno.json | 6 + @space-operator/deno-command-rpc/deno.lock | 414 + @space-operator/deno-command-rpc/src/deps.ts | 15 + @space-operator/deno-command-rpc/src/mod.ts | 122 + @space-operator/deno-command-rpc/src/utils.ts | 157 + @space-operator/deno.json | 0 @space-operator/deno.lock | 346 + @space-operator/flow-lib/deno.json | 12 + @space-operator/flow-lib/deno.lock | 400 + @space-operator/flow-lib/src/command.ts | 234 + @space-operator/flow-lib/src/common.ts | 7 + @space-operator/flow-lib/src/context.ts | 247 + @space-operator/flow-lib/src/deps.ts | 5 + @space-operator/flow-lib/src/mod.ts | 14 + @space-operator/flow-lib/src/value.ts | 479 + @space-operator/flow-lib/tests/value.ts | 54 + Cargo.lock | 10942 ++++++++++++++++ Cargo.toml | 61 + LICENSE | 668 + README.md | 14 + admin.toml | 37 + .../auction_house_sell.rs | 264 + .../create_auction_house.rs | 268 + .../nft/need_new_command_interface/utilize.rs | 168 + .../crates/cmds-solana/src/clockwork/mod.rs | 2 + .../clockwork/payments/disburse_payment_ix.rs | 166 + .../cmds-solana/src/clockwork/payments/mod.rs | 13 + .../src/clockwork/payments/payment.rs | 187 + .../src/clockwork/payments/update_payment.rs | 157 + .../cmds-solana/src/clockwork/threads/mod.rs | 129 + .../src/clockwork/threads/thread_create.rs | 104 + .../src/clockwork/threads/thread_delete.rs | 121 + .../src/clockwork/threads/thread_pause.rs | 101 + .../src/clockwork/threads/thread_reset.rs | 101 + .../src/clockwork/threads/thread_resume.rs | 101 + .../src/clockwork/threads/thread_update.rs | 124 + .../crates/cmds-solana/src/metaboss/burn.rs | 82 + .../cmds-solana/src/metaboss/burn_print.rs | 94 + .../crates/cmds-solana/src/metaboss/decode.rs | 62 + .../src/metaboss/migrate_collection.rs | 92 + .../crates/cmds-solana/src/metaboss/mod.rs | 11 + .../src/metaboss/primary_sale_happened.rs | 86 + .../cmds-solana/src/metaboss/set_immutable.rs | 84 + .../src/metaboss/snapshot_cm_accounts.rs | 77 + .../src/metaboss/snapshot_holders.rs | 139 + .../src/metaboss/snapshot_mints.rs | 145 + .../src/metaboss/update/creator.rs | 103 + .../cmds-solana/src/metaboss/update/data.rs | 93 + .../cmds-solana/src/metaboss/update/mod.rs | 4 + .../cmds-solana/src/metaboss/update/name.rs | 93 + .../cmds-solana/src/metaboss/update/symbol.rs | 93 + .../src/metaboss/update_authority.rs | 106 + .../proxy_authority/create_proxy_authority.rs | 132 + .../cmds-solana/src/proxy_authority/mod.rs | 3 + .../cmds-solana/src/proxy_authority/utils.rs | 7 + .../cmds-solana/src/xnft/create_install.rs | 103 + .../src/xnft/create_permissioned_install.rs | 105 + .../cmds-solana/src/xnft/create_xnft.rs | 134 + .../cmds-solana/src/xnft/delete_install.rs | 79 + .../cmds-solana/src/xnft/grant_access.rs | 97 + archive/crates/cmds-solana/src/xnft/mod.rs | 130 + .../cmds-solana/src/xnft/parameters.json | 17 + .../cmds-solana/src/xnft/revoke_access.rs | 86 + certs/supabase-prod-ca-2021.crt | 23 + clippy.toml | 2 + crates/cmds-deno/@space-operator | 1 + crates/cmds-deno/Cargo.toml | 30 + crates/cmds-deno/deno.json | 1 + crates/cmds-deno/deno.lock | 322 + crates/cmds-deno/deps.ts | 1 + crates/cmds-deno/deps_jsr.ts | 1 + crates/cmds-deno/deps_local.ts | 1 + .../node-definitions/deno_playground.json | 90 + crates/cmds-deno/run.ts | 5 + crates/cmds-deno/src/lib.rs | 208 + crates/cmds-deno/tests/add.json | 83 + crates/cmds-deno/tests/add.ts | 7 + crates/cmds-deno/tests/deno.json | 1 + crates/cmds-deno/tests/deno.lock | 321 + crates/cmds-pdg/Cargo.toml | 25 + .../node-definitions/gen_metaplex_attrs.json | 70 + .../node-definitions/gen_pdg_attrs.json | 86 + .../node-definitions/generate_base.json | 78 + .../node-definitions/get_effect_list.json | 70 + .../node-definitions/parse_pdg_attrs.json | 86 + .../cmds-pdg/node-definitions/pdg_render.json | 112 + .../node-definitions/push_effect_list.json | 78 + .../update_render_params.json | 78 + crates/cmds-pdg/src/gen_metaplex_attrs.rs | 42 + crates/cmds-pdg/src/gen_pdg_attrs.rs | 76 + crates/cmds-pdg/src/generate_base.rs | 50 + crates/cmds-pdg/src/get_effect_list.rs | 42 + crates/cmds-pdg/src/lib.rs | 8 + crates/cmds-pdg/src/parse_pdg_attrs.rs | 51 + crates/cmds-pdg/src/pdg_render.rs | 230 + crates/cmds-pdg/src/push_effect_list.rs | 39 + crates/cmds-pdg/src/update_render_params.rs | 48 + crates/cmds-solana/Cargo.toml | 128 + .../associated_token_account.json | 100 + .../clockwork/payments/create_payment.json | 124 + .../payments/disburse_payment_ix.json | 118 + .../clockwork/payments/update_payment.json | 94 + .../clockwork/threads/thread_create.json | 116 + .../clockwork/threads/thread_delete.json | 94 + .../compression/burn_cNFT.json | 142 + .../compression/create_tree.json | 132 + .../compression/mint_compressed_NFT.json | 156 + .../compression/mint_to_collection_v1.json | 180 + .../compression/transfer.json | 166 + .../compression/update_cNFT.json | 166 + .../node-definitions/create_mint_account.json | 118 + .../create_proxy_authority.json | 76 + .../create_token_account.json | 102 + .../cmds-solana/node-definitions/das_api.json | 94 + .../node-definitions/find_pda.json | 110 + .../node-definitions/generate_keypair.json | 92 + .../node-definitions/get_balance.json | 70 + .../governance/add_required_signatory.json | 100 + .../governance/add_signatory.json | 124 + .../governance/cancel_proposal.json | 119 + .../governance/cast_vote.json | 164 + .../governance/complete_proposal.json | 102 + .../governance/create_governance.json | 132 + .../governance/create_native_treasury.json | 92 + .../governance/create_proposal.json | 186 + .../governance/create_realm.json | 154 + .../governance/create_token_owner_record.json | 108 + .../governance/deposit_governing_tokens.json | 136 + .../governance/execute_transaction.json | 156 + .../governance/finalize_vote.json | 132 + .../governance/insert_transaction.json | 140 + .../governance/post_message.json | 150 + .../governance/refund_proposal_deposit.json | 100 + .../relinquish_token_owner_record_locks.json | 116 + .../governance/relinquish_vote.json | 140 + .../governance/remove_required_signatory.json | 108 + .../governance/remove_transaction.json | 124 + .../governance/revoke_governing_tokens.json | 136 + .../governance/set_governance_config.json | 94 + .../governance/set_governance_delegate.json | 124 + .../governance/set_realm_authority.json | 110 + .../governance/set_realm_config.json | 142 + .../set_token_owner_record_locks.json | 124 + .../governance/sign_off_proposal.json | 118 + .../governance/withdraw_governing_tokens.json | 128 + .../node-definitions/jupiter/swap.json | 118 + crates/cmds-solana/node-definitions/memo.json | 86 + .../metaboss/update_authority.ignore | 87 + .../node-definitions/mint_token.json | 118 + .../nft/approve_collection_authority.json | 102 + .../nft/approve_use_authority.json | 126 + .../nft/arweave_file_upload.json | 86 + .../nft/arweave_nft_upload.json | 92 + .../nft/auction_house_sell.json | 118 + .../nft/candy_machine/add_config_lines.json | 110 + .../nft/candy_machine/initialize.json | 150 + .../candy_machine/initialize_candy_guard.json | 108 + .../nft/candy_machine/mint.json | 175 + .../nft/candy_machine/wrap.json | 110 + .../add_config_lines_core.json | 110 + .../initialize_candy_machine_core.json | 126 + .../initialize_core_candy_guards.json | 108 + .../nft/candy_machine_core/mint_core.json | 150 + .../nft/candy_machine_core/wrap_core.json | 110 + .../nft/core/collection_sample copy.json | 43 + .../nft/core/collection_sample.json | 43 + .../nft/core/fetch_assets.json | 70 + .../nft/core/mpl_core_create_asset.json | 134 + .../nft/core/mpl_core_create_collection.json | 133 + .../nft/core/mpl_core_update_asset.json | 137 + .../nft/core/mpl_core_update_plugin.json | 110 + .../node-definitions/nft/core/sample.json | 43 + .../nft/create_master_edition.json | 130 + .../nft/create_metadata_account.json | 133 + .../node-definitions/nft/get_left_uses.json | 70 + .../nft/set_token_standard.json | 102 + .../node-definitions/nft/sign_metadata.json | 94 + .../nft/update_metadata_account.json | 124 + .../node-definitions/nft/utilize.json | 126 + .../node-definitions/nft/v1/burn_v1.json | 102 + .../node-definitions/nft/v1/create_v1.json | 200 + .../node-definitions/nft/v1/delegate_v1.json | 122 + .../node-definitions/nft/v1/mint_v1.json | 149 + .../node-definitions/nft/v1/update_v1.json | 124 + .../nft/v1/verify_collection_v1.json | 110 + .../nft/v1/verify_creator_v1.json | 110 + .../nft/verify_collection.json | 110 + .../node-definitions/pyth_price.json | 70 + .../record/initialize_record_with_seed.json | 108 + .../node-definitions/record/read_record.json | 90 + .../record/write_to_record.json | 110 + .../node-definitions/request_airdrop.json | 78 + .../spl_token/set_authority.json | 118 + .../spl_token_2022/set_authority.json | 118 + .../node-definitions/streamflow/create.json | 150 + .../node-definitions/streamflow/withdraw.json | 118 + .../node-definitions/transfer_sol.json | 102 + .../node-definitions/transfer_token.json | 156 + .../cmds-solana/node-definitions/wallet.json | 76 + .../node-definitions/wormhole/get_vaa.json | 92 + .../nft_bridge/eth/redeem_nft_on_eth.json | 86 + .../nft_bridge/eth/transfer_nft_from_eth.json | 126 + .../nft_bridge/nft_complete_native.json | 116 + .../nft_bridge/nft_complete_wrapped.json | 128 + .../nft_bridge/nft_complete_wrapped_meta.json | 120 + .../nft_bridge/nft_transfer_native.json | 144 + .../nft_bridge/nft_transfer_wrapped.json | 136 + .../node-definitions/wormhole/parse_vaa.json | 118 + .../wormhole/post_message.json | 106 + .../node-definitions/wormhole/post_vaa.json | 116 + .../wormhole/token_bridge/attest.json | 118 + .../token_bridge/complete_native.json | 116 + .../complete_transfer_wrapped.json | 122 + .../wormhole/token_bridge/create_wrapped.json | 120 + .../token_bridge/eth/attest_from_eth.json | 98 + .../eth/create_wrapped_on_eth.json | 100 + .../token_bridge/eth/redeem_on_eth.json | 86 + .../token_bridge/eth/transfer_from_eth.json | 126 + .../wormhole/token_bridge/initialize.json | 78 + .../token_bridge/transfer_native.json | 158 + .../token_bridge/transfer_wrapped.json | 152 + .../wormhole/utils/get_foreign_asset_eth.json | 95 + .../wormhole/verify_signatures.json | 110 + .../node-definitions/xnft/create_install.json | 116 + .../xnft/create_permissioned_install.json | 122 + .../node-definitions/xnft/create_xnft.json | 134 + .../node-definitions/xnft/delete_install.json | 102 + .../node-definitions/xnft/grant_access.json | 108 + .../node-definitions/xnft/revoke_access.json | 108 + .../src/associated_token_account.rs | 83 + crates/cmds-solana/src/compression/burn.rs | 192 + .../src/compression/create_tree.rs | 312 + .../cmds-solana/src/compression/metadata.json | 27 + .../src/compression/mint_to_collection_v1.rs | 192 + crates/cmds-solana/src/compression/mint_v1.rs | 101 + crates/cmds-solana/src/compression/mod.rs | 262 + .../cmds-solana/src/compression/transfer.rs | 226 + .../src/compression/types/asset.rs | 334 + .../cmds-solana/src/compression/types/mod.rs | 1 + crates/cmds-solana/src/compression/update.rs | 193 + crates/cmds-solana/src/create_mint_account.rs | 88 + .../cmds-solana/src/create_token_account.rs | 106 + crates/cmds-solana/src/das.rs | 98 + crates/cmds-solana/src/error.rs | 71 + crates/cmds-solana/src/find_pda.rs | 137 + crates/cmds-solana/src/generate_keypair.rs | 200 + crates/cmds-solana/src/get_balance.rs | 48 + .../src/governance/add_required_signatory.rs | 103 + .../src/governance/add_signatory.rs | 131 + .../src/governance/cancel_proposal.rs | 93 + .../cmds-solana/src/governance/cast_vote.rs | 147 + .../src/governance/complete_proposal.rs | 84 + .../src/governance/create_governance.rs | 122 + .../src/governance/create_native_treasury.rs | 88 + .../src/governance/create_proposal.rs | 163 + .../src/governance/create_realm.rs | 213 + .../governance/create_token_owner_record.rs | 112 + .../governance/deposit_governing_tokens.rs | 145 + .../src/governance/execute_transaction.rs | 109 + .../src/governance/finalize_vote.rs | 106 + .../src/governance/insert_transaction.rs | 131 + crates/cmds-solana/src/governance/mod.rs | 1101 ++ .../src/governance/post_message.rs | 142 + .../src/governance/refund_proposal_deposit.rs | 96 + .../relinquish_token_owner_record_locks.rs | 115 + .../src/governance/relinquish_vote.rs | 132 + .../governance/remove_required_signatory.rs | 103 + .../src/governance/remove_transaction.rs | 93 + .../src/governance/revoke_governing_tokens.rs | 128 + .../src/governance/set_governance_config.rs | 72 + .../src/governance/set_governance_delegate.rs | 114 + .../src/governance/set_realm_authority.rs | 97 + .../src/governance/set_realm_config.rs | 149 + .../set_token_owner_record_locks.rs | 111 + .../src/governance/sign_off_proposal.rs | 103 + .../governance/withdraw_governing_tokens.rs | 125 + crates/cmds-solana/src/jupiter/mod.rs | 1 + crates/cmds-solana/src/jupiter/swap.rs | 162 + crates/cmds-solana/src/lib.rs | 101 + crates/cmds-solana/src/memo.rs | 47 + crates/cmds-solana/src/mint_token.rs | 81 + .../src/nft/approve_collection_authority.rs | 94 + .../src/nft/approve_use_authority.rs | 211 + .../src/nft/arweave_file_upload.rs | 94 + .../cmds-solana/src/nft/arweave_nft_upload.rs | 406 + .../add_config_lines_core.rs | 74 + .../initialize_core_candy_guards.rs | 93 + .../initialize_core_candy_machine.rs | 116 + .../src/nft/candy_machine_core/mint_core.rs | 119 + .../src/nft/candy_machine_core/mod.rs | 606 + .../src/nft/candy_machine_core/wrap_core.rs | 78 + .../nft/candy_machine_v3/add_config_lines.rs | 74 + .../src/nft/candy_machine_v3/guards.json | 77 + .../src/nft/candy_machine_v3/initialize.rs | 182 + .../initialize_candy_guard.rs | 95 + .../src/nft/candy_machine_v3/mint.rs | 176 + .../src/nft/candy_machine_v3/mod.rs | 594 + .../src/nft/candy_machine_v3/wrap.rs | 75 + .../cmds-solana/src/nft/core/create_asset.rs | 146 + .../src/nft/core/create_collection.rs | 176 + .../cmds-solana/src/nft/core/fetch_assets.rs | 85 + crates/cmds-solana/src/nft/core/mod.rs | 5 + .../cmds-solana/src/nft/core/update_asset.rs | 85 + .../cmds-solana/src/nft/core/update_plugin.rs | 74 + .../src/nft/create_master_edition.rs | 87 + .../src/nft/create_metadata_account.rs | 160 + crates/cmds-solana/src/nft/get_left_uses.rs | 73 + crates/cmds-solana/src/nft/mod.rs | 332 + .../cmds-solana/src/nft/set_token_standard.rs | 67 + crates/cmds-solana/src/nft/sign_metadata.rs | 135 + .../src/nft/update_metadata_account.rs | 95 + crates/cmds-solana/src/nft/v1/burn_v1.rs | 103 + crates/cmds-solana/src/nft/v1/create_v1.rs | 173 + crates/cmds-solana/src/nft/v1/delegate_v1.rs | 775 ++ crates/cmds-solana/src/nft/v1/mint_v1.rs | 110 + crates/cmds-solana/src/nft/v1/mod.rs | 87 + crates/cmds-solana/src/nft/v1/update_v1.rs | 830 ++ .../src/nft/v1/verify_collection_v1.rs | 72 + .../src/nft/v1/verify_creator_v1.rs | 78 + .../cmds-solana/src/nft/verify_collection.rs | 95 + crates/cmds-solana/src/pyth_price.rs | 53 + .../src/record/initialize_record_with_seed.rs | 107 + crates/cmds-solana/src/record/mod.rs | 101 + crates/cmds-solana/src/record/read_record.rs | 48 + .../cmds-solana/src/record/write_to_record.rs | 71 + crates/cmds-solana/src/request_airdrop.rs | 57 + crates/cmds-solana/src/spl/mod.rs | 1 + crates/cmds-solana/src/spl/set_authority.rs | 82 + crates/cmds-solana/src/spl_token_2022/mod.rs | 1 + .../src/spl_token_2022/set_authority.rs | 61 + crates/cmds-solana/src/streamflow/create.rs | 172 + crates/cmds-solana/src/streamflow/mod.rs | 232 + crates/cmds-solana/src/streamflow/withdraw.rs | 176 + crates/cmds-solana/src/transfer_sol.rs | 114 + crates/cmds-solana/src/transfer_token.rs | 234 + crates/cmds-solana/src/utils.rs | 92 + crates/cmds-solana/src/utils/bundlr_signer.rs | 1 + crates/cmds-solana/src/wallet.rs | 123 + crates/cmds-solana/src/wormhole/get_vaa.rs | 96 + crates/cmds-solana/src/wormhole/mod.rs | 319 + .../wormhole/nft_bridge/complete_native.rs | 159 + .../wormhole/nft_bridge/complete_wrapped.rs | 265 + .../nft_bridge/complete_wrapped_meta.rs | 178 + .../src/wormhole/nft_bridge/eth/mod.rs | 2 + .../nft_bridge/eth/redeem_nft_on_eth.rs | 173 + .../nft_bridge/eth/transfer_nft_from_eth.rs | 165 + .../src/wormhole/nft_bridge/mod.rs | 69 + .../wormhole/nft_bridge/transfer_native.rs | 172 + .../wormhole/nft_bridge/transfer_wrapped.rs | 164 + crates/cmds-solana/src/wormhole/parse_vaa.rs | 274 + .../cmds-solana/src/wormhole/post_message.rs | 118 + crates/cmds-solana/src/wormhole/post_vaa.rs | 95 + .../src/wormhole/token_bridge/attest.rs | 138 + .../wormhole/token_bridge/complete_native.rs | 160 + .../token_bridge/complete_transfer_wrapped.rs | 207 + .../wormhole/token_bridge/create_wrapped.rs | 174 + .../token_bridge/eth/attest_from_eth.rs | 172 + .../token_bridge/eth/create_wrapped_on_eth.rs | 164 + .../src/wormhole/token_bridge/eth/mod.rs | 165 + .../token_bridge/eth/redeem_on_eth.rs | 172 + .../token_bridge/eth/transfer_from_eth.rs | 186 + .../src/wormhole/token_bridge/initialize.rs | 73 + .../src/wormhole/token_bridge/mod.rs | 226 + .../wormhole/token_bridge/transfer_native.rs | 166 + .../wormhole/token_bridge/transfer_wrapped.rs | 167 + .../wormhole/utils/get_foreign_asset_eth.rs | 62 + crates/cmds-solana/src/wormhole/utils/mod.rs | 1 + .../src/wormhole/verify_signatures.rs | 142 + crates/cmds-std/Cargo.toml | 35 + crates/cmds-std/benches/postgrest.rs | 69 + crates/cmds-std/node-definitions/collect.json | 70 + crates/cmds-std/node-definitions/const.json | 61 + .../node-definitions/display/chart.json | 71 + .../node-definitions/display/json_viewer.json | 63 + .../node-definitions/display/table.json | 71 + .../cmds-std/node-definitions/flow_input.json | 65 + .../node-definitions/flow_output.json | 76 + .../node-definitions/flow_run_info.json | 73 + crates/cmds-std/node-definitions/foreach.json | 85 + crates/cmds-std/node-definitions/http.json | 124 + .../cmds-std/node-definitions/interflow.json | 63 + .../interflow_instructions.json | 82 + .../node-definitions/json/json_get_field.json | 92 + .../node-definitions/json_extract.json | 102 + .../node-definitions/json_insert.json | 87 + .../kvstore/create_store.json | 63 + .../kvstore/delete_store.json | 63 + .../node-definitions/kvstore/kv_explorer.json | 57 + .../node-definitions/kvstore/read_item.json | 92 + .../node-definitions/kvstore/write_item.json | 87 + crates/cmds-std/node-definitions/note.json | 70 + .../postgrest/builder_eq.json | 101 + .../postgrest/builder_insert.json | 78 + .../postgrest/builder_is.json | 101 + .../postgrest/builder_limit.json | 89 + .../postgrest/builder_match.json | 78 + .../postgrest/builder_neq.json | 101 + .../postgrest/builder_not.json | 113 + .../postgrest/builder_order.json | 89 + .../postgrest/builder_select.json | 89 + .../postgrest/builder_update.json | 78 + .../postgrest/builder_upsert.json | 78 + .../postgrest/execute_query.json | 84 + .../node-definitions/postgrest/new_query.json | 97 + .../node-definitions/postgrest/new_rpc.json | 105 + crates/cmds-std/node-definitions/print.json | 70 + .../node-definitions/std/expression.json | 78 + .../node-definitions/std/math_operation.json | 118 + .../cmds-std/node-definitions/std/range.json | 86 + .../node-definitions/std/to_bytes.json | 70 + .../node-definitions/std/to_string.json | 71 + .../cmds-std/node-definitions/std/to_vec.json | 78 + .../storage/create_signed_url.json | 85 + .../node-definitions/storage/delete.json | 54 + .../node-definitions/storage/download.json | 66 + .../storage/file_explorer.json | 67 + .../storage/get_file_metadata.json | 66 + .../storage/get_public_url.json | 54 + .../node-definitions/storage/list.json | 44 + .../node-definitions/storage/upload.json | 81 + .../node-definitions/supabase/supabase.json | 70 + crates/cmds-std/node-definitions/wait.json | 79 + crates/cmds-std/src/const_cmd.rs | 196 + crates/cmds-std/src/error.rs | 34 + crates/cmds-std/src/flow_run_info.rs | 31 + crates/cmds-std/src/http_request.rs | 284 + crates/cmds-std/src/json_extract.rs | 65 + crates/cmds-std/src/json_insert.rs | 67 + crates/cmds-std/src/kvstore/create_store.rs | 51 + crates/cmds-std/src/kvstore/delete_store.rs | 51 + crates/cmds-std/src/kvstore/explorer.rs | 147 + crates/cmds-std/src/kvstore/mod.rs | 5 + crates/cmds-std/src/kvstore/read_item.rs | 75 + crates/cmds-std/src/kvstore/write_item.rs | 66 + crates/cmds-std/src/lib.rs | 51 + crates/cmds-std/src/note.rs | 29 + crates/cmds-std/src/postgrest/builder_eq.rs | 43 + .../cmds-std/src/postgrest/builder_insert.rs | 42 + crates/cmds-std/src/postgrest/builder_is.rs | 43 + .../cmds-std/src/postgrest/builder_limit.rs | 42 + .../cmds-std/src/postgrest/builder_match.rs | 54 + crates/cmds-std/src/postgrest/builder_neq.rs | 43 + crates/cmds-std/src/postgrest/builder_not.rs | 44 + .../cmds-std/src/postgrest/builder_order.rs | 42 + .../cmds-std/src/postgrest/builder_select.rs | 42 + .../cmds-std/src/postgrest/builder_update.rs | 42 + .../cmds-std/src/postgrest/builder_upsert.rs | 42 + .../cmds-std/src/postgrest/execute_query.rs | 95 + crates/cmds-std/src/postgrest/mod.rs | 14 + crates/cmds-std/src/postgrest/new_query.rs | 43 + crates/cmds-std/src/postgrest/new_rpc.rs | 48 + crates/cmds-std/src/print_cmd.rs | 65 + crates/cmds-std/src/std/json_get_field.rs | 141 + crates/cmds-std/src/std/mod.rs | 5 + crates/cmds-std/src/std/range.rs | 58 + crates/cmds-std/src/std/to_bytes.rs | 32 + crates/cmds-std/src/std/to_string.rs | 68 + crates/cmds-std/src/std/to_vec.rs | 73 + .../cmds-std/src/storage/create_signed_url.rs | 137 + crates/cmds-std/src/storage/delete.rs | 52 + crates/cmds-std/src/storage/download.rs | 78 + crates/cmds-std/src/storage/explorer.rs | 59 + .../cmds-std/src/storage/get_file_metadata.rs | 80 + crates/cmds-std/src/storage/get_public_url.rs | 39 + crates/cmds-std/src/storage/list.rs | 92 + crates/cmds-std/src/storage/mod.rs | 33 + crates/cmds-std/src/storage/upload.rs | 116 + crates/cmds-std/src/supabase/mod.rs | 64 + crates/cmds-std/src/wait_cmd.rs | 33 + crates/command-rpc/Cargo.toml | 25 + crates/command-rpc/src/client.rs | 278 + crates/command-rpc/src/lib.rs | 2 + crates/command-rpc/src/server.rs | 53 + crates/db/Cargo.toml | 54 + crates/db/src/apikey.rs | 258 + crates/db/src/config.rs | 131 + crates/db/src/connection/admin.rs | 578 + crates/db/src/connection/conn_impl.rs | 1627 +++ crates/db/src/connection/csv_export.rs | 93 + crates/db/src/connection/mod.rs | 318 + crates/db/src/connection/proxied_user_conn.rs | 406 + crates/db/src/error.rs | 297 + crates/db/src/lib.rs | 68 + crates/db/src/local_storage/mod.rs | 181 + crates/db/src/pool.rs | 278 + crates/db/src/wasm_storage.rs | 63 + crates/flow-server/Cargo.toml | 70 + crates/flow-server/Dockerfile | 47 + crates/flow-server/benches/crypto.rs | 32 + crates/flow-server/entrypoint.bash | 5 + crates/flow-server/src/api/apikey_info.rs | 24 + crates/flow-server/src/api/auth_proxy.rs | 26 + crates/flow-server/src/api/claim_token.rs | 52 + crates/flow-server/src/api/clone_flow.rs | 44 + crates/flow-server/src/api/confirm_auth.rs | 34 + crates/flow-server/src/api/create_apikey.rs | 42 + crates/flow-server/src/api/data_export.rs | 18 + crates/flow-server/src/api/data_import.rs | 20 + crates/flow-server/src/api/db_push_logs.rs | 25 + crates/flow-server/src/api/db_rpc.rs | 24 + crates/flow-server/src/api/delete_apikey.rs | 30 + crates/flow-server/src/api/deploy_flow.rs | 29 + crates/flow-server/src/api/flow_run/mod.rs | 80 + crates/flow-server/src/api/get_flow_output.rs | 41 + crates/flow-server/src/api/get_info.rs | 24 + .../src/api/get_signature_request.rs | 89 + crates/flow-server/src/api/init_auth.rs | 29 + .../src/api/kvstore/create_store.rs | 71 + .../src/api/kvstore/delete_item.rs | 33 + .../src/api/kvstore/delete_store.rs | 31 + crates/flow-server/src/api/kvstore/mod.rs | 6 + .../flow-server/src/api/kvstore/read_item.rs | 36 + .../flow-server/src/api/kvstore/write_item.rs | 49 + crates/flow-server/src/api/mod.rs | 63 + .../flow-server/src/api/start_deployment.rs | 99 + crates/flow-server/src/api/start_flow.rs | 74 + .../flow-server/src/api/start_flow_shared.rs | 79 + .../src/api/start_flow_unverified.rs | 93 + crates/flow-server/src/api/stop_flow.rs | 46 + .../flow-server/src/api/submit_signature.rs | 38 + crates/flow-server/src/api/upsert_wallet.rs | 27 + crates/flow-server/src/api/ws_auth_proxy.rs | 32 + .../src/db_worker/flow_run_worker.rs | 434 + crates/flow-server/src/db_worker/messages.rs | 43 + crates/flow-server/src/db_worker/mod.rs | 385 + crates/flow-server/src/db_worker/signer.rs | 209 + .../flow-server/src/db_worker/token_worker.rs | 441 + .../flow-server/src/db_worker/user_worker.rs | 868 ++ crates/flow-server/src/error.rs | 87 + crates/flow-server/src/flow_logs.rs | 244 + crates/flow-server/src/lib.rs | 566 + crates/flow-server/src/main.rs | 298 + crates/flow-server/src/middleware/auth.rs | 499 + crates/flow-server/src/middleware/auth_v1.rs | 386 + crates/flow-server/src/middleware/mod.rs | 19 + crates/flow-server/src/middleware/req_fn.rs | 103 + crates/flow-server/src/user.rs | 426 + crates/flow-server/src/ws.rs | 358 + crates/flow/.gitignore | 2 + crates/flow/Cargo.toml | 53 + crates/flow/src/command/collect.rs | 55 + crates/flow/src/command/flow_input.rs | 64 + crates/flow/src/command/flow_output.rs | 59 + crates/flow/src/command/foreach.rs | 56 + crates/flow/src/command/interflow.rs | 124 + .../src/command/interflow_instructions.rs | 136 + crates/flow/src/command/mod.rs | 26 + crates/flow/src/command/rhai.rs | 64 + crates/flow/src/command/wasm.rs | 101 + crates/flow/src/context.rs | 110 + crates/flow/src/error.rs | 48 + crates/flow/src/flow_graph.rs | 1972 +++ crates/flow/src/flow_registry.rs | 670 + crates/flow/src/flow_run_events.rs | 155 + crates/flow/src/flow_set.rs | 377 + crates/flow/src/lib.rs | 10 + crates/flow/test_files/2_foreach.json | 446 + .../test_files/deno_instructions/deno.json | 1 + .../test_files/deno_instructions/deno.lock | 323 + .../deno_instructions/node-definition.json | 97 + .../deno_instructions/transfer_sol.ts | 31 + crates/flow/test_files/deno_sig/deno.json | 1 + crates/flow/test_files/deno_sig/deno.lock | 391 + .../test_files/deno_sig/node-definition.json | 92 + .../flow/test_files/deno_sig/transfer_sol.ts | 41 + crates/flow/test_files/nft.json | 1967 +++ crates/flow/test_files/uneven_loop.json | 840 ++ crates/integration-tests/Cargo.toml | 10 + crates/integration-tests/src/main.rs | 65 + crates/pdg-common/Cargo.toml | 17 + crates/pdg-common/src/lib.rs | 210 + .../pdg-common/src/nft_metadata/generate.rs | 654 + .../pdg-common/src/nft_metadata/metaplex.rs | 291 + crates/pdg-common/src/nft_metadata/mod.rs | 2121 +++ crates/pdg-common/src/nft_metadata/pdg.rs | 27 + .../src/nft_metadata/tests/123.json | 1 + .../nft_metadata/tests/postman request.json | 513 + crates/rhai-script/Cargo.toml | 20 + .../node-definitions/rhai_script_0x1.json | 89 + .../node-definitions/rhai_script_0x2.json | 96 + .../node-definitions/rhai_script_0x3.json | 103 + .../node-definitions/rhai_script_0x4.json | 110 + .../node-definitions/rhai_script_0x5.json | 117 + .../node-definitions/rhai_script_1x1.json | 99 + .../node-definitions/rhai_script_1x2.json | 106 + .../node-definitions/rhai_script_1x3.json | 113 + .../node-definitions/rhai_script_1x4.json | 120 + .../node-definitions/rhai_script_1x5.json | 127 + .../node-definitions/rhai_script_2x1.json | 109 + .../node-definitions/rhai_script_2x2.json | 116 + .../node-definitions/rhai_script_2x3.json | 123 + .../node-definitions/rhai_script_2x4.json | 130 + .../node-definitions/rhai_script_2x5.json | 137 + .../node-definitions/rhai_script_3x1.json | 119 + .../node-definitions/rhai_script_3x2.json | 126 + .../node-definitions/rhai_script_3x3.json | 133 + .../node-definitions/rhai_script_3x4.json | 140 + .../node-definitions/rhai_script_3x5.json | 147 + .../node-definitions/rhai_script_4x1.json | 129 + .../node-definitions/rhai_script_4x2.json | 136 + .../node-definitions/rhai_script_4x3.json | 143 + .../node-definitions/rhai_script_4x4.json | 150 + .../node-definitions/rhai_script_4x5.json | 157 + .../node-definitions/rhai_script_5x1.json | 139 + .../node-definitions/rhai_script_5x2.json | 146 + .../node-definitions/rhai_script_5x3.json | 153 + .../node-definitions/rhai_script_5x4.json | 160 + .../node-definitions/rhai_script_5x5.json | 167 + crates/rhai-script/src/bin/gen.rs | 87 + crates/rhai-script/src/convert.rs | 73 + crates/rhai-script/src/lib.rs | 167 + crates/space-wasm/.gitignore | 2 + crates/space-wasm/Cargo.toml | 31 + crates/space-wasm/src/ffi.rs | 89 + crates/space-wasm/src/lib.rs | 127 + crates/space-wasm/src/tests.rs | 125 + .../tests/automatic/.cargo/config.toml | 2 + crates/space-wasm/tests/automatic/Cargo.lock | 137 + crates/space-wasm/tests/automatic/Cargo.toml | 15 + crates/space-wasm/tests/automatic/src/lib.rs | 22 + .../space-wasm/tests/env/.cargo/config.toml | 2 + crates/space-wasm/tests/env/Cargo.lock | 136 + crates/space-wasm/tests/env/Cargo.toml | 14 + crates/space-wasm/tests/env/src/lib.rs | 6 + .../space-wasm/tests/float/.cargo/config.toml | 2 + crates/space-wasm/tests/float/Cargo.lock | 136 + crates/space-wasm/tests/float/Cargo.toml | 14 + crates/space-wasm/tests/float/src/lib.rs | 7 + .../space-wasm/tests/http/.cargo/config.toml | 2 + crates/space-wasm/tests/http/Cargo.lock | 161 + crates/space-wasm/tests/http/Cargo.toml | 15 + crates/space-wasm/tests/http/src/lib.rs | 20 + .../tests/manual/.cargo/config.toml | 2 + crates/space-wasm/tests/manual/Cargo.lock | 115 + crates/space-wasm/tests/manual/Cargo.toml | 15 + crates/space-wasm/tests/manual/src/lib.rs | 46 + .../tests/number/.cargo/config.toml | 2 + crates/space-wasm/tests/number/Cargo.lock | 136 + crates/space-wasm/tests/number/Cargo.toml | 14 + crates/space-wasm/tests/number/src/lib.rs | 6 + .../tests/simple/.cargo/config.toml | 2 + crates/space-wasm/tests/simple/Cargo.lock | 136 + crates/space-wasm/tests/simple/Cargo.toml | 14 + crates/space-wasm/tests/simple/src/lib.rs | 6 + crates/srpc/Cargo.toml | 31 + crates/srpc/benches/srpc_bench.rs | 72 + crates/srpc/src/lib.rs | 571 + crates/utils/Cargo.toml | 17 + crates/utils/src/actix_service.rs | 57 + crates/utils/src/address_book.rs | 242 + crates/utils/src/lib.rs | 64 + crates/utils/src/serde_base64.rs | 54 + crates/utils/src/serde_bs58.rs | 66 + docker/.gitignore | 6 + docker/README.md | 59 + docker/deno.json | 11 + docker/deno.lock | 335 + docker/docker-compose.yml | 342 + docker/env.example | 108 + docker/flow-server-config.toml | 27 + docker/gen-secrets.ts | 106 + docker/import-data.ts | 48 + .../migrations/20240514130738_init.sql | 651 + .../migrations/20240517061121_grant.sql | 6 + .../20240518143018_auth_trigger.sql | 17 + .../20240524150823_grant_sequence.sql | 1 + .../migrations/20240525104546_kvstore_fk.sql | 14 + .../migrations/20240905183752_encrypt.sql | 2 + .../20240906100833_grant_wallet_update.sql | 1 + .../20240907035451_update_wallets_table.sql | 3 + .../20240930062601_remove_keypair.sql | 1 + .../20241008071914_update_nodes.sql | 7 + .../20241008143527_nodes_update_policy.sql | 1 + .../migrations/20241008144128_with_check.sql | 1 + .../20241030051429_check_native_nodes.sql | 3 + .../20241202120303_update_flows_table.sql | 8 + .../20241214133549_flow_deployment.sql | 77 + .../20241230141331_wallet_purpose.sql | 1 + .../20241230142807_flow_gg_marketplace.sql | 1 + docker/tests/login.ts | 59 + docker/volumes/api/kong.yml | 258 + docker/volumes/db/init/data.sql | 0 docker/volumes/db/jwt.sql | 5 + docker/volumes/db/logs.sql | 4 + docker/volumes/db/realtime.sql | 4 + docker/volumes/db/roles.sql | 11 + docker/volumes/db/webhooks.sql | 208 + docker/volumes/functions/main/index.ts | 94 + docker/volumes/logs/vector.yml | 232 + guest.toml | 25 + lib/client/Cargo.toml | 12 + lib/client/src/lib.rs | 1 + lib/client/src/types.rs | 320 + lib/flow-lib/.gitignore | 2 + lib/flow-lib/Cargo.toml | 42 + lib/flow-lib/src/command/builder.rs | 239 + lib/flow-lib/src/command/mod.rs | 194 + lib/flow-lib/src/config/client.rs | 343 + lib/flow-lib/src/config/mod.rs | 340 + lib/flow-lib/src/config/node.rs | 41 + lib/flow-lib/src/context.rs | 583 + lib/flow-lib/src/lib.rs | 44 + lib/flow-lib/src/solana.rs | 1321 ++ lib/flow-lib/src/solana/utils.rs | 192 + lib/flow-lib/src/solana/watcher.rs | 171 + lib/flow-lib/src/utils/extensions.rs | 272 + lib/flow-lib/src/utils/mod.rs | 9 + lib/flow-lib/src/utils/tower_client.rs | 130 + lib/flow-value/Cargo.toml | 30 + lib/flow-value/src/crud.rs | 114 + lib/flow-value/src/crud/path.rs | 95 + lib/flow-value/src/de.rs | 560 + lib/flow-value/src/de/const_bytes.rs | 68 + lib/flow-value/src/de/de_enum.rs | 144 + lib/flow-value/src/de/de_struct.rs | 232 + lib/flow-value/src/de/text_repr.rs | 171 + lib/flow-value/src/decimal.rs | 132 + lib/flow-value/src/json_repr/iter_ser.rs | 56 + lib/flow-value/src/keypair.rs | 93 + lib/flow-value/src/lib.rs | 897 ++ lib/flow-value/src/macros.rs | 35 + lib/flow-value/src/pubkey.rs | 71 + lib/flow-value/src/ser.rs | 406 + lib/flow-value/src/ser/iter_ser.rs | 56 + lib/flow-value/src/ser/map_key.rs | 180 + lib/flow-value/src/ser/maps.rs | 106 + lib/flow-value/src/ser/seq.rs | 346 + lib/flow-value/src/ser/tagged_bytes.rs | 185 + lib/flow-value/src/ser/text_repr.rs | 63 + lib/flow-value/src/signature.rs | 64 + lib/flow-value/src/tests/ser.json | 1 + lib/flow-value/src/value_type.rs | 148 + lib/flow-value/src/with.rs | 663 + lib/space-lib/Cargo.toml | 17 + lib/space-lib/README.md | 46 + lib/space-lib/src/common.rs | 19 + lib/space-lib/src/error.rs | 26 + lib/space-lib/src/ffi.rs | 38 + lib/space-lib/src/http.rs | 88 + lib/space-lib/src/lib.rs | 57 + lib/space-macro/Cargo.toml | 16 + lib/space-macro/src/lib.rs | 56 + lib/space-operator-cli/CHANGELOG.md | 14 + lib/space-operator-cli/Cargo.lock | 2965 +++++ lib/space-operator-cli/Cargo.toml | 47 + lib/space-operator-cli/README.md | 436 + lib/space-operator-cli/src/main.rs | 1880 +++ lib/space-operator-cli/src/schema.rs | 151 + lib/spo-helius/Cargo.toml | 15 + lib/spo-helius/src/lib.rs | 242 + node-definition.json | 89 + schema/node-definition.schema.json | 151 + schema/value.schema.json | 255 + scripts/build_images.bash | 37 + scripts/build_wasm_tests.bash | 16 + scripts/ecr-push.bash | 36 + test_files/HTTP Request.json | 1506 +++ test_files/const_form_data.json | 750 ++ test_files/file_upload.json | 338 + test_files/foreach.json | 688 + test_files/generate_keypair.json | 531 + test_files/interflow_simple.json | 152 + test_files/subflow.json | 259 + 787 files changed, 121984 insertions(+) create mode 100644 .cargo/config.toml create mode 100755 .github/assert_cargo_lock_unchanged.bash create mode 100644 .github/workflows/deps.yml create mode 100644 .github/workflows/docker-main.yaml create mode 100644 .github/workflows/docker.yaml create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 @space-operator/client/LICENSE create mode 100644 @space-operator/client/deno.json create mode 100644 @space-operator/client/deno.lock create mode 100644 @space-operator/client/notebook/Untitled.ipynb create mode 100644 @space-operator/client/src/client.ts create mode 100644 @space-operator/client/src/deps.ts create mode 100644 @space-operator/client/src/mod.ts create mode 100644 @space-operator/client/src/supabase.ts create mode 100644 @space-operator/client/src/types/common.ts create mode 100644 @space-operator/client/src/types/rest.ts create mode 100644 @space-operator/client/src/types/ws.ts create mode 100644 @space-operator/client/src/ws.ts create mode 100644 @space-operator/client/tests/auth.ts create mode 100644 @space-operator/client/tests/deploy.ts create mode 100644 @space-operator/client/tests/flow.ts create mode 100644 @space-operator/client/tests/upsert.ts create mode 100644 @space-operator/deno-command-rpc/README.md create mode 100644 @space-operator/deno-command-rpc/deno.json create mode 100644 @space-operator/deno-command-rpc/deno.lock create mode 100644 @space-operator/deno-command-rpc/src/deps.ts create mode 100644 @space-operator/deno-command-rpc/src/mod.ts create mode 100644 @space-operator/deno-command-rpc/src/utils.ts create mode 100644 @space-operator/deno.json create mode 100644 @space-operator/deno.lock create mode 100644 @space-operator/flow-lib/deno.json create mode 100644 @space-operator/flow-lib/deno.lock create mode 100644 @space-operator/flow-lib/src/command.ts create mode 100644 @space-operator/flow-lib/src/common.ts create mode 100644 @space-operator/flow-lib/src/context.ts create mode 100644 @space-operator/flow-lib/src/deps.ts create mode 100644 @space-operator/flow-lib/src/mod.ts create mode 100644 @space-operator/flow-lib/src/value.ts create mode 100644 @space-operator/flow-lib/tests/value.ts create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 admin.toml create mode 100644 archive/cmds-solana/src/nft/need_new_command_interface/auction_house_sell.rs create mode 100644 archive/cmds-solana/src/nft/need_new_command_interface/create_auction_house.rs create mode 100644 archive/cmds-solana/src/nft/need_new_command_interface/utilize.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/mod.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/payments/disburse_payment_ix.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/payments/mod.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/payments/payment.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/payments/update_payment.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/mod.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_create.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_delete.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_pause.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_reset.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_resume.rs create mode 100644 archive/crates/cmds-solana/src/clockwork/threads/thread_update.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/burn.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/burn_print.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/decode.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/migrate_collection.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/mod.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/primary_sale_happened.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/set_immutable.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/snapshot_cm_accounts.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/snapshot_holders.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/snapshot_mints.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update/creator.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update/data.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update/mod.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update/name.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update/symbol.rs create mode 100644 archive/crates/cmds-solana/src/metaboss/update_authority.rs create mode 100644 archive/crates/cmds-solana/src/proxy_authority/create_proxy_authority.rs create mode 100644 archive/crates/cmds-solana/src/proxy_authority/mod.rs create mode 100644 archive/crates/cmds-solana/src/proxy_authority/utils.rs create mode 100644 archive/crates/cmds-solana/src/xnft/create_install.rs create mode 100644 archive/crates/cmds-solana/src/xnft/create_permissioned_install.rs create mode 100644 archive/crates/cmds-solana/src/xnft/create_xnft.rs create mode 100644 archive/crates/cmds-solana/src/xnft/delete_install.rs create mode 100644 archive/crates/cmds-solana/src/xnft/grant_access.rs create mode 100644 archive/crates/cmds-solana/src/xnft/mod.rs create mode 100644 archive/crates/cmds-solana/src/xnft/parameters.json create mode 100644 archive/crates/cmds-solana/src/xnft/revoke_access.rs create mode 100644 certs/supabase-prod-ca-2021.crt create mode 100644 clippy.toml create mode 120000 crates/cmds-deno/@space-operator create mode 100644 crates/cmds-deno/Cargo.toml create mode 100644 crates/cmds-deno/deno.json create mode 100644 crates/cmds-deno/deno.lock create mode 120000 crates/cmds-deno/deps.ts create mode 100644 crates/cmds-deno/deps_jsr.ts create mode 100644 crates/cmds-deno/deps_local.ts create mode 100644 crates/cmds-deno/node-definitions/deno_playground.json create mode 100644 crates/cmds-deno/run.ts create mode 100644 crates/cmds-deno/src/lib.rs create mode 100644 crates/cmds-deno/tests/add.json create mode 100644 crates/cmds-deno/tests/add.ts create mode 100644 crates/cmds-deno/tests/deno.json create mode 100644 crates/cmds-deno/tests/deno.lock create mode 100644 crates/cmds-pdg/Cargo.toml create mode 100644 crates/cmds-pdg/node-definitions/gen_metaplex_attrs.json create mode 100644 crates/cmds-pdg/node-definitions/gen_pdg_attrs.json create mode 100644 crates/cmds-pdg/node-definitions/generate_base.json create mode 100644 crates/cmds-pdg/node-definitions/get_effect_list.json create mode 100644 crates/cmds-pdg/node-definitions/parse_pdg_attrs.json create mode 100644 crates/cmds-pdg/node-definitions/pdg_render.json create mode 100644 crates/cmds-pdg/node-definitions/push_effect_list.json create mode 100644 crates/cmds-pdg/node-definitions/update_render_params.json create mode 100644 crates/cmds-pdg/src/gen_metaplex_attrs.rs create mode 100644 crates/cmds-pdg/src/gen_pdg_attrs.rs create mode 100644 crates/cmds-pdg/src/generate_base.rs create mode 100644 crates/cmds-pdg/src/get_effect_list.rs create mode 100644 crates/cmds-pdg/src/lib.rs create mode 100644 crates/cmds-pdg/src/parse_pdg_attrs.rs create mode 100644 crates/cmds-pdg/src/pdg_render.rs create mode 100644 crates/cmds-pdg/src/push_effect_list.rs create mode 100644 crates/cmds-pdg/src/update_render_params.rs create mode 100644 crates/cmds-solana/Cargo.toml create mode 100644 crates/cmds-solana/node-definitions/associated_token_account.json create mode 100644 crates/cmds-solana/node-definitions/clockwork/payments/create_payment.json create mode 100644 crates/cmds-solana/node-definitions/clockwork/payments/disburse_payment_ix.json create mode 100644 crates/cmds-solana/node-definitions/clockwork/payments/update_payment.json create mode 100644 crates/cmds-solana/node-definitions/clockwork/threads/thread_create.json create mode 100644 crates/cmds-solana/node-definitions/clockwork/threads/thread_delete.json create mode 100644 crates/cmds-solana/node-definitions/compression/burn_cNFT.json create mode 100644 crates/cmds-solana/node-definitions/compression/create_tree.json create mode 100644 crates/cmds-solana/node-definitions/compression/mint_compressed_NFT.json create mode 100644 crates/cmds-solana/node-definitions/compression/mint_to_collection_v1.json create mode 100644 crates/cmds-solana/node-definitions/compression/transfer.json create mode 100644 crates/cmds-solana/node-definitions/compression/update_cNFT.json create mode 100644 crates/cmds-solana/node-definitions/create_mint_account.json create mode 100644 crates/cmds-solana/node-definitions/create_proxy_authority.json create mode 100644 crates/cmds-solana/node-definitions/create_token_account.json create mode 100644 crates/cmds-solana/node-definitions/das_api.json create mode 100644 crates/cmds-solana/node-definitions/find_pda.json create mode 100644 crates/cmds-solana/node-definitions/generate_keypair.json create mode 100644 crates/cmds-solana/node-definitions/get_balance.json create mode 100644 crates/cmds-solana/node-definitions/governance/add_required_signatory.json create mode 100644 crates/cmds-solana/node-definitions/governance/add_signatory.json create mode 100644 crates/cmds-solana/node-definitions/governance/cancel_proposal.json create mode 100644 crates/cmds-solana/node-definitions/governance/cast_vote.json create mode 100644 crates/cmds-solana/node-definitions/governance/complete_proposal.json create mode 100644 crates/cmds-solana/node-definitions/governance/create_governance.json create mode 100644 crates/cmds-solana/node-definitions/governance/create_native_treasury.json create mode 100644 crates/cmds-solana/node-definitions/governance/create_proposal.json create mode 100644 crates/cmds-solana/node-definitions/governance/create_realm.json create mode 100644 crates/cmds-solana/node-definitions/governance/create_token_owner_record.json create mode 100644 crates/cmds-solana/node-definitions/governance/deposit_governing_tokens.json create mode 100644 crates/cmds-solana/node-definitions/governance/execute_transaction.json create mode 100644 crates/cmds-solana/node-definitions/governance/finalize_vote.json create mode 100644 crates/cmds-solana/node-definitions/governance/insert_transaction.json create mode 100644 crates/cmds-solana/node-definitions/governance/post_message.json create mode 100644 crates/cmds-solana/node-definitions/governance/refund_proposal_deposit.json create mode 100644 crates/cmds-solana/node-definitions/governance/relinquish_token_owner_record_locks.json create mode 100644 crates/cmds-solana/node-definitions/governance/relinquish_vote.json create mode 100644 crates/cmds-solana/node-definitions/governance/remove_required_signatory.json create mode 100644 crates/cmds-solana/node-definitions/governance/remove_transaction.json create mode 100644 crates/cmds-solana/node-definitions/governance/revoke_governing_tokens.json create mode 100644 crates/cmds-solana/node-definitions/governance/set_governance_config.json create mode 100644 crates/cmds-solana/node-definitions/governance/set_governance_delegate.json create mode 100644 crates/cmds-solana/node-definitions/governance/set_realm_authority.json create mode 100644 crates/cmds-solana/node-definitions/governance/set_realm_config.json create mode 100644 crates/cmds-solana/node-definitions/governance/set_token_owner_record_locks.json create mode 100644 crates/cmds-solana/node-definitions/governance/sign_off_proposal.json create mode 100644 crates/cmds-solana/node-definitions/governance/withdraw_governing_tokens.json create mode 100644 crates/cmds-solana/node-definitions/jupiter/swap.json create mode 100644 crates/cmds-solana/node-definitions/memo.json create mode 100644 crates/cmds-solana/node-definitions/metaboss/update_authority.ignore create mode 100644 crates/cmds-solana/node-definitions/mint_token.json create mode 100644 crates/cmds-solana/node-definitions/nft/approve_collection_authority.json create mode 100644 crates/cmds-solana/node-definitions/nft/approve_use_authority.json create mode 100644 crates/cmds-solana/node-definitions/nft/arweave_file_upload.json create mode 100644 crates/cmds-solana/node-definitions/nft/arweave_nft_upload.json create mode 100644 crates/cmds-solana/node-definitions/nft/auction_house_sell.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine/add_config_lines.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine/initialize.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine/initialize_candy_guard.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine/mint.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine/wrap.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine_core/add_config_lines_core.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_candy_machine_core.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_core_candy_guards.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine_core/mint_core.json create mode 100644 crates/cmds-solana/node-definitions/nft/candy_machine_core/wrap_core.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/collection_sample copy.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/collection_sample.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/fetch_assets.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/mpl_core_create_asset.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/mpl_core_create_collection.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/mpl_core_update_asset.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/mpl_core_update_plugin.json create mode 100644 crates/cmds-solana/node-definitions/nft/core/sample.json create mode 100644 crates/cmds-solana/node-definitions/nft/create_master_edition.json create mode 100644 crates/cmds-solana/node-definitions/nft/create_metadata_account.json create mode 100644 crates/cmds-solana/node-definitions/nft/get_left_uses.json create mode 100644 crates/cmds-solana/node-definitions/nft/set_token_standard.json create mode 100644 crates/cmds-solana/node-definitions/nft/sign_metadata.json create mode 100644 crates/cmds-solana/node-definitions/nft/update_metadata_account.json create mode 100644 crates/cmds-solana/node-definitions/nft/utilize.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/burn_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/create_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/delegate_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/mint_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/update_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/verify_collection_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/v1/verify_creator_v1.json create mode 100644 crates/cmds-solana/node-definitions/nft/verify_collection.json create mode 100644 crates/cmds-solana/node-definitions/pyth_price.json create mode 100644 crates/cmds-solana/node-definitions/record/initialize_record_with_seed.json create mode 100644 crates/cmds-solana/node-definitions/record/read_record.json create mode 100644 crates/cmds-solana/node-definitions/record/write_to_record.json create mode 100644 crates/cmds-solana/node-definitions/request_airdrop.json create mode 100644 crates/cmds-solana/node-definitions/spl_token/set_authority.json create mode 100644 crates/cmds-solana/node-definitions/spl_token_2022/set_authority.json create mode 100644 crates/cmds-solana/node-definitions/streamflow/create.json create mode 100644 crates/cmds-solana/node-definitions/streamflow/withdraw.json create mode 100644 crates/cmds-solana/node-definitions/transfer_sol.json create mode 100644 crates/cmds-solana/node-definitions/transfer_token.json create mode 100644 crates/cmds-solana/node-definitions/wallet.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/get_vaa.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/redeem_nft_on_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/transfer_nft_from_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_native.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped_meta.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_native.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_wrapped.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/parse_vaa.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/post_message.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/post_vaa.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/attest.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_native.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_transfer_wrapped.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/create_wrapped.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/attest_from_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/create_wrapped_on_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/redeem_on_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/transfer_from_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/initialize.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_native.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_wrapped.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/utils/get_foreign_asset_eth.json create mode 100644 crates/cmds-solana/node-definitions/wormhole/verify_signatures.json create mode 100644 crates/cmds-solana/node-definitions/xnft/create_install.json create mode 100644 crates/cmds-solana/node-definitions/xnft/create_permissioned_install.json create mode 100644 crates/cmds-solana/node-definitions/xnft/create_xnft.json create mode 100644 crates/cmds-solana/node-definitions/xnft/delete_install.json create mode 100644 crates/cmds-solana/node-definitions/xnft/grant_access.json create mode 100644 crates/cmds-solana/node-definitions/xnft/revoke_access.json create mode 100644 crates/cmds-solana/src/associated_token_account.rs create mode 100644 crates/cmds-solana/src/compression/burn.rs create mode 100644 crates/cmds-solana/src/compression/create_tree.rs create mode 100644 crates/cmds-solana/src/compression/metadata.json create mode 100644 crates/cmds-solana/src/compression/mint_to_collection_v1.rs create mode 100644 crates/cmds-solana/src/compression/mint_v1.rs create mode 100644 crates/cmds-solana/src/compression/mod.rs create mode 100644 crates/cmds-solana/src/compression/transfer.rs create mode 100644 crates/cmds-solana/src/compression/types/asset.rs create mode 100644 crates/cmds-solana/src/compression/types/mod.rs create mode 100644 crates/cmds-solana/src/compression/update.rs create mode 100644 crates/cmds-solana/src/create_mint_account.rs create mode 100644 crates/cmds-solana/src/create_token_account.rs create mode 100644 crates/cmds-solana/src/das.rs create mode 100644 crates/cmds-solana/src/error.rs create mode 100644 crates/cmds-solana/src/find_pda.rs create mode 100644 crates/cmds-solana/src/generate_keypair.rs create mode 100644 crates/cmds-solana/src/get_balance.rs create mode 100644 crates/cmds-solana/src/governance/add_required_signatory.rs create mode 100644 crates/cmds-solana/src/governance/add_signatory.rs create mode 100644 crates/cmds-solana/src/governance/cancel_proposal.rs create mode 100644 crates/cmds-solana/src/governance/cast_vote.rs create mode 100644 crates/cmds-solana/src/governance/complete_proposal.rs create mode 100644 crates/cmds-solana/src/governance/create_governance.rs create mode 100644 crates/cmds-solana/src/governance/create_native_treasury.rs create mode 100644 crates/cmds-solana/src/governance/create_proposal.rs create mode 100644 crates/cmds-solana/src/governance/create_realm.rs create mode 100644 crates/cmds-solana/src/governance/create_token_owner_record.rs create mode 100644 crates/cmds-solana/src/governance/deposit_governing_tokens.rs create mode 100644 crates/cmds-solana/src/governance/execute_transaction.rs create mode 100644 crates/cmds-solana/src/governance/finalize_vote.rs create mode 100644 crates/cmds-solana/src/governance/insert_transaction.rs create mode 100644 crates/cmds-solana/src/governance/mod.rs create mode 100644 crates/cmds-solana/src/governance/post_message.rs create mode 100644 crates/cmds-solana/src/governance/refund_proposal_deposit.rs create mode 100644 crates/cmds-solana/src/governance/relinquish_token_owner_record_locks.rs create mode 100644 crates/cmds-solana/src/governance/relinquish_vote.rs create mode 100644 crates/cmds-solana/src/governance/remove_required_signatory.rs create mode 100644 crates/cmds-solana/src/governance/remove_transaction.rs create mode 100644 crates/cmds-solana/src/governance/revoke_governing_tokens.rs create mode 100644 crates/cmds-solana/src/governance/set_governance_config.rs create mode 100644 crates/cmds-solana/src/governance/set_governance_delegate.rs create mode 100644 crates/cmds-solana/src/governance/set_realm_authority.rs create mode 100644 crates/cmds-solana/src/governance/set_realm_config.rs create mode 100644 crates/cmds-solana/src/governance/set_token_owner_record_locks.rs create mode 100644 crates/cmds-solana/src/governance/sign_off_proposal.rs create mode 100644 crates/cmds-solana/src/governance/withdraw_governing_tokens.rs create mode 100644 crates/cmds-solana/src/jupiter/mod.rs create mode 100644 crates/cmds-solana/src/jupiter/swap.rs create mode 100644 crates/cmds-solana/src/lib.rs create mode 100644 crates/cmds-solana/src/memo.rs create mode 100644 crates/cmds-solana/src/mint_token.rs create mode 100644 crates/cmds-solana/src/nft/approve_collection_authority.rs create mode 100644 crates/cmds-solana/src/nft/approve_use_authority.rs create mode 100644 crates/cmds-solana/src/nft/arweave_file_upload.rs create mode 100644 crates/cmds-solana/src/nft/arweave_nft_upload.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/add_config_lines_core.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_guards.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_machine.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/mint_core.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/mod.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_core/wrap_core.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/add_config_lines.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/guards.json create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/initialize.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/initialize_candy_guard.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/mint.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/mod.rs create mode 100644 crates/cmds-solana/src/nft/candy_machine_v3/wrap.rs create mode 100644 crates/cmds-solana/src/nft/core/create_asset.rs create mode 100644 crates/cmds-solana/src/nft/core/create_collection.rs create mode 100644 crates/cmds-solana/src/nft/core/fetch_assets.rs create mode 100644 crates/cmds-solana/src/nft/core/mod.rs create mode 100644 crates/cmds-solana/src/nft/core/update_asset.rs create mode 100644 crates/cmds-solana/src/nft/core/update_plugin.rs create mode 100644 crates/cmds-solana/src/nft/create_master_edition.rs create mode 100644 crates/cmds-solana/src/nft/create_metadata_account.rs create mode 100644 crates/cmds-solana/src/nft/get_left_uses.rs create mode 100644 crates/cmds-solana/src/nft/mod.rs create mode 100644 crates/cmds-solana/src/nft/set_token_standard.rs create mode 100644 crates/cmds-solana/src/nft/sign_metadata.rs create mode 100644 crates/cmds-solana/src/nft/update_metadata_account.rs create mode 100644 crates/cmds-solana/src/nft/v1/burn_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/create_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/delegate_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/mint_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/mod.rs create mode 100644 crates/cmds-solana/src/nft/v1/update_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/verify_collection_v1.rs create mode 100644 crates/cmds-solana/src/nft/v1/verify_creator_v1.rs create mode 100644 crates/cmds-solana/src/nft/verify_collection.rs create mode 100644 crates/cmds-solana/src/pyth_price.rs create mode 100644 crates/cmds-solana/src/record/initialize_record_with_seed.rs create mode 100644 crates/cmds-solana/src/record/mod.rs create mode 100644 crates/cmds-solana/src/record/read_record.rs create mode 100644 crates/cmds-solana/src/record/write_to_record.rs create mode 100644 crates/cmds-solana/src/request_airdrop.rs create mode 100644 crates/cmds-solana/src/spl/mod.rs create mode 100644 crates/cmds-solana/src/spl/set_authority.rs create mode 100644 crates/cmds-solana/src/spl_token_2022/mod.rs create mode 100644 crates/cmds-solana/src/spl_token_2022/set_authority.rs create mode 100644 crates/cmds-solana/src/streamflow/create.rs create mode 100644 crates/cmds-solana/src/streamflow/mod.rs create mode 100644 crates/cmds-solana/src/streamflow/withdraw.rs create mode 100644 crates/cmds-solana/src/transfer_sol.rs create mode 100644 crates/cmds-solana/src/transfer_token.rs create mode 100644 crates/cmds-solana/src/utils.rs create mode 100644 crates/cmds-solana/src/utils/bundlr_signer.rs create mode 100644 crates/cmds-solana/src/wallet.rs create mode 100644 crates/cmds-solana/src/wormhole/get_vaa.rs create mode 100644 crates/cmds-solana/src/wormhole/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/complete_native.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped_meta.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/eth/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/eth/redeem_nft_on_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/eth/transfer_nft_from_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/transfer_native.rs create mode 100644 crates/cmds-solana/src/wormhole/nft_bridge/transfer_wrapped.rs create mode 100644 crates/cmds-solana/src/wormhole/parse_vaa.rs create mode 100644 crates/cmds-solana/src/wormhole/post_message.rs create mode 100644 crates/cmds-solana/src/wormhole/post_vaa.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/attest.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/complete_native.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/complete_transfer_wrapped.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/create_wrapped.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/eth/attest_from_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/eth/create_wrapped_on_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/eth/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/eth/redeem_on_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/eth/transfer_from_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/initialize.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/transfer_native.rs create mode 100644 crates/cmds-solana/src/wormhole/token_bridge/transfer_wrapped.rs create mode 100644 crates/cmds-solana/src/wormhole/utils/get_foreign_asset_eth.rs create mode 100644 crates/cmds-solana/src/wormhole/utils/mod.rs create mode 100644 crates/cmds-solana/src/wormhole/verify_signatures.rs create mode 100644 crates/cmds-std/Cargo.toml create mode 100644 crates/cmds-std/benches/postgrest.rs create mode 100644 crates/cmds-std/node-definitions/collect.json create mode 100644 crates/cmds-std/node-definitions/const.json create mode 100644 crates/cmds-std/node-definitions/display/chart.json create mode 100644 crates/cmds-std/node-definitions/display/json_viewer.json create mode 100644 crates/cmds-std/node-definitions/display/table.json create mode 100644 crates/cmds-std/node-definitions/flow_input.json create mode 100644 crates/cmds-std/node-definitions/flow_output.json create mode 100644 crates/cmds-std/node-definitions/flow_run_info.json create mode 100644 crates/cmds-std/node-definitions/foreach.json create mode 100644 crates/cmds-std/node-definitions/http.json create mode 100644 crates/cmds-std/node-definitions/interflow.json create mode 100644 crates/cmds-std/node-definitions/interflow_instructions.json create mode 100644 crates/cmds-std/node-definitions/json/json_get_field.json create mode 100644 crates/cmds-std/node-definitions/json_extract.json create mode 100644 crates/cmds-std/node-definitions/json_insert.json create mode 100644 crates/cmds-std/node-definitions/kvstore/create_store.json create mode 100644 crates/cmds-std/node-definitions/kvstore/delete_store.json create mode 100644 crates/cmds-std/node-definitions/kvstore/kv_explorer.json create mode 100644 crates/cmds-std/node-definitions/kvstore/read_item.json create mode 100644 crates/cmds-std/node-definitions/kvstore/write_item.json create mode 100644 crates/cmds-std/node-definitions/note.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_eq.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_insert.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_is.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_limit.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_match.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_neq.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_not.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_order.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_select.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_update.json create mode 100644 crates/cmds-std/node-definitions/postgrest/builder_upsert.json create mode 100644 crates/cmds-std/node-definitions/postgrest/execute_query.json create mode 100644 crates/cmds-std/node-definitions/postgrest/new_query.json create mode 100644 crates/cmds-std/node-definitions/postgrest/new_rpc.json create mode 100644 crates/cmds-std/node-definitions/print.json create mode 100644 crates/cmds-std/node-definitions/std/expression.json create mode 100644 crates/cmds-std/node-definitions/std/math_operation.json create mode 100644 crates/cmds-std/node-definitions/std/range.json create mode 100644 crates/cmds-std/node-definitions/std/to_bytes.json create mode 100644 crates/cmds-std/node-definitions/std/to_string.json create mode 100644 crates/cmds-std/node-definitions/std/to_vec.json create mode 100644 crates/cmds-std/node-definitions/storage/create_signed_url.json create mode 100644 crates/cmds-std/node-definitions/storage/delete.json create mode 100644 crates/cmds-std/node-definitions/storage/download.json create mode 100644 crates/cmds-std/node-definitions/storage/file_explorer.json create mode 100644 crates/cmds-std/node-definitions/storage/get_file_metadata.json create mode 100644 crates/cmds-std/node-definitions/storage/get_public_url.json create mode 100644 crates/cmds-std/node-definitions/storage/list.json create mode 100644 crates/cmds-std/node-definitions/storage/upload.json create mode 100644 crates/cmds-std/node-definitions/supabase/supabase.json create mode 100644 crates/cmds-std/node-definitions/wait.json create mode 100644 crates/cmds-std/src/const_cmd.rs create mode 100644 crates/cmds-std/src/error.rs create mode 100644 crates/cmds-std/src/flow_run_info.rs create mode 100644 crates/cmds-std/src/http_request.rs create mode 100644 crates/cmds-std/src/json_extract.rs create mode 100644 crates/cmds-std/src/json_insert.rs create mode 100644 crates/cmds-std/src/kvstore/create_store.rs create mode 100644 crates/cmds-std/src/kvstore/delete_store.rs create mode 100644 crates/cmds-std/src/kvstore/explorer.rs create mode 100644 crates/cmds-std/src/kvstore/mod.rs create mode 100644 crates/cmds-std/src/kvstore/read_item.rs create mode 100644 crates/cmds-std/src/kvstore/write_item.rs create mode 100644 crates/cmds-std/src/lib.rs create mode 100644 crates/cmds-std/src/note.rs create mode 100644 crates/cmds-std/src/postgrest/builder_eq.rs create mode 100644 crates/cmds-std/src/postgrest/builder_insert.rs create mode 100644 crates/cmds-std/src/postgrest/builder_is.rs create mode 100644 crates/cmds-std/src/postgrest/builder_limit.rs create mode 100644 crates/cmds-std/src/postgrest/builder_match.rs create mode 100644 crates/cmds-std/src/postgrest/builder_neq.rs create mode 100644 crates/cmds-std/src/postgrest/builder_not.rs create mode 100644 crates/cmds-std/src/postgrest/builder_order.rs create mode 100644 crates/cmds-std/src/postgrest/builder_select.rs create mode 100644 crates/cmds-std/src/postgrest/builder_update.rs create mode 100644 crates/cmds-std/src/postgrest/builder_upsert.rs create mode 100644 crates/cmds-std/src/postgrest/execute_query.rs create mode 100644 crates/cmds-std/src/postgrest/mod.rs create mode 100644 crates/cmds-std/src/postgrest/new_query.rs create mode 100644 crates/cmds-std/src/postgrest/new_rpc.rs create mode 100644 crates/cmds-std/src/print_cmd.rs create mode 100644 crates/cmds-std/src/std/json_get_field.rs create mode 100644 crates/cmds-std/src/std/mod.rs create mode 100644 crates/cmds-std/src/std/range.rs create mode 100644 crates/cmds-std/src/std/to_bytes.rs create mode 100644 crates/cmds-std/src/std/to_string.rs create mode 100644 crates/cmds-std/src/std/to_vec.rs create mode 100644 crates/cmds-std/src/storage/create_signed_url.rs create mode 100644 crates/cmds-std/src/storage/delete.rs create mode 100644 crates/cmds-std/src/storage/download.rs create mode 100644 crates/cmds-std/src/storage/explorer.rs create mode 100644 crates/cmds-std/src/storage/get_file_metadata.rs create mode 100644 crates/cmds-std/src/storage/get_public_url.rs create mode 100644 crates/cmds-std/src/storage/list.rs create mode 100644 crates/cmds-std/src/storage/mod.rs create mode 100644 crates/cmds-std/src/storage/upload.rs create mode 100644 crates/cmds-std/src/supabase/mod.rs create mode 100644 crates/cmds-std/src/wait_cmd.rs create mode 100644 crates/command-rpc/Cargo.toml create mode 100644 crates/command-rpc/src/client.rs create mode 100644 crates/command-rpc/src/lib.rs create mode 100644 crates/command-rpc/src/server.rs create mode 100644 crates/db/Cargo.toml create mode 100644 crates/db/src/apikey.rs create mode 100644 crates/db/src/config.rs create mode 100644 crates/db/src/connection/admin.rs create mode 100644 crates/db/src/connection/conn_impl.rs create mode 100644 crates/db/src/connection/csv_export.rs create mode 100644 crates/db/src/connection/mod.rs create mode 100644 crates/db/src/connection/proxied_user_conn.rs create mode 100644 crates/db/src/error.rs create mode 100644 crates/db/src/lib.rs create mode 100644 crates/db/src/local_storage/mod.rs create mode 100644 crates/db/src/pool.rs create mode 100644 crates/db/src/wasm_storage.rs create mode 100644 crates/flow-server/Cargo.toml create mode 100644 crates/flow-server/Dockerfile create mode 100644 crates/flow-server/benches/crypto.rs create mode 100755 crates/flow-server/entrypoint.bash create mode 100644 crates/flow-server/src/api/apikey_info.rs create mode 100644 crates/flow-server/src/api/auth_proxy.rs create mode 100644 crates/flow-server/src/api/claim_token.rs create mode 100644 crates/flow-server/src/api/clone_flow.rs create mode 100644 crates/flow-server/src/api/confirm_auth.rs create mode 100644 crates/flow-server/src/api/create_apikey.rs create mode 100644 crates/flow-server/src/api/data_export.rs create mode 100644 crates/flow-server/src/api/data_import.rs create mode 100644 crates/flow-server/src/api/db_push_logs.rs create mode 100644 crates/flow-server/src/api/db_rpc.rs create mode 100644 crates/flow-server/src/api/delete_apikey.rs create mode 100644 crates/flow-server/src/api/deploy_flow.rs create mode 100644 crates/flow-server/src/api/flow_run/mod.rs create mode 100644 crates/flow-server/src/api/get_flow_output.rs create mode 100644 crates/flow-server/src/api/get_info.rs create mode 100644 crates/flow-server/src/api/get_signature_request.rs create mode 100644 crates/flow-server/src/api/init_auth.rs create mode 100644 crates/flow-server/src/api/kvstore/create_store.rs create mode 100644 crates/flow-server/src/api/kvstore/delete_item.rs create mode 100644 crates/flow-server/src/api/kvstore/delete_store.rs create mode 100644 crates/flow-server/src/api/kvstore/mod.rs create mode 100644 crates/flow-server/src/api/kvstore/read_item.rs create mode 100644 crates/flow-server/src/api/kvstore/write_item.rs create mode 100644 crates/flow-server/src/api/mod.rs create mode 100644 crates/flow-server/src/api/start_deployment.rs create mode 100644 crates/flow-server/src/api/start_flow.rs create mode 100644 crates/flow-server/src/api/start_flow_shared.rs create mode 100644 crates/flow-server/src/api/start_flow_unverified.rs create mode 100644 crates/flow-server/src/api/stop_flow.rs create mode 100644 crates/flow-server/src/api/submit_signature.rs create mode 100644 crates/flow-server/src/api/upsert_wallet.rs create mode 100644 crates/flow-server/src/api/ws_auth_proxy.rs create mode 100644 crates/flow-server/src/db_worker/flow_run_worker.rs create mode 100644 crates/flow-server/src/db_worker/messages.rs create mode 100644 crates/flow-server/src/db_worker/mod.rs create mode 100644 crates/flow-server/src/db_worker/signer.rs create mode 100644 crates/flow-server/src/db_worker/token_worker.rs create mode 100644 crates/flow-server/src/db_worker/user_worker.rs create mode 100644 crates/flow-server/src/error.rs create mode 100644 crates/flow-server/src/flow_logs.rs create mode 100644 crates/flow-server/src/lib.rs create mode 100644 crates/flow-server/src/main.rs create mode 100644 crates/flow-server/src/middleware/auth.rs create mode 100644 crates/flow-server/src/middleware/auth_v1.rs create mode 100644 crates/flow-server/src/middleware/mod.rs create mode 100644 crates/flow-server/src/middleware/req_fn.rs create mode 100644 crates/flow-server/src/user.rs create mode 100644 crates/flow-server/src/ws.rs create mode 100644 crates/flow/.gitignore create mode 100644 crates/flow/Cargo.toml create mode 100644 crates/flow/src/command/collect.rs create mode 100644 crates/flow/src/command/flow_input.rs create mode 100644 crates/flow/src/command/flow_output.rs create mode 100644 crates/flow/src/command/foreach.rs create mode 100644 crates/flow/src/command/interflow.rs create mode 100644 crates/flow/src/command/interflow_instructions.rs create mode 100644 crates/flow/src/command/mod.rs create mode 100644 crates/flow/src/command/rhai.rs create mode 100644 crates/flow/src/command/wasm.rs create mode 100644 crates/flow/src/context.rs create mode 100644 crates/flow/src/error.rs create mode 100644 crates/flow/src/flow_graph.rs create mode 100644 crates/flow/src/flow_registry.rs create mode 100644 crates/flow/src/flow_run_events.rs create mode 100644 crates/flow/src/flow_set.rs create mode 100644 crates/flow/src/lib.rs create mode 100644 crates/flow/test_files/2_foreach.json create mode 100644 crates/flow/test_files/deno_instructions/deno.json create mode 100644 crates/flow/test_files/deno_instructions/deno.lock create mode 100644 crates/flow/test_files/deno_instructions/node-definition.json create mode 100644 crates/flow/test_files/deno_instructions/transfer_sol.ts create mode 100644 crates/flow/test_files/deno_sig/deno.json create mode 100644 crates/flow/test_files/deno_sig/deno.lock create mode 100644 crates/flow/test_files/deno_sig/node-definition.json create mode 100644 crates/flow/test_files/deno_sig/transfer_sol.ts create mode 100644 crates/flow/test_files/nft.json create mode 100644 crates/flow/test_files/uneven_loop.json create mode 100644 crates/integration-tests/Cargo.toml create mode 100644 crates/integration-tests/src/main.rs create mode 100644 crates/pdg-common/Cargo.toml create mode 100644 crates/pdg-common/src/lib.rs create mode 100644 crates/pdg-common/src/nft_metadata/generate.rs create mode 100644 crates/pdg-common/src/nft_metadata/metaplex.rs create mode 100644 crates/pdg-common/src/nft_metadata/mod.rs create mode 100644 crates/pdg-common/src/nft_metadata/pdg.rs create mode 100644 crates/pdg-common/src/nft_metadata/tests/123.json create mode 100644 crates/pdg-common/src/nft_metadata/tests/postman request.json create mode 100644 crates/rhai-script/Cargo.toml create mode 100644 crates/rhai-script/node-definitions/rhai_script_0x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_0x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_0x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_0x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_0x5.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_1x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_1x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_1x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_1x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_1x5.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_2x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_2x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_2x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_2x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_2x5.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_3x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_3x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_3x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_3x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_3x5.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_4x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_4x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_4x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_4x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_4x5.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_5x1.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_5x2.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_5x3.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_5x4.json create mode 100644 crates/rhai-script/node-definitions/rhai_script_5x5.json create mode 100644 crates/rhai-script/src/bin/gen.rs create mode 100644 crates/rhai-script/src/convert.rs create mode 100644 crates/rhai-script/src/lib.rs create mode 100644 crates/space-wasm/.gitignore create mode 100644 crates/space-wasm/Cargo.toml create mode 100644 crates/space-wasm/src/ffi.rs create mode 100644 crates/space-wasm/src/lib.rs create mode 100644 crates/space-wasm/src/tests.rs create mode 100644 crates/space-wasm/tests/automatic/.cargo/config.toml create mode 100644 crates/space-wasm/tests/automatic/Cargo.lock create mode 100644 crates/space-wasm/tests/automatic/Cargo.toml create mode 100644 crates/space-wasm/tests/automatic/src/lib.rs create mode 100644 crates/space-wasm/tests/env/.cargo/config.toml create mode 100644 crates/space-wasm/tests/env/Cargo.lock create mode 100644 crates/space-wasm/tests/env/Cargo.toml create mode 100644 crates/space-wasm/tests/env/src/lib.rs create mode 100644 crates/space-wasm/tests/float/.cargo/config.toml create mode 100644 crates/space-wasm/tests/float/Cargo.lock create mode 100644 crates/space-wasm/tests/float/Cargo.toml create mode 100644 crates/space-wasm/tests/float/src/lib.rs create mode 100644 crates/space-wasm/tests/http/.cargo/config.toml create mode 100644 crates/space-wasm/tests/http/Cargo.lock create mode 100644 crates/space-wasm/tests/http/Cargo.toml create mode 100644 crates/space-wasm/tests/http/src/lib.rs create mode 100644 crates/space-wasm/tests/manual/.cargo/config.toml create mode 100644 crates/space-wasm/tests/manual/Cargo.lock create mode 100644 crates/space-wasm/tests/manual/Cargo.toml create mode 100644 crates/space-wasm/tests/manual/src/lib.rs create mode 100644 crates/space-wasm/tests/number/.cargo/config.toml create mode 100644 crates/space-wasm/tests/number/Cargo.lock create mode 100644 crates/space-wasm/tests/number/Cargo.toml create mode 100644 crates/space-wasm/tests/number/src/lib.rs create mode 100644 crates/space-wasm/tests/simple/.cargo/config.toml create mode 100644 crates/space-wasm/tests/simple/Cargo.lock create mode 100644 crates/space-wasm/tests/simple/Cargo.toml create mode 100644 crates/space-wasm/tests/simple/src/lib.rs create mode 100644 crates/srpc/Cargo.toml create mode 100644 crates/srpc/benches/srpc_bench.rs create mode 100644 crates/srpc/src/lib.rs create mode 100644 crates/utils/Cargo.toml create mode 100644 crates/utils/src/actix_service.rs create mode 100644 crates/utils/src/address_book.rs create mode 100644 crates/utils/src/lib.rs create mode 100644 crates/utils/src/serde_base64.rs create mode 100644 crates/utils/src/serde_bs58.rs create mode 100644 docker/.gitignore create mode 100644 docker/README.md create mode 100644 docker/deno.json create mode 100644 docker/deno.lock create mode 100644 docker/docker-compose.yml create mode 100644 docker/env.example create mode 100644 docker/flow-server-config.toml create mode 100755 docker/gen-secrets.ts create mode 100755 docker/import-data.ts create mode 100644 docker/supabase/migrations/20240514130738_init.sql create mode 100644 docker/supabase/migrations/20240517061121_grant.sql create mode 100644 docker/supabase/migrations/20240518143018_auth_trigger.sql create mode 100644 docker/supabase/migrations/20240524150823_grant_sequence.sql create mode 100644 docker/supabase/migrations/20240525104546_kvstore_fk.sql create mode 100644 docker/supabase/migrations/20240905183752_encrypt.sql create mode 100644 docker/supabase/migrations/20240906100833_grant_wallet_update.sql create mode 100644 docker/supabase/migrations/20240907035451_update_wallets_table.sql create mode 100644 docker/supabase/migrations/20240930062601_remove_keypair.sql create mode 100644 docker/supabase/migrations/20241008071914_update_nodes.sql create mode 100644 docker/supabase/migrations/20241008143527_nodes_update_policy.sql create mode 100644 docker/supabase/migrations/20241008144128_with_check.sql create mode 100644 docker/supabase/migrations/20241030051429_check_native_nodes.sql create mode 100644 docker/supabase/migrations/20241202120303_update_flows_table.sql create mode 100644 docker/supabase/migrations/20241214133549_flow_deployment.sql create mode 100644 docker/supabase/migrations/20241230141331_wallet_purpose.sql create mode 100644 docker/supabase/migrations/20241230142807_flow_gg_marketplace.sql create mode 100644 docker/tests/login.ts create mode 100644 docker/volumes/api/kong.yml create mode 100755 docker/volumes/db/init/data.sql create mode 100644 docker/volumes/db/jwt.sql create mode 100644 docker/volumes/db/logs.sql create mode 100644 docker/volumes/db/realtime.sql create mode 100644 docker/volumes/db/roles.sql create mode 100644 docker/volumes/db/webhooks.sql create mode 100644 docker/volumes/functions/main/index.ts create mode 100644 docker/volumes/logs/vector.yml create mode 100644 guest.toml create mode 100644 lib/client/Cargo.toml create mode 100644 lib/client/src/lib.rs create mode 100644 lib/client/src/types.rs create mode 100644 lib/flow-lib/.gitignore create mode 100644 lib/flow-lib/Cargo.toml create mode 100644 lib/flow-lib/src/command/builder.rs create mode 100644 lib/flow-lib/src/command/mod.rs create mode 100644 lib/flow-lib/src/config/client.rs create mode 100644 lib/flow-lib/src/config/mod.rs create mode 100644 lib/flow-lib/src/config/node.rs create mode 100644 lib/flow-lib/src/context.rs create mode 100644 lib/flow-lib/src/lib.rs create mode 100644 lib/flow-lib/src/solana.rs create mode 100644 lib/flow-lib/src/solana/utils.rs create mode 100644 lib/flow-lib/src/solana/watcher.rs create mode 100644 lib/flow-lib/src/utils/extensions.rs create mode 100644 lib/flow-lib/src/utils/mod.rs create mode 100644 lib/flow-lib/src/utils/tower_client.rs create mode 100644 lib/flow-value/Cargo.toml create mode 100644 lib/flow-value/src/crud.rs create mode 100644 lib/flow-value/src/crud/path.rs create mode 100644 lib/flow-value/src/de.rs create mode 100644 lib/flow-value/src/de/const_bytes.rs create mode 100644 lib/flow-value/src/de/de_enum.rs create mode 100644 lib/flow-value/src/de/de_struct.rs create mode 100644 lib/flow-value/src/de/text_repr.rs create mode 100644 lib/flow-value/src/decimal.rs create mode 100644 lib/flow-value/src/json_repr/iter_ser.rs create mode 100644 lib/flow-value/src/keypair.rs create mode 100644 lib/flow-value/src/lib.rs create mode 100644 lib/flow-value/src/macros.rs create mode 100644 lib/flow-value/src/pubkey.rs create mode 100644 lib/flow-value/src/ser.rs create mode 100644 lib/flow-value/src/ser/iter_ser.rs create mode 100644 lib/flow-value/src/ser/map_key.rs create mode 100644 lib/flow-value/src/ser/maps.rs create mode 100644 lib/flow-value/src/ser/seq.rs create mode 100644 lib/flow-value/src/ser/tagged_bytes.rs create mode 100644 lib/flow-value/src/ser/text_repr.rs create mode 100644 lib/flow-value/src/signature.rs create mode 100644 lib/flow-value/src/tests/ser.json create mode 100644 lib/flow-value/src/value_type.rs create mode 100644 lib/flow-value/src/with.rs create mode 100644 lib/space-lib/Cargo.toml create mode 100644 lib/space-lib/README.md create mode 100644 lib/space-lib/src/common.rs create mode 100644 lib/space-lib/src/error.rs create mode 100644 lib/space-lib/src/ffi.rs create mode 100644 lib/space-lib/src/http.rs create mode 100644 lib/space-lib/src/lib.rs create mode 100644 lib/space-macro/Cargo.toml create mode 100644 lib/space-macro/src/lib.rs create mode 100644 lib/space-operator-cli/CHANGELOG.md create mode 100644 lib/space-operator-cli/Cargo.lock create mode 100644 lib/space-operator-cli/Cargo.toml create mode 100644 lib/space-operator-cli/README.md create mode 100644 lib/space-operator-cli/src/main.rs create mode 100644 lib/space-operator-cli/src/schema.rs create mode 100644 lib/spo-helius/Cargo.toml create mode 100644 lib/spo-helius/src/lib.rs create mode 100644 node-definition.json create mode 100644 schema/node-definition.schema.json create mode 100644 schema/value.schema.json create mode 100755 scripts/build_images.bash create mode 100755 scripts/build_wasm_tests.bash create mode 100755 scripts/ecr-push.bash create mode 100644 test_files/HTTP Request.json create mode 100644 test_files/const_form_data.json create mode 100644 test_files/file_upload.json create mode 100644 test_files/foreach.json create mode 100644 test_files/generate_keypair.json create mode 100644 test_files/interflow_simple.json create mode 100644 test_files/subflow.json diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..9de5b046 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[profile.dev] +debug = 0 +opt-level = 1 diff --git a/.github/assert_cargo_lock_unchanged.bash b/.github/assert_cargo_lock_unchanged.bash new file mode 100755 index 00000000..5f733aec --- /dev/null +++ b/.github/assert_cargo_lock_unchanged.bash @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +[ -z "$(git diff -- Cargo.lock)" ] diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml new file mode 100644 index 00000000..13a0ef35 --- /dev/null +++ b/.github/workflows/deps.yml @@ -0,0 +1,26 @@ +name: Rust deps + +on: + pull_request: + branches: ["main"] + push: + branches: ["main"] + +jobs: + rust-deps: + if: ${{ ! contains(github.event.pull_request.labels.*.name, 'no-test') }} + name: Rust deps + runs-on: self-hosted + steps: + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: default + override: true + - uses: actions/checkout@v4 + with: + submodules: "recursive" + - uses: Swatinem/rust-cache@v2 + - run: rm Cargo.lock + - name: Build + run: cargo check --tests diff --git a/.github/workflows/docker-main.yaml b/.github/workflows/docker-main.yaml new file mode 100644 index 00000000..ed826dee --- /dev/null +++ b/.github/workflows/docker-main.yaml @@ -0,0 +1,28 @@ +name: Docker - main branch + +on: + push: + branches: + - "main" + +jobs: + build: + name: Build docker image + runs-on: self-hosted + env: + BRANCH: ${{ github.head_ref || github.ref_name }} + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Build + run: ./scripts/build_images.bash + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.ACCESS_KEY }} + - name: Push + run: ./scripts/ecr-push.bash login + - name: Clean up + run: podman image prune -f diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..4abc4d4a --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,27 @@ +name: Docker + +on: + pull_request: + +jobs: + build: + if: contains(github.event.pull_request.labels.*.name, 'docker') + name: Build docker image + runs-on: self-hosted + env: + BRANCH: ${{ github.head_ref || github.ref_name }} + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Build + run: ./scripts/build_images.bash + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.ACCESS_KEY }} + - name: Push + run: ./scripts/ecr-push.bash login + - name: Clean up + run: podman image prune -f diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..4fbd6ea5 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,52 @@ +name: Rust + +on: + pull_request: + branches: ["main"] + push: + branches: ["main"] + +jobs: + unit-test: + if: ${{ ! contains(github.event.pull_request.labels.*.name, 'no-test') }} + name: Unit test + runs-on: self-hosted + permissions: + checks: write + env: + SOLANA_DEVNET_URL: ${{ secrets.SOLANA_DEVNET_URL }} + steps: + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: default + override: true + - uses: actions/checkout@v4 + with: + submodules: "recursive" + - uses: Swatinem/rust-cache@v2 + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --quiet --tests -- + -D clippy::dbg_macro + -D clippy::print_stdout + -D clippy::print_stderr + -A clippy::too_many_arguments + - name: Assert Cargo.lock unchanged + run: .github/assert_cargo_lock_unchanged.bash + - name: Install deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - name: Build Integration Tests + run: cargo build --bin integration-tests --quiet + - name: Run Integration Tests + env: + APIKEY: ${{ secrets.INTEGRATION_TESTS_APIKEY }} + KEYPAIR: ${{ secrets.INTEGRATION_TESTS_KEYPAIR }} + run: cargo run --bin integration-tests --quiet + - name: Build tests + run: cargo test --quiet --no-run + - name: Run tests + run: cargo test -- --skip need_key_ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b1fb7f27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target +.vscode +_data +guest_local_storage +local_storage +config.toml +.env +.ipynb_checkpoints diff --git a/@space-operator/client/LICENSE b/@space-operator/client/LICENSE new file mode 100644 index 00000000..0e583f09 --- /dev/null +++ b/@space-operator/client/LICENSE @@ -0,0 +1,668 @@ +Open source license: AGPLv3 + +For commercial projects and to keep your source code proprietary, +please get a license at www.spaceoperator.com + +////////////////////////////////////////////////////////////////////// + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/@space-operator/client/deno.json b/@space-operator/client/deno.json new file mode 100644 index 00000000..681424fd --- /dev/null +++ b/@space-operator/client/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@space-operator/client", + "version": "0.10.0", + "exports": "./src/mod.ts" +} \ No newline at end of file diff --git a/@space-operator/client/deno.lock b/@space-operator/client/deno.lock new file mode 100644 index 00000000..79604ad5 --- /dev/null +++ b/@space-operator/client/deno.lock @@ -0,0 +1,403 @@ +{ + "version": "4", + "specifiers": { + "jsr:@space-operator/flow-lib@0.10.0": "0.10.0", + "jsr:@std/bytes@0.221": "0.221.0", + "jsr:@std/dotenv@*": "0.225.2", + "jsr:@std/encoding@0.221": "0.221.0", + "jsr:@std/msgpack@0.221.0": "0.221.0", + "jsr:@supabase/supabase-js@2": "2.48.1", + "npm:@solana/web3.js@1": "1.98.0", + "npm:@solana/web3.js@^1.91.4": "1.98.0", + "npm:@supabase/auth-js@2": "2.68.0", + "npm:@supabase/auth-js@2.67.3": "2.67.3", + "npm:@supabase/functions-js@2.4.4": "2.4.4", + "npm:@supabase/node-fetch@2.6.15": "2.6.15", + "npm:@supabase/postgrest-js@1.18.1": "1.18.1", + "npm:@supabase/realtime-js@2.11.2": "2.11.2", + "npm:@supabase/storage-js@2.7.1": "2.7.1", + "npm:@supabase/supabase-js@2": "2.48.1" + }, + "jsr": { + "@space-operator/flow-lib@0.10.0": { + "integrity": "d6f74303435982c9b70319bf9c75b419e1c15549c675468a75a1918b19a41fe1", + "dependencies": [ + "jsr:@std/encoding", + "jsr:@std/msgpack", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/dotenv@0.225.2": { + "integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@supabase/supabase-js@2.48.1": { + "integrity": "747e85c2a546efcc9f84b343b08e10e3818d468ae214506b59dc65a36fd2d702", + "dependencies": [ + "npm:@supabase/auth-js@2.67.3", + "npm:@supabase/functions-js", + "npm:@supabase/node-fetch", + "npm:@supabase/postgrest-js", + "npm:@supabase/realtime-js", + "npm:@supabase/storage-js" + ] + } + }, + "npm": { + "@babel/runtime@7.26.7": { + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "dependencies": [ + "regenerator-runtime" + ] + }, + "@noble/curves@1.8.1": { + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "dependencies": [ + "@noble/hashes" + ] + }, + "@noble/hashes@1.7.1": { + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==" + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": [ + "buffer" + ] + }, + "@solana/web3.js@1.98.0": { + "integrity": "sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==", + "dependencies": [ + "@babel/runtime", + "@noble/curves", + "@noble/hashes", + "@solana/buffer-layout", + "agentkeepalive", + "bigint-buffer", + "bn.js", + "borsh", + "bs58", + "buffer", + "fast-stable-stringify", + "jayson", + "node-fetch", + "rpc-websockets", + "superstruct" + ] + }, + "@supabase/auth-js@2.67.3": { + "integrity": "sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==", + "dependencies": [ + "@supabase/node-fetch" + ] + }, + "@supabase/auth-js@2.68.0": { + "integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==", + "dependencies": [ + "@supabase/node-fetch" + ] + }, + "@supabase/functions-js@2.4.4": { + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "dependencies": [ + "@supabase/node-fetch" + ] + }, + "@supabase/node-fetch@2.6.15": { + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": [ + "whatwg-url" + ] + }, + "@supabase/postgrest-js@1.18.1": { + "integrity": "sha512-dWDnoC0MoDHKhaEOrsEKTadWQcBNknZVQcSgNE/Q2wXh05mhCL1ut/jthRUrSbYcqIw/CEjhaeIPp7dLarT0bg==", + "dependencies": [ + "@supabase/node-fetch" + ] + }, + "@supabase/realtime-js@2.11.2": { + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "dependencies": [ + "@supabase/node-fetch", + "@types/phoenix", + "@types/ws@8.5.14", + "ws@8.18.0_bufferutil@4.0.9_utf-8-validate@5.0.10" + ] + }, + "@supabase/storage-js@2.7.1": { + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "dependencies": [ + "@supabase/node-fetch" + ] + }, + "@supabase/supabase-js@2.48.1": { + "integrity": "sha512-VMD+CYk/KxfwGbI4fqwSUVA7CLr1izXpqfFerhnYPSi6LEKD8GoR4kuO5Cc8a+N43LnfSQwLJu4kVm2e4etEmA==", + "dependencies": [ + "@supabase/auth-js@2.67.3", + "@supabase/functions-js", + "@supabase/node-fetch", + "@supabase/postgrest-js", + "@supabase/realtime-js", + "@supabase/storage-js" + ] + }, + "@swc/helpers@0.5.15": { + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": [ + "tslib" + ] + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": [ + "@types/node@22.5.4" + ] + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "@types/phoenix@1.6.6": { + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "@types/uuid@8.3.4": { + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": [ + "@types/node@22.5.4" + ] + }, + "@types/ws@8.5.14": { + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dependencies": [ + "@types/node@22.5.4" + ] + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": [ + "jsonparse", + "through" + ] + }, + "agentkeepalive@4.6.0": { + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dependencies": [ + "humanize-ms" + ] + }, + "base-x@3.0.10": { + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "dependencies": [ + "safe-buffer" + ] + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": [ + "bindings" + ] + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": [ + "file-uri-to-path" + ] + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": [ + "bn.js", + "bs58", + "text-encoding-utf-8" + ] + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": [ + "base-x" + ] + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, + "bufferutil@4.0.9": { + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "dependencies": [ + "node-gyp-build" + ] + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==" + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": [ + "es6-promise" + ] + }, + "eventemitter3@5.0.1": { + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": [ + "ms" + ] + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "isomorphic-ws@4.0.1_ws@7.5.10": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": [ + "ws@7.5.10" + ] + }, + "jayson@4.1.3_ws@7.5.10": { + "integrity": "sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ==", + "dependencies": [ + "@types/connect", + "@types/node@12.20.55", + "@types/ws@7.4.7", + "JSONStream", + "commander", + "delay", + "es6-promisify", + "eyes", + "isomorphic-ws", + "json-stringify-safe", + "uuid", + "ws@7.5.10" + ] + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": [ + "whatwg-url" + ] + }, + "node-gyp-build@4.8.4": { + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "rpc-websockets@9.0.4_bufferutil@4.0.9_utf-8-validate@5.0.10": { + "integrity": "sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ==", + "dependencies": [ + "@swc/helpers", + "@types/uuid", + "@types/ws@8.5.14", + "buffer", + "bufferutil", + "eventemitter3", + "utf-8-validate", + "uuid", + "ws@8.18.0_bufferutil@4.0.9_utf-8-validate@5.0.10" + ] + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "superstruct@2.0.2": { + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==" + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": [ + "node-gyp-build" + ] + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + }, + "ws@7.5.10": { + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==" + }, + "ws@8.18.0_bufferutil@4.0.9_utf-8-validate@5.0.10": { + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dependencies": [ + "bufferutil", + "utf-8-validate" + ] + } + } +} diff --git a/@space-operator/client/notebook/Untitled.ipynb b/@space-operator/client/notebook/Untitled.ipynb new file mode 100644 index 00000000..9371fc16 --- /dev/null +++ b/@space-operator/client/notebook/Untitled.ipynb @@ -0,0 +1,157 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "b679f896-b963-4576-bf21-d3af8792f71d", + "metadata": {}, + "outputs": [], + "source": [ + "import * as lib from '../src/mod.ts';" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a35db6ea-232e-4de7-95df-33f656ff2562", + "metadata": {}, + "outputs": [], + "source": [ + "let innerBody = new lib.Value({\n", + " \"inputs\": {\n", + " \"M\": {\n", + " \"uri\": {\n", + " \"S\": \"https://j45eekk7guujcl6hxt4xxjkaceqhomvgkgnda6uz76hrmymj7zea.arweave.net/TzpCKV81KJEvx7z5e6VAESB3MqZRmjB6mf-PFmGJ_kg\"\n", + " },\n", + " \"supply\": {\n", + " \"D\": \"100000\"\n", + " },\n", + " \"decimals\": {\n", + " \"D\": \"9\"\n", + " },\n", + " \"fee_payer\": {\n", + " \"S\": \"GDLeqUvBoC4uUwdNTge5c2WrJPEs6XDTz91xGnLiFEoxQ8SeThsuYHhsK6jTMWbEFAQrHciwMmqK9H8QQsdsDJU\"\n", + " },\n", + " \"is_mutable\": {\n", + " \"B\": false\n", + " },\n", + " \"mint_authority\": {\n", + " \"S\": \"GDLeqUvBoC4uUwdNTge5c2WrJPEs6XDTz91xGnLiFEoxQ8SeThsuYHhsK6jTMWbEFAQrHciwMmqK9H8QQsdsDJU\"\n", + " }\n", + " }\n", + " }\n", + "});" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "d88438db-1501-4c8c-983e-354c26b0b888", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"M\": {\n", + " \"inputs\": {\n", + " \"M\": {\n", + " \"M\": {\n", + " \"M\": {\n", + " \"uri\": {\n", + " \"M\": {\n", + " \"S\": {\n", + " \"S\": \"https://j45eekk7guujcl6hxt4xxjkaceqhomvgkgnda6uz76hrmymj7zea.arweave.net/TzpCKV81KJEvx7z5e6VAESB3MqZRmjB6mf-PFmGJ_kg\"\n", + " }\n", + " }\n", + " },\n", + " \"supply\": {\n", + " \"M\": {\n", + " \"D\": {\n", + " \"S\": \"100000\"\n", + " }\n", + " }\n", + " },\n", + " \"decimals\": {\n", + " \"M\": {\n", + " \"D\": {\n", + " \"S\": \"9\"\n", + " }\n", + " }\n", + " },\n", + " \"fee_payer\": {\n", + " \"M\": {\n", + " \"S\": {\n", + " \"S\": \"GDLeqUvBoC4uUwdNTge5c2WrJPEs6XDTz91xGnLiFEoxQ8SeThsuYHhsK6jTMWbEFAQrHciwMmqK9H8QQsdsDJU\"\n", + " }\n", + " }\n", + " },\n", + " \"is_mutable\": {\n", + " \"M\": {\n", + " \"B\": {\n", + " \"B\": false\n", + " }\n", + " }\n", + " },\n", + " \"mint_authority\": {\n", + " \"M\": {\n", + " \"S\": {\n", + " \"S\": \"GDLeqUvBoC4uUwdNTge5c2WrJPEs6XDTz91xGnLiFEoxQ8SeThsuYHhsK6jTMWbEFAQrHciwMmqK9H8QQsdsDJU\"\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "console.log(JSON.stringify(innerBody, null, 2));" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "b9d3a449-e0ae-4f21-b50f-abbd37cb622c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Value { M: { S: Value { S: \u001b[32m\"100\"\u001b[39m } } }" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Deno", + "language": "typescript", + "name": "deno" + }, + "language_info": { + "codemirror_mode": "typescript", + "file_extension": ".ts", + "mimetype": "text/x.typescript", + "name": "typescript", + "nbconvert_exporter": "script", + "pygments_lexer": "typescript", + "version": "5.6.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/@space-operator/client/src/client.ts b/@space-operator/client/src/client.ts new file mode 100644 index 00000000..ea0848d8 --- /dev/null +++ b/@space-operator/client/src/client.ts @@ -0,0 +1,376 @@ +import { createClient, type SupabaseClient } from "jsr:@supabase/supabase-js@2"; +import { bs58, web3, Value, type IValue, Buffer } from "./deps.ts"; +import { + type ErrorBody, + type ISignatureRequest, + type StartFlowOutput, + type StartFlowParams, + type StartFlowSharedOutput, + type StartFlowSharedParams, + type StartFlowUnverifiedOutput, + type StartFlowUnverifiedParams, + type StopFlowOutput, + type StopFlowParams, + type SubmitSignatureOutput, + type SubmitSignatureParams, + type GetFlowOutputOutput, + type FlowId, + type FlowRunId, + type DeploymentId, + DeploymentSpecifier, + SignatureRequest, + type InitAuthOutput, + type ConfirmAuthOutput, + type StartDeploymentParams, + type StartDeploymentOutput, + type IDeploymentSpecifier, +} from "./mod.ts"; +import type { Database } from "./supabase.ts"; + +export type TokenProvider = string | (() => Promise); + +function header(key: string): string { + if (key.startsWith("b3-")) { + return "x-api-key"; + } else { + return "authorization"; + } +} + +async function getToken(token?: TokenProvider): Promise { + switch (typeof token) { + case "undefined": + throw new Error("no authentication token"); + case "string": + return token; + case "function": + return await token(); + default: + throw new Error("invalid token type"); + } +} + +export interface ClientOptions { + host?: string; + // Authorization token + token?: TokenProvider; + // Supabase Anon key + anonKey?: TokenProvider; + supabaseUrl?: string; +} + +const HOST = "https://dev-api.spaceoperator.com"; +const SUPABASE_URL = "https://base.spaceoperator.com"; + +function noop() {} + +async function parseResponse(resp: Response): Promise { + if (resp.status !== 200 && resp.status !== 201) { + let error; + if (resp.headers.get("content-type") === "application/json") { + const body = await resp.json(); + error = (body as ErrorBody).error ?? JSON.stringify(body); + } else { + error = await resp.text(); + } + if (error === undefined) { + error = "An error occurred"; + } + throw new Error(error); + } + return await resp.json(); +} + +export class Client { + host: string; + supabaseUrl: string; + token?: TokenProvider; + anonKey?: TokenProvider; + #supabase?: SupabaseClient; + private logger: Function = noop; + + constructor(options: ClientOptions = {}) { + this.host = options.host ?? HOST; + this.supabaseUrl = options.supabaseUrl ?? SUPABASE_URL; + this.token = options.token; + this.anonKey = options.anonKey; + } + + async supabase(token?: { + access_token: string; + refresh_token: string; + }): Promise> { + if (this.#supabase === undefined) { + this.#supabase = createClient( + this.supabaseUrl, + await getToken(this.anonKey) + ); + const session = await this.#supabase.auth.getSession(); + if (!session.data.session) { + if (token !== undefined) { + await this.#supabase.auth.setSession(token); + } else { + await this.#claimToken(); + } + } + } + return this.#supabase; + } + + async upsertWallet(body: any): Promise { + return await this.#sendJSONPost(`${this.host}/wallets/upsert`, body); + } + + setToken(token: string | (() => Promise)) { + this.token = token; + } + + public setLogger(logger: Function) { + this.logger = logger; + } + + async #setAuthHeader( + req: Request, + auth: boolean | TokenProvider = true + ): Promise { + switch (typeof auth) { + case "boolean": + if (auth === true) { + const token = await getToken(this.token); + req.headers.set(header(token), token); + } + break; + case "string": + req.headers.set(header(auth), auth); + break; + case "function": { + const token = await auth(); + req.headers.set(header(token), token); + break; + } + default: + throw new TypeError("unexpected type"); + } + return req; + } + + async #sendJSONGet( + url: string, + auth: boolean | TokenProvider = true + ): Promise { + let req = new Request(url); + req = await this.#setAuthHeader(req, auth); + + const resp = await fetch(req); + return await parseResponse(resp); + } + + async #sendJSONPost( + url: string, + body?: any, + auth: boolean | TokenProvider = true, + anonKey: boolean = false + ): Promise { + const reqBody = body !== undefined ? JSON.stringify(body) : undefined; + const headers: Record = + body !== undefined + ? { + "content-type": "application/json", + } + : {}; + let req = new Request(url, { + method: "POST", + body: reqBody, + headers, + }); + req = await this.#setAuthHeader(req, auth); + + if (anonKey === true) { + req.headers.set("apikey", await getToken(this.anonKey)); + } + + const resp = await fetch(req); + return await parseResponse(resp); + } + + async initAuth(pubkey: web3.PublicKey | string): Promise { + let pubkeyBs58; + if (typeof pubkey === "string") { + pubkeyBs58 = pubkey; + } else { + pubkeyBs58 = pubkey.toBase58(); + } + return ( + (await this.#sendJSONPost( + `${this.host}/auth/init`, + { + pubkey: pubkeyBs58, + }, + false, + true + )) as InitAuthOutput + ).msg; + } + + async confirmAuth( + msg: string, + signature: ArrayBuffer | Uint8Array | string + ): Promise { + let sig; + if (typeof signature === "string") { + sig = signature; + } else { + sig = bs58.encodeBase58(signature); + } + const token = `${msg}.${sig}`; + return await this.#sendJSONPost( + `${this.host}/auth/confirm`, + { + token, + }, + false, + true + ); + } + + async startFlow( + id: FlowId, + params: StartFlowParams + ): Promise { + return await this.#sendJSONPost(`${this.host}/flow/start/${id}`, params); + } + + async startFlowShared( + id: FlowId, + params: StartFlowSharedParams + ): Promise { + return await this.#sendJSONPost( + `${this.host}/flow/start_shared/${id}`, + params + ); + } + + async startFlowUnverified( + id: FlowId, + publicKey: web3.PublicKey, + params: StartFlowUnverifiedParams + ): Promise { + return await this.#sendJSONPost( + `${this.host}/flow/start_unverified/${id}`, + params, + publicKey.toBase58() + ); + } + + async getFlowOutput( + runId: FlowRunId, + token?: string + ): Promise { + const value: IValue = await this.#sendJSONGet( + `${this.host}/flow/output/${runId}`, + token ?? true + ); + return Value.fromJSON(value); + } + + async getSignatureRequest( + runId: FlowRunId, + token?: string + ): Promise { + const value: ISignatureRequest = await this.#sendJSONGet( + `${this.host}/flow/signature_request/${runId}`, + token ?? true + ); + return new SignatureRequest(value); + } + + async stopFlow( + runId: FlowRunId, + params: StopFlowParams + ): Promise { + return await this.#sendJSONPost(`${this.host}/flow/stop/${runId}`, params); + } + + async submitSignature( + params: SubmitSignatureParams + ): Promise { + return await this.#sendJSONPost( + `${this.host}/signature/submit`, + params, + false + ); + } + + async signAndSubmitSignature( + req: SignatureRequest, + publicKey: web3.PublicKey, + signTransaction: ( + tx: web3.VersionedTransaction + ) => Promise + ) { + const requestedPublicKey = new web3.PublicKey(req.pubkey); + if (!publicKey.equals(requestedPublicKey)) { + throw new Error( + `different public key:\nrequested: ${ + req.pubkey + }}\nwallet: ${publicKey.toBase58()}` + ); + } + + const tx = req.buildTransaction(); + const signerPosition = tx.message.staticAccountKeys.findIndex((pk) => + pk.equals(requestedPublicKey) + ); + if (signerPosition === -1) { + throw new Error("pubkey is not in signers list"); + } + this.logger("tx", tx); + const signedTx: web3.VersionedTransaction = await signTransaction(tx); + this.logger("signedTx", signedTx); + const signature = signedTx.signatures[signerPosition]; + if (signature == null) throw new Error("signature is null"); + + const before = Buffer.from(tx.message.serialize()); + const after = Buffer.from(signedTx.message.serialize()); + const new_msg = before.equals(after) ? undefined : after.toString("base64"); + await this.submitSignature({ + id: req.id, + signature: bs58.encodeBase58(signature), + new_msg, + }); + } + + async deployFlow(id: FlowId): Promise { + const result: { + deployment_id: string; + } = await this.#sendJSONPost(`${this.host}/flow/deploy/${id}`, {}); + return result.deployment_id; + } + + async startDeployment( + deployment: IDeploymentSpecifier, + params?: StartDeploymentParams + ): Promise { + return await this.#sendJSONPost( + `${this.host}/deployment/start?${new DeploymentSpecifier( + deployment + ).formatQuery()}`, + params + ); + } + + async #claimToken() { + if (this.#supabase === undefined) return; + interface Output { + access_token: string; + refresh_token: string; + } + const token: Output = await this.#sendJSONPost( + `${this.host}/auth/claim_token` + ); + await this.#supabase.auth.setSession({ + access_token: token.access_token, + refresh_token: token.refresh_token, + }); + } +} diff --git a/@space-operator/client/src/deps.ts b/@space-operator/client/src/deps.ts new file mode 100644 index 00000000..2d5622e4 --- /dev/null +++ b/@space-operator/client/src/deps.ts @@ -0,0 +1,7 @@ +export * as web3 from "npm:@solana/web3.js@1"; +export * as lib from "jsr:@space-operator/flow-lib@0.10.0"; +export * as bs58 from "jsr:@std/encoding@^0.221.0/base58"; +export * as supabase from "npm:@supabase/supabase-js@2"; +export { Value, type IValue } from "jsr:@space-operator/flow-lib@0.10.0/value"; +export { Buffer } from "node:buffer"; +export type { Session as SupabaseSession } from "npm:@supabase/auth-js@2"; diff --git a/@space-operator/client/src/mod.ts b/@space-operator/client/src/mod.ts new file mode 100644 index 00000000..80a79e57 --- /dev/null +++ b/@space-operator/client/src/mod.ts @@ -0,0 +1,58 @@ +export { Value, type IValue } from "./deps.ts"; +export { Client, type ClientOptions } from "./client.ts"; +export { WsClient, type WcClientOptions } from "./ws.ts"; +export type { + FlowId, + FlowRunId, + UserId, + NodeId, + DeploymentId, + ErrorBody, + RestResult, +} from "./types/common.ts"; +export { DeploymentSpecifier } from "./types/rest.ts"; +export type { + GetFlowOutputOutput, + PartialConfig, + SolanaActionConfig, + StartFlowOutput, + StartFlowParams, + StartFlowSharedOutput, + StartFlowSharedParams, + StartFlowUnverifiedOutput, + StartFlowUnverifiedParams, + SubmitSignatureOutput, + SubmitSignatureParams, + ValuesConfig, + StopFlowParams, + StopFlowOutput, + InitAuthOutput, + ConfirmAuthOutput, + StartDeploymentParams, + IDeploymentSpecifier, + StartDeploymentOutput, +} from "./types/rest.ts"; +export { + type WsResponse, + type AuthenticateRequest, + type AuthenticateResponse, + type SubscribeFlowRunEventsRequest, + type SubscribeFlowRunEventsResponse, + type SubscribeSignatureRequestsRequest, + type SubscribeSignatureRequestsResponse, + type SignatureRequestsEvent, + type FlowRunEvent, + type FlowRunEventEnum, + type LogLevel, + type FlowStart, + type FlowError, + type FlowLog, + type FlowFinish, + type NodeStart, + type NodeError, + type NodeOutput, + type NodeLog, + type NodeFinish, + type ISignatureRequest, + SignatureRequest, +} from "./types/ws.ts"; diff --git a/@space-operator/client/src/supabase.ts b/@space-operator/client/src/supabase.ts new file mode 100644 index 00000000..cbdeaf5e --- /dev/null +++ b/@space-operator/client/src/supabase.ts @@ -0,0 +1,1286 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + pgbouncer: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + get_auth: { + Args: { + p_usename: string + } + Returns: { + username: string + password: string + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + apikeys: { + Row: { + created_at: string + key_hash: string + name: string + trimmed_key: string + user_id: string + } + Insert: { + created_at?: string + key_hash: string + name: string + trimmed_key: string + user_id: string + } + Update: { + created_at?: string + key_hash?: string + name?: string + trimmed_key?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_deployments: { + Row: { + action_config: Json | null + action_identity: string | null + created_at: string + entrypoint: number + fees: Json[] + id: string + output_instructions: boolean + start_permission: Json + user_id: string + } + Insert: { + action_config?: Json | null + action_identity?: string | null + created_at?: string + entrypoint: number + fees: Json[] + id: string + output_instructions: boolean + start_permission: Json + user_id: string + } + Update: { + action_config?: Json | null + action_identity?: string | null + created_at?: string + entrypoint?: number + fees?: Json[] + id?: string + output_instructions?: boolean + start_permission?: Json + user_id?: string + } + Relationships: [] + } + flow_deployments_flows: { + Row: { + data: Json + deployment_id: string + flow_id: number + user_id: string + } + Insert: { + data: Json + deployment_id: string + flow_id: number + user_id: string + } + Update: { + data?: Json + deployment_id?: string + flow_id?: number + user_id?: string + } + Relationships: [ + { + foreignKeyName: "flow_deployments_flows_deployment_id_fkey" + columns: ["deployment_id"] + referencedRelation: "flow_deployments" + referencedColumns: ["id"] + }, + { + foreignKeyName: "flow_deployments_flows_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_deployments_tags: { + Row: { + deployment_id: string + entrypoint: number + tag: string + user_id: string + } + Insert: { + deployment_id: string + entrypoint: number + tag: string + user_id: string + } + Update: { + deployment_id?: string + entrypoint?: number + tag?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "flow_deployments_tags_deployment_id_entrypoint_fkey" + columns: ["deployment_id", "entrypoint"] + referencedRelation: "flow_deployments" + referencedColumns: ["id", "entrypoint"] + }, + { + foreignKeyName: "flow_deployments_tags_deployment_id_fkey" + columns: ["deployment_id"] + referencedRelation: "flow_deployments" + referencedColumns: ["id"] + }, + { + foreignKeyName: "flow_deployments_tags_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_deployments_wallets: { + Row: { + deployment_id: string + user_id: string + wallet_id: number + } + Insert: { + deployment_id: string + user_id: string + wallet_id: number + } + Update: { + deployment_id?: string + user_id?: string + wallet_id?: number + } + Relationships: [ + { + foreignKeyName: "flow_deployments_wallets_deployment_id_fkey" + columns: ["deployment_id"] + referencedRelation: "flow_deployments" + referencedColumns: ["id"] + }, + { + foreignKeyName: "flow_deployments_wallets_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_run: { + Row: { + call_depth: number + collect_instructions: boolean + edges: Json[] + end_time: string | null + environment: Json + errors: string[] | null + flow_id: number + id: string + inputs: Json + instructions_bundling: Json + network: Json + nodes: Json[] + not_run: string[] | null + origin: Json + output: Json | null + partial_config: Json | null + signers: Json + start_time: string | null + user_id: string + } + Insert: { + call_depth: number + collect_instructions: boolean + edges: Json[] + end_time?: string | null + environment: Json + errors?: string[] | null + flow_id: number + id: string + inputs: Json + instructions_bundling: Json + network: Json + nodes: Json[] + not_run?: string[] | null + origin: Json + output?: Json | null + partial_config?: Json | null + signers: Json + start_time?: string | null + user_id: string + } + Update: { + call_depth?: number + collect_instructions?: boolean + edges?: Json[] + end_time?: string | null + environment?: Json + errors?: string[] | null + flow_id?: number + id?: string + inputs?: Json + instructions_bundling?: Json + network?: Json + nodes?: Json[] + not_run?: string[] | null + origin?: Json + output?: Json | null + partial_config?: Json | null + signers?: Json + start_time?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-flow_id" + columns: ["flow_id"] + referencedRelation: "flows" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_run_logs: { + Row: { + content: string + flow_run_id: string + log_index: number + log_level: string + module: string | null + node_id: string | null + time: string + times: number | null + user_id: string + } + Insert: { + content: string + flow_run_id: string + log_index: number + log_level: string + module?: string | null + node_id?: string | null + time: string + times?: number | null + user_id: string + } + Update: { + content?: string + flow_run_id?: string + log_index?: number + log_level?: string + module?: string | null + node_id?: string | null + time?: string + times?: number | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-flow_run_id" + columns: ["flow_run_id"] + referencedRelation: "flow_run" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk-node_run_id" + columns: ["flow_run_id", "node_id", "times"] + referencedRelation: "node_run" + referencedColumns: ["flow_run_id", "node_id", "times"] + }, + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flow_run_shared: { + Row: { + flow_run_id: string + user_id: string + } + Insert: { + flow_run_id: string + user_id: string + } + Update: { + flow_run_id?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-flow_run_id" + columns: ["flow_run_id"] + referencedRelation: "flow_run" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + flows: { + Row: { + created_at: string + current_network: Json + custom_networks: Json[] + description: string + edges: Json[] + environment: Json + gg_marketplace: boolean | null + guide: Json | null + id: number + instructions_bundling: Json + isPublic: boolean + lastest_flow_run_id: string | null + mosaic: Json | null + name: string + nodes: Json[] + parent_flow: number | null + start_shared: boolean + start_unverified: boolean + tags: string[] + updated_at: string | null + user_id: string + uuid: string | null + viewport: Json + } + Insert: { + created_at?: string + current_network?: Json + custom_networks?: Json[] + description?: string + edges?: Json[] + environment?: Json + gg_marketplace?: boolean | null + guide?: Json | null + id?: number + instructions_bundling?: Json + isPublic?: boolean + lastest_flow_run_id?: string | null + mosaic?: Json | null + name?: string + nodes?: Json[] + parent_flow?: number | null + start_shared?: boolean + start_unverified?: boolean + tags?: string[] + updated_at?: string | null + user_id?: string + uuid?: string | null + viewport?: Json + } + Update: { + created_at?: string + current_network?: Json + custom_networks?: Json[] + description?: string + edges?: Json[] + environment?: Json + gg_marketplace?: boolean | null + guide?: Json | null + id?: number + instructions_bundling?: Json + isPublic?: boolean + lastest_flow_run_id?: string | null + mosaic?: Json | null + name?: string + nodes?: Json[] + parent_flow?: number | null + start_shared?: boolean + start_unverified?: boolean + tags?: string[] + updated_at?: string | null + user_id?: string + uuid?: string | null + viewport?: Json + } + Relationships: [ + { + foreignKeyName: "flows_lastest_flow_run_id_fkey" + columns: ["lastest_flow_run_id"] + referencedRelation: "flow_run" + referencedColumns: ["id"] + }, + { + foreignKeyName: "flows_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + kvstore: { + Row: { + key: string + last_updated: string | null + store_name: string + user_id: string + value: Json + } + Insert: { + key: string + last_updated?: string | null + store_name: string + user_id?: string + value: Json + } + Update: { + key?: string + last_updated?: string | null + store_name?: string + user_id?: string + value?: Json + } + Relationships: [ + { + foreignKeyName: "kvstore_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "kvstore_user_id_store_name_fkey" + columns: ["user_id", "store_name"] + referencedRelation: "kvstore_metadata" + referencedColumns: ["user_id", "store_name"] + }, + ] + } + kvstore_metadata: { + Row: { + stats_size: number + store_name: string + user_id: string + } + Insert: { + stats_size?: number + store_name: string + user_id?: string + } + Update: { + stats_size?: number + store_name?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "kvstore_metadata_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "kvstore_metadata_user_id_user_quotas_fkey" + columns: ["user_id"] + referencedRelation: "user_quotas" + referencedColumns: ["user_id"] + }, + ] + } + node_run: { + Row: { + end_time: string | null + errors: string[] | null + flow_run_id: string + input: Json + node_id: string + output: Json | null + start_time: string | null + times: number + user_id: string + } + Insert: { + end_time?: string | null + errors?: string[] | null + flow_run_id: string + input?: Json + node_id: string + output?: Json | null + start_time?: string | null + times: number + user_id: string + } + Update: { + end_time?: string | null + errors?: string[] | null + flow_run_id?: string + input?: Json + node_id?: string + output?: Json | null + start_time?: string | null + times?: number + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-flow_run_id" + columns: ["flow_run_id"] + referencedRelation: "flow_run" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + nodes: { + Row: { + created_at: string | null + data: Json + id: number + isPublic: boolean | null + licenses: string[] | null + name: string | null + sources: Json + status: string | null + storage_path: string | null + targets: Json + "targets_form.extra": Json + "targets_form.form_data": Json | null + "targets_form.json_schema": Json | null + "targets_form.ui_schema": Json | null + type: string | null + unique_node_id: string | null + user_id: string | null + } + Insert: { + created_at?: string | null + data?: Json + id?: number + isPublic?: boolean | null + licenses?: string[] | null + name?: string | null + sources?: Json + status?: string | null + storage_path?: string | null + targets?: Json + "targets_form.extra"?: Json + "targets_form.form_data"?: Json | null + "targets_form.json_schema"?: Json | null + "targets_form.ui_schema"?: Json | null + type?: string | null + unique_node_id?: string | null + user_id?: string | null + } + Update: { + created_at?: string | null + data?: Json + id?: number + isPublic?: boolean | null + licenses?: string[] | null + name?: string | null + sources?: Json + status?: string | null + storage_path?: string | null + targets?: Json + "targets_form.extra"?: Json + "targets_form.form_data"?: Json | null + "targets_form.json_schema"?: Json | null + "targets_form.ui_schema"?: Json | null + type?: string | null + unique_node_id?: string | null + user_id?: string | null + } + Relationships: [ + { + foreignKeyName: "nodes_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + pubkey_whitelists: { + Row: { + info: string | null + pubkey: string + } + Insert: { + info?: string | null + pubkey: string + } + Update: { + info?: string | null + pubkey?: string + } + Relationships: [] + } + signature_requests: { + Row: { + created_at: string + flow_run_id: string | null + id: number + msg: string + new_msg: string | null + pubkey: string + signature: string | null + signatures: Json[] | null + user_id: string + } + Insert: { + created_at?: string + flow_run_id?: string | null + id?: number + msg: string + new_msg?: string | null + pubkey: string + signature?: string | null + signatures?: Json[] | null + user_id: string + } + Update: { + created_at?: string + flow_run_id?: string | null + id?: number + msg?: string + new_msg?: string | null + pubkey?: string + signature?: string | null + signatures?: Json[] | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "fk-user_id" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "signature_requests_flow_run_id_fkey" + columns: ["flow_run_id"] + referencedRelation: "flow_run" + referencedColumns: ["id"] + }, + ] + } + user_quotas: { + Row: { + credit: number + kvstore_count: number + kvstore_count_limit: number + kvstore_size: number + kvstore_size_limit: number + used_credit: number + user_id: string + } + Insert: { + credit?: number + kvstore_count?: number + kvstore_count_limit?: number + kvstore_size?: number + kvstore_size_limit?: number + used_credit?: number + user_id: string + } + Update: { + credit?: number + kvstore_count?: number + kvstore_count_limit?: number + kvstore_size?: number + kvstore_size_limit?: number + used_credit?: number + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_quotas_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + users_public: { + Row: { + avatar: string | null + description: string | null + email: string + flow_skills: Json | null + node_skills: Json | null + pub_key: string + status: string + tasks_skills: Json | null + updated_at: string | null + user_id: string + username: string | null + } + Insert: { + avatar?: string | null + description?: string | null + email: string + flow_skills?: Json | null + node_skills?: Json | null + pub_key: string + status?: string + tasks_skills?: Json | null + updated_at?: string | null + user_id: string + username?: string | null + } + Update: { + avatar?: string | null + description?: string | null + email?: string + flow_skills?: Json | null + node_skills?: Json | null + pub_key?: string + status?: string + tasks_skills?: Json | null + updated_at?: string | null + user_id?: string + username?: string | null + } + Relationships: [ + { + foreignKeyName: "users_public_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + wallets: { + Row: { + adapter: string | null + created_at: string | null + description: string + encrypted_keypair: Json | null + icon: string | null + id: number + name: string + public_key: string + purpose: string | null + type: string | null + user_id: string + } + Insert: { + adapter?: string | null + created_at?: string | null + description?: string + encrypted_keypair?: Json | null + icon?: string | null + id?: number + name?: string + public_key: string + purpose?: string | null + type?: string | null + user_id: string + } + Update: { + adapter?: string | null + created_at?: string | null + description?: string + encrypted_keypair?: Json | null + icon?: string | null + id?: number + name?: string + public_key?: string + purpose?: string | null + type?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "wallets_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + increase_credit: { + Args: { + user_id: string + amount: number + } + Returns: number + } + increase_used_credit: { + Args: { + user_id: string + amount: number + } + Returns: number + } + is_nft_admin: { + Args: { + user_id: string + } + Returns: boolean + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + owner_id: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } + Relationships: [] + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + Relationships: [] + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + owner_id: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Relationships: [ + { + foreignKeyName: "objects_bucketId_fkey" + columns: ["bucket_id"] + referencedRelation: "buckets" + referencedColumns: ["id"] + }, + ] + } + s3_multipart_uploads: { + Row: { + bucket_id: string + created_at: string + id: string + in_progress_size: number + key: string + owner_id: string | null + upload_signature: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + id: string + in_progress_size?: number + key: string + owner_id?: string | null + upload_signature: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + id?: string + in_progress_size?: number + key?: string + owner_id?: string | null + upload_signature?: string + version?: string + } + Relationships: [ + { + foreignKeyName: "s3_multipart_uploads_bucket_id_fkey" + columns: ["bucket_id"] + referencedRelation: "buckets" + referencedColumns: ["id"] + }, + ] + } + s3_multipart_uploads_parts: { + Row: { + bucket_id: string + created_at: string + etag: string + id: string + key: string + owner_id: string | null + part_number: number + size: number + upload_id: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + etag: string + id?: string + key: string + owner_id?: string | null + part_number: number + size?: number + upload_id: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + etag?: string + id?: string + key?: string + owner_id?: string | null + part_number?: number + size?: number + upload_id?: string + version?: string + } + Relationships: [ + { + foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey" + columns: ["bucket_id"] + referencedRelation: "buckets" + referencedColumns: ["id"] + }, + { + foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey" + columns: ["upload_id"] + referencedRelation: "s3_multipart_uploads" + referencedColumns: ["id"] + }, + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: string[] + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + list_multipart_uploads_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + next_key_token?: string + next_upload_token?: string + } + Returns: { + key: string + id: string + created_at: string + }[] + } + list_objects_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + start_after?: string + next_token?: string + } + Returns: { + name: string + id: string + metadata: Json + updated_at: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends + | keyof PublicSchema["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] + ? PublicSchema["Enums"][PublicEnumNameOrOptions] + : never diff --git a/@space-operator/client/src/types/common.ts b/@space-operator/client/src/types/common.ts new file mode 100644 index 00000000..b86afa2f --- /dev/null +++ b/@space-operator/client/src/types/common.ts @@ -0,0 +1,9 @@ +export type FlowId = number; +export type DeploymentId = string; +export type FlowRunId = string; +export type NodeId = string; +export type UserId = string; +export interface ErrorBody { + error: string; +} +export type RestResult = T | ErrorBody; diff --git a/@space-operator/client/src/types/rest.ts b/@space-operator/client/src/types/rest.ts new file mode 100644 index 00000000..0782aba6 --- /dev/null +++ b/@space-operator/client/src/types/rest.ts @@ -0,0 +1,125 @@ +import type { Value, IValue, SupabaseSession } from "../deps.ts"; +import type { FlowId } from "../mod.ts"; +import type { FlowRunId, NodeId } from "./common.ts"; + +export interface InitAuthOutput { + msg: string; +} + +export interface ConfirmAuthOutput { + session: SupabaseSession; + new_user: boolean; +} + +export type GetFlowOutputOutput = Value; + +export interface StartFlowSharedParams { + inputs?: Record; +} + +export interface StartFlowSharedOutput { + flow_run_id: FlowRunId; +} + +export interface SolanaActionConfig { + action_signer: string; + action_identity: string; +} + +export interface StartFlowUnverifiedParams { + inputs?: Record; + output_instructions?: boolean; + action_identity?: string; + action_config?: SolanaActionConfig; + fees?: Array<[string, number]>; +} + +export interface StartFlowUnverifiedOutput { + flow_run_id: FlowRunId; + token: string; +} + +export interface ValuesConfig { + nodes: Record; + default_run_id?: FlowRunId; +} + +export interface PartialConfig { + only_nodes: Array; + values_config: ValuesConfig; +} + +export interface StartFlowParams { + inputs?: Record; + partial_config?: PartialConfig; + environment?: Record; +} + +export interface StartFlowOutput { + flow_run_id: FlowRunId; +} + +export interface SubmitSignatureParams { + id: number; + signature: string; + new_msg?: string; +} + +export interface SubmitSignatureOutput { + success: true; +} + +export interface StopFlowParams { + timeout_millies?: number; +} + +export interface StopFlowOutput { + success: true; +} + +export interface IDeploymentSpecifier { + id?: string; + flow?: FlowId; + tag?: string; +} + +export class DeploymentSpecifier implements IDeploymentSpecifier { + id?: string; + flow?: FlowId; + tag?: string; + constructor(ctor: IDeploymentSpecifier) { + this.id = ctor.id; + this.flow = ctor.flow; + this.tag = ctor.tag; + } + + static Id(id: string): DeploymentSpecifier { + return new DeploymentSpecifier({ id }); + } + + static Tag(flow: FlowId, tag: string): DeploymentSpecifier { + return new DeploymentSpecifier({ flow, tag }); + } + + formatQuery(): string { + const query = new URLSearchParams(); + if (this.id !== undefined) { + query.append("id", this.id); + } + if (this.flow !== undefined && this.tag !== undefined) { + query.append("flow", this.flow.toString()); + query.append("tag", this.tag); + } + return query.toString(); + } +} + +export interface StartDeploymentParams { + inputs?: Record; +} + +export interface StartDeploymentOutput { + flow_run_id: FlowRunId; + token: string; +} + diff --git a/@space-operator/client/src/types/ws.ts b/@space-operator/client/src/types/ws.ts new file mode 100644 index 00000000..dc913b5d --- /dev/null +++ b/@space-operator/client/src/types/ws.ts @@ -0,0 +1,246 @@ +import type { FlowRunId, NodeId } from "./common.ts"; +import { type Value, Buffer, bs58, web3 } from "../deps.ts"; + +export interface WsResponse { + id: number; + Ok?: T; + Err?: string; +} + +export class AuthenticateRequest { + id: number; + method: "Authenticate"; + params: { + token: string; + }; + constructor(id: number, token: string) { + this.id = id; + this.method = "Authenticate"; + this.params = { token }; + } +} + +export interface AuthenticateResponse { + id: number; + Ok?: { + user_id?: string; + pubkey?: string; + flow_run_id: string; + }; + Err?: string; +} + +export class SubscribeFlowRunEventsRequest { + id: number; + method: "SubscribeFlowRunEvents"; + params: { + flow_run_id: string; + token?: string; + }; + constructor(id: number, flow_run_id: string, token?: string) { + this.id = id; + this.method = "SubscribeFlowRunEvents"; + this.params = { + flow_run_id, + token, + }; + } +} + +export interface SubscribeFlowRunEventsResponse { + id: number; + Ok?: { + stream_id: number; + }; + Err?: string; +} + +export class SubscribeSignatureRequestsRequest { + id: number; + method: "Authenticate"; + params: {}; + constructor(id: number) { + this.id = id; + this.method = "Authenticate"; + this.params = {}; + } +} + +export interface SubscribeSignatureRequestsResponse { + id: number; + Ok?: { + stream_id: number; + }; + Err?: string; +} + +export interface SignatureRequestsEvent { + stream_id: number; + event: "SignatureRequest"; + data: SignatureRequest; +} + +export type FlowRunEvent = { stream_id: number } & FlowRunEventEnum; + +export type FlowRunEventEnum = + | { + event: "FlowStart"; + data: FlowStart; + } + | { + event: "FlowError"; + data: FlowLog; + } + | { + event: "FlowFinish"; + data: FlowFinish; + } + | { + event: "FlowLog"; + data: FlowLog; + } + | { + event: "NodeStart"; + data: NodeStart; + } + | { + event: "NodeOutput"; + data: NodeOutput; + } + | { + event: "NodeError"; + data: NodeError; + } + | { + event: "NodeFinish"; + data: NodeFinish; + } + | { + event: "NodeLog"; + data: NodeLog; + } + | { + event: "SignatureRequest"; + data: SignatureRequest; + }; + +export type LogLevel = "Trace" | "Debug" | "Info" | "Warn" | "Error"; + +export interface ISignatureRequest { + id: number; + time: string; + pubkey: string; + message: string; + timeout: number; + flow_run_id?: FlowRunId; + signatures?: Array<{ pubkey: string; signature: string }>; +} + +export class SignatureRequest implements ISignatureRequest { + id: number; + time: string; + pubkey: string; + message: string; + timeout: number; + flow_run_id?: FlowRunId; + signatures?: Array<{ pubkey: string; signature: string }>; + constructor(x: ISignatureRequest) { + this.id = x.id; + this.time = x.time; + this.pubkey = x.pubkey; + this.message = x.message; + this.timeout = x.timeout; + this.flow_run_id = x.flow_run_id; + this.signatures = x.signatures; + } + + buildTransaction(): web3.VersionedTransaction { + const buffer = Buffer.from(this.message, "base64"); + const solMsg = web3.VersionedMessage.deserialize(buffer); + + let sigs = undefined; + if (this.signatures) { + sigs = []; + const defaultSignature = bs58.encodeBase58(new Uint8Array(64)); + for (let i = 0; i < solMsg.header.numRequiredSignatures; i++) { + const pubkey = solMsg.staticAccountKeys[i].toBase58(); + let signature = this.signatures.find( + (x) => x.pubkey === pubkey + )?.signature; + if (signature === undefined) { + signature = defaultSignature; + } + sigs.push(bs58.decodeBase58(signature)); + } + } + + return new web3.VersionedTransaction(solMsg, sigs); + } +} + +export interface FlowStart { + flow_run_id: FlowRunId; + time: string; +} + +export interface FlowError { + flow_run_id: FlowRunId; + time: string; + error: string; +} + +export interface FlowLog { + flow_run_id: FlowRunId; + time: string; + level: LogLevel; + module?: string; + content: string; +} + +export interface FlowFinish { + flow_run_id: FlowRunId; + time: string; + not_run: Array; + output: Value; +} + +export interface NodeStart { + flow_run_id: FlowRunId; + time: string; + node_id: NodeId; + times: number; + input: Value; +} + +export interface NodeOutput { + flow_run_id: FlowRunId; + time: string; + node_id: NodeId; + times: number; + output: Value; +} + +export interface NodeError { + flow_run_id: FlowRunId; + time: string; + node_id: NodeId; + times: number; + error: string; +} + +export interface NodeLog { + flow_run_id: FlowRunId; + time: string; + node_id: NodeId; + times: number; + level: LogLevel; + module?: string; + content: string; +} + +export interface NodeFinish { + flow_run_id: FlowRunId; + time: string; + node_id: NodeId; + times: number; +} diff --git a/@space-operator/client/src/ws.ts b/@space-operator/client/src/ws.ts new file mode 100644 index 00000000..a562e085 --- /dev/null +++ b/@space-operator/client/src/ws.ts @@ -0,0 +1,235 @@ +import { FlowRunId } from "./types/common.ts"; +import { + AuthenticateRequest, + AuthenticateResponse, + FlowFinish, + FlowRunEvent, + SignatureRequest, + SignatureRequestsEvent, + SubscribeFlowRunEventsRequest, + SubscribeFlowRunEventsResponse, + SubscribeSignatureRequestsRequest, + SubscribeSignatureRequestsResponse, + WsResponse, +} from "./types/ws.ts"; + +export interface WcClientOptions { + url?: string; + token?: string | (() => Promise); + logger?: (msg: string, data: any) => any; +} + +export const WS_URL = "wss://dev-api.spaceoperator.com/ws"; + +function noop() {} + +export class WsClient { + private identity?: AuthenticateResponse["Ok"]; + private logger: Function = noop; + private url: string; + private conn?: WebSocket; + private counter: number = 0; + private token?: string | (() => Promise); + private reqCallbacks: Map = + new Map(); + private streamCallbacks: Map = new Map(); + private queue: Array = []; + + constructor(options: WcClientOptions) { + this.url = options.url ?? WS_URL; + this.token = options.token; + this.logger = options.logger ?? noop; + } + + public getIdentity(): WsClient["identity"] { + return this.identity; + } + + public setLogger(logger: Function) { + this.logger = logger; + } + + public setToken(token: string | (() => Promise)) { + this.token = token; + } + + public async subscribeFlowRunEvents( + callback: (ev: FlowRunEvent) => any, + id: FlowRunId, + token?: string, + finishCallback?: (ev: FlowFinish) => any + ) { + const result: SubscribeFlowRunEventsResponse = await this.send( + new SubscribeFlowRunEventsRequest(this.nextId(), id, token) + ); + if (result.Err !== undefined) { + throw new Error(result.Err); + } + if (result.Ok !== undefined) { + const id = result.Ok.stream_id; + this.streamCallbacks.set(id, { + callback: (ev: FlowRunEvent) => { + if (ev.event === "SignatureRequest") { + ev.data = new SignatureRequest(ev.data); + } + callback(ev); + if (ev.event === "FlowFinish") { + if (finishCallback !== undefined) { + finishCallback(ev.data); + this.streamCallbacks.delete(id); + } + } + }, + }); + } else { + throw new Error("response must contains Ok or Err"); + } + } + + public async subscribeSignatureRequest( + callback: (ev: SignatureRequestsEvent) => any + ) { + const result: SubscribeSignatureRequestsResponse = await this.send( + new SubscribeSignatureRequestsRequest(this.nextId()) + ); + if (result.Err !== undefined) { + throw new Error(result.Err); + } + if (result.Ok !== undefined) { + this.streamCallbacks.set(result.Ok.stream_id, { + callback: (ev: SignatureRequestsEvent) => { + if (ev.event === "SignatureRequest") { + ev.data = new SignatureRequest(ev.data); + } + callback(ev); + }, + }); + } else { + throw new Error("response must contains Ok or Err"); + } + } + + private async getToken(): Promise { + switch (typeof this.token) { + case "undefined": + throw new Error("no authentication token"); + case "string": + return this.token; + case "function": + return await this.token(); + default: + throw new TypeError("invalid token type"); + } + } + + private connect() { + if (this.conn !== undefined) return; + this.conn = new WebSocket(this.url); + this.conn.onopen = () => this.onConnOpen(); + this.conn.onmessage = (event) => this.onConnMessage(event); + this.conn.onerror = (error) => this.onConnError(error); + this.conn.onclose = (event) => this.onConnClose(event); + } + + private disconnect() { + this.conn = undefined; + this.reqCallbacks.forEach(({ reject }) => { + reject("disconnected"); + }); + this.streamCallbacks.clear(); + this.reqCallbacks.clear(); + } + + private onConnClose(event: any) { + this.log("closing", event); + this.disconnect(); + } + + private onConnError(event: any) { + this.log("error", event); + this.disconnect(); + } + + private onConnMessage(msg: { data: any }) { + if (typeof msg.data === "string") { + const json = JSON.parse(msg.data); + this.log("received", json); + if (json.id != null) { + const cb = this.reqCallbacks.get(json.id); + if (cb != null) { + this.reqCallbacks.delete(json.id); + cb.resolve(json); + } else { + throw `no callback for req ${json.id}`; + } + } else if (json.stream_id != null) { + const cb = this.streamCallbacks.get(json.stream_id); + if (cb != null) { + cb.callback(json); + } else { + throw `no callback for stream ${json.steam_id}`; + } + } else { + throw new Error("invalid message"); + } + } + } + + private log(msg: string, data?: any) { + this.logger(msg, data); + } + + private nextId(): number { + this.counter += 1; + if (this.counter > 0xffffffff) this.counter = 0; + return this.counter; + } + + private async send(msg: { + id: number; + method: string; + params: any; + }): Promise> { + const text = JSON.stringify(msg); + if (this.conn !== undefined) { + this.log("sending", msg); + this.conn.send(text); + } else { + this.log("queueing", msg); + this.queue.push(text); + this.connect(); + } + return await new Promise((resolve, reject) => { + this.reqCallbacks.set(msg.id, { resolve, reject }); + }); + } + + async authenticate() { + if (this.token === undefined) return; + const token = await this.getToken(); + const result: AuthenticateResponse = await this.send( + new AuthenticateRequest(this.nextId(), token) + ); + if (result.Err !== undefined) { + console.error("Authenticate error", result.Err); + } + if (result.Ok !== undefined) { + this.identity = result.Ok; + } else { + throw new Error("response must contains Ok or Err"); + } + } + + private async onConnOpen() { + await this.authenticate(); + this.queue = this.queue.filter((msg) => { + if (this.conn !== undefined) { + this.log("sending queued message", msg); + this.conn.send(msg); + return false; + } else { + return true; + } + }); + } +} diff --git a/@space-operator/client/tests/auth.ts b/@space-operator/client/tests/auth.ts new file mode 100644 index 00000000..f8a6ecdc --- /dev/null +++ b/@space-operator/client/tests/auth.ts @@ -0,0 +1,52 @@ +import * as client from "../src/mod.ts"; +import { web3, bs58 } from "../src/deps.ts"; +import * as dotenv from "jsr:@std/dotenv"; +import * as nacl from "npm:tweetnacl"; + +function ed25519SignText(keypair: web3.Keypair, message: string): Uint8Array { + return nacl.default.sign.detached( + new TextEncoder().encode(message), + keypair.secretKey + ); +} + +dotenv.loadSync({ + export: true, +}); + +const anonKey = Deno.env.get("ANON_KEY"); +if (!anonKey) throw new Error("no ANON_KEY"); + +const c = new client.Client({ + host: "http://localhost:8080", + supabaseUrl: "http://localhost:8000", + anonKey, +}); + +function keypair_from_env() { + const value = Deno.env.get("TEST_KEYPAIR"); + if (!value) return undefined; + return web3.Keypair.fromSecretKey(bs58.decodeBase58(value)); +} + +const keypair = keypair_from_env() ?? web3.Keypair.generate(); +console.log("using", keypair.publicKey.toBase58()); + +const run = async (keypair: web3.Keypair) => { + const msg = await c.initAuth(keypair.publicKey); + const sig = ed25519SignText(keypair, msg); + const result = await c.confirmAuth(msg, sig); + const sup = await c.supabase(result.session); + return await sup.auth.getUser(); +}; + +const results = await Promise.all([ + run(keypair), + run(keypair), + run(keypair), + run(web3.Keypair.generate()), + run(web3.Keypair.generate()), + run(web3.Keypair.generate()), +]); + +console.log(results); diff --git a/@space-operator/client/tests/deploy.ts b/@space-operator/client/tests/deploy.ts new file mode 100644 index 00000000..b630c522 --- /dev/null +++ b/@space-operator/client/tests/deploy.ts @@ -0,0 +1,98 @@ +import { bs58, Value, web3 } from "../src/deps.ts"; +import * as client from "../src/mod.ts"; +import * as dotenv from "jsr:@std/dotenv"; + +dotenv.loadSync({ + export: true, +}); + +const anonKey = Deno.env.get("ANON_KEY"); +if (!anonKey) throw new Error("no ANON_KEY"); + +const token = Deno.env.get("APIKEY"); +if (!token) throw new Error("no APIKEY"); + +const owner = new client.Client({ + host: "http://localhost:8080", + supabaseUrl: "http://localhost:8000", + anonKey, + token, +}); +const ownerKeypair = web3.Keypair.fromSecretKey( + bs58.decodeBase58(Deno.env.get("KEYPAIR") ?? "") +); + +const run = async () => { + const flowId = 3643; + console.log("deploy flow", flowId); + const id = await owner.deployFlow(flowId); + + const keypair = web3.Keypair.generate(); + + const starter = new client.Client({ + host: "http://localhost:8080", + supabaseUrl: "http://localhost:8000", + anonKey, + }); + + starter.setToken(keypair.publicKey.toString()); + console.log("start deployment", id); + const { flow_run_id, token } = await starter.startDeployment( + { + id, + }, + { + inputs: new Value({ + sender: keypair.publicKey, + n: 2, + }).M!, + } + ); + console.log("flow_run_id", flow_run_id); + + { + console.log("getSignatureRequest"); + const req = await owner.getSignatureRequest(flow_run_id); + console.log("req id", req.id); + await owner.signAndSubmitSignature( + req, + ownerKeypair.publicKey, + async (tx) => { + tx.sign([ownerKeypair]); + return tx; + } + ); + } + + { + console.log("getSignatureRequest"); + const req = await starter.getSignatureRequest(flow_run_id, token); + console.log("req id", req.id); + await starter.signAndSubmitSignature(req, keypair.publicKey, async (tx) => { + tx.sign([keypair]); + return tx; + }); + } + + const result = await starter.getFlowOutput(flow_run_id, token); + return result; +}; + +const res = await Promise.all([ + (async () => { + try { + await run(); + } catch (error) { + const sup = await owner.supabase(); + const result = await sup + .from("node_run") + .select("errors") + .not("errors", "is", "null"); + console.log(result); + + throw error; + } + })(), +]); + +console.log(res); diff --git a/@space-operator/client/tests/flow.ts b/@space-operator/client/tests/flow.ts new file mode 100644 index 00000000..7054731c --- /dev/null +++ b/@space-operator/client/tests/flow.ts @@ -0,0 +1,51 @@ +import * as client from "../src/mod.ts"; +import { Value, web3, bs58 } from "../src/deps.ts"; +import { Keypair } from "npm:@solana/web3.js@^1.91.4"; +import * as dotenv from "jsr:@std/dotenv"; + +dotenv.loadSync({ + export: true, +}); + +const c = new client.Client({ + host: "http://localhost:8080", +}); + +function keypair_from_env() { + const value = Deno.env.get("TEST_KEYPAIR"); + if (!value) return undefined; + return Keypair.fromSecretKey(bs58.decodeBase58(value)); +} + +const keypair = keypair_from_env() ?? web3.Keypair.generate(); +console.log("using", keypair.publicKey.toBase58()); +const connection = new web3.Connection("https://api.devnet.solana.com"); +if ((await connection.getBalance(keypair.publicKey)) == 0) { + console.log("request airdrop"); + await connection.requestAirdrop(keypair.publicKey, web3.LAMPORTS_PER_SOL); + while ((await connection.getBalance(keypair.publicKey)) == 0) {} +} + +const run = async () => { + const result = await c.startFlowUnverified(2154, keypair.publicKey, { + inputs: new Value({ + sender: keypair.publicKey, + }).M!, + output_instructions: false, + fees: [["HuktZqYAXSeMz5hMtdEnvsJAXtapg24zXU2tkDnGgaSZ", 1000]], + }); + + const req = await c.getSignatureRequest(result.flow_run_id, result.token); + + await c.signAndSubmitSignature(req, keypair.publicKey, async (tx) => { + tx.sign([keypair]); + return tx; + }); + + const output = await c.getFlowOutput(result.flow_run_id, result.token); + return output; +}; + +const res = await Promise.all([run()]); + +console.log(res); diff --git a/@space-operator/client/tests/upsert.ts b/@space-operator/client/tests/upsert.ts new file mode 100644 index 00000000..8b5dd182 --- /dev/null +++ b/@space-operator/client/tests/upsert.ts @@ -0,0 +1,47 @@ +import * as client from "../src/mod.ts"; +import { web3, bs58 } from "../src/deps.ts"; +import * as dotenv from "jsr:@std/dotenv"; +import * as nacl from "npm:tweetnacl"; + +function ed25519SignText(keypair: web3.Keypair, message: string): Uint8Array { + return nacl.default.sign.detached( + new TextEncoder().encode(message), + keypair.secretKey + ); +} + +dotenv.loadSync({ + export: true, +}); + +const c = new client.Client({ + host: "http://localhost:8080", + anonKey: Deno.env.get("ANON_KEY"), +}); + +function keypair_from_env() { + const value = Deno.env.get("TEST_KEYPAIR"); + if (!value) return undefined; + return web3.Keypair.fromSecretKey(bs58.decodeBase58(value)); +} + +const keypair = keypair_from_env() ?? web3.Keypair.generate(); +console.log("using", keypair.publicKey.toBase58()); + +const run = async (keypair: web3.Keypair) => { + const msg = await c.initAuth(keypair.publicKey); + const sig = ed25519SignText(keypair, msg); + const res = await c.confirmAuth(msg, sig); + c.setToken(`Bearer ${res.session.access_token}`); + const walletKey = web3.Keypair.generate(); + + const wallet = await c.upsertWallet({ + type: "HARDCODED", + name: "test encrypted wallet", + public_key: walletKey.publicKey.toBase58(), + keypair: bs58.encodeBase58(walletKey.secretKey), + }); + console.log(wallet); +}; + +await Promise.all([run(keypair)]); diff --git a/@space-operator/deno-command-rpc/README.md b/@space-operator/deno-command-rpc/README.md new file mode 100644 index 00000000..84f7d6b9 --- /dev/null +++ b/@space-operator/deno-command-rpc/README.md @@ -0,0 +1 @@ +# deno_command_rpc diff --git a/@space-operator/deno-command-rpc/deno.json b/@space-operator/deno-command-rpc/deno.json new file mode 100644 index 00000000..9d05f6cf --- /dev/null +++ b/@space-operator/deno-command-rpc/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@space-operator/deno-command-rpc", + "version": "0.10.0", + "exports": "./src/mod.ts", + "imports": {} +} \ No newline at end of file diff --git a/@space-operator/deno-command-rpc/deno.lock b/@space-operator/deno-command-rpc/deno.lock new file mode 100644 index 00000000..d13d591c --- /dev/null +++ b/@space-operator/deno-command-rpc/deno.lock @@ -0,0 +1,414 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@oak/commons@0.7": "jsr:@oak/commons@0.7.0", + "jsr:@oak/oak@14.2.0": "jsr:@oak/oak@14.2.0", + "jsr:@space-operator/flow-lib@0.8.2": "jsr:@space-operator/flow-lib@0.8.2", + "jsr:@space-operator/flow-lib@0.8.3": "jsr:@space-operator/flow-lib@0.8.3", + "jsr:@space-operator/flow-lib@0.9.0": "jsr:@space-operator/flow-lib@0.9.0", + "jsr:@std/assert@0.218": "jsr:@std/assert@0.218.2", + "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", + "jsr:@std/bytes@0.218": "jsr:@std/bytes@0.218.2", + "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/crypto@0.218": "jsr:@std/crypto@0.218.2", + "jsr:@std/encoding@0.218": "jsr:@std/encoding@0.218.2", + "jsr:@std/encoding@^0.218.2": "jsr:@std/encoding@0.218.2", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/http@0.218": "jsr:@std/http@0.218.2", + "jsr:@std/io@0.218": "jsr:@std/io@0.218.2", + "jsr:@std/media-types@0.218": "jsr:@std/media-types@0.218.2", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "jsr:@std/path@0.218": "jsr:@std/path@0.218.2", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.91.4", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1" + }, + "jsr": { + "@oak/commons@0.7.0": { + "integrity": "4bd889b3dc9ddac1b602034d88c137f06de7078775961b51081beb5f175c120b" + }, + "@oak/oak@14.2.0": { + "integrity": "b683b089693004ac3bca80b52159b3e9ad214dc8246ff5dc61ba658da78bc166", + "dependencies": [ + "jsr:@oak/commons@0.7", + "jsr:@std/assert@0.218", + "jsr:@std/bytes@0.218", + "jsr:@std/crypto@0.218", + "jsr:@std/encoding@0.218", + "jsr:@std/http@0.218", + "jsr:@std/io@0.218", + "jsr:@std/media-types@0.218", + "jsr:@std/path@0.218", + "npm:path-to-regexp@6.2.1" + ] + }, + "@space-operator/flow-lib@0.8.2": { + "integrity": "a56eaf147eedec9516e99ab99457e5ec437ac5d8914839ae06f2e0dd9b794998", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@space-operator/flow-lib@0.8.3": { + "integrity": "f9638deb0beedd13eb7600e8a6028cbd05aee0a0a7ef6b25e35af3661ff4191a", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@space-operator/flow-lib@0.9.0": { + "integrity": "e97a2002a192e7dfc79c10eeb7c0fc667be24ec1d77b85ac8288d25eb1c594ad", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/assert@0.218.2": { + "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" + }, + "@std/bytes@0.218.2": { + "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/crypto@0.218.2": { + "integrity": "8c5031a3a1c3ac3bed3c0d4bed2fe7e7faedcb673bbfa0edd10570c8452f5cd2", + "dependencies": [ + "jsr:@std/assert@^0.218.2", + "jsr:@std/encoding@^0.218.2" + ] + }, + "@std/encoding@0.218.2": { + "integrity": "da55a763c29bf0dbf06fd286430b358266eb99c28789d89fe9a3e28edecb8d8e" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/http@0.218.2": { + "integrity": "54223b62702e665b9dab6373ea2e51235e093ef47228d21cfa0469ee5ac75c9b", + "dependencies": [ + "jsr:@std/assert@^0.218.2", + "jsr:@std/encoding@^0.218.2" + ] + }, + "@std/io@0.218.2": { + "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", + "dependencies": [ + "jsr:@std/bytes@^0.218.2" + ] + }, + "@std/media-types@0.218.2": { + "integrity": "1ed3bd2a05e44bad3fc2bab1767d0ce7f2fd68baee62a980751ce51633acb788" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + }, + "@std/path@0.218.2": { + "integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662", + "dependencies": [ + "jsr:@std/assert@^0.218.2" + ] + } + }, + "npm": { + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.4": { + "integrity": "sha512-zconqecIcBqEF6JiM4xYF865Xc4aas+iWK5qnu7nwKPq9ilRYcn+2GiwpYXqUqqBUe0XCO17w18KO0F8h+QATg==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "path-to-regexp@6.2.1": { + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/@space-operator/deno-command-rpc/src/deps.ts b/@space-operator/deno-command-rpc/src/deps.ts new file mode 100644 index 00000000..38236650 --- /dev/null +++ b/@space-operator/deno-command-rpc/src/deps.ts @@ -0,0 +1,15 @@ +export { + Context, + type IValue, + Value, + type ContextData, + type CommandTrait, + type ServiceProxy, +} from "jsr:@space-operator/flow-lib@0.10.0"; +// } from "../../flow-lib/src/mod.ts"; +export { + Application, + type ListenOptions, + Router, + Status, +} from "jsr:@oak/oak@14.2.0"; diff --git a/@space-operator/deno-command-rpc/src/mod.ts b/@space-operator/deno-command-rpc/src/mod.ts new file mode 100644 index 00000000..815c8ebc --- /dev/null +++ b/@space-operator/deno-command-rpc/src/mod.ts @@ -0,0 +1,122 @@ +import { PromiseSet, CaptureLog } from "./utils.ts"; +import { + Context, + type IValue, + Value, + type ContextData, + type CommandTrait, + Application, + type ListenOptions, + Router, + Status, +} from "./deps.ts"; + +const RUN_SVC = "run"; + +interface IRequest { + envelope: string; + svc_name: string; + svc_id: string; + input: T; +} + +interface RunInput { + ctx: ContextData; + params: Record; +} + +interface RunOutput { + Ok?: Record; + Err?: string; +} + +interface Response { + envelope: string; + success: boolean; + data: T; +} + +export async function start( + cmd: CommandTrait, + listenOptions: Pick +) { + const realConsole = globalThis.console; + const router = new Router(); + router.post("/call", async (ctx) => { + const req: IRequest = await ctx.request.body.json(); + if (req.svc_name === RUN_SVC) { + const input: RunInput = req.input; + const params = Value.fromJSON({ M: input.params }); + let data: RunOutput; + let success = false; + const logPromises = new PromiseSet(); + try { + // build context + const context = new Context(input.ctx); + + // replace console + if (context.command?.log) { + globalThis.console = new CaptureLog( + realConsole, + context.command?.log, + logPromises + ); + } else { + globalThis.console = realConsole; + } + + // deserialize inputs + const convertedInputs = + typeof cmd.deserializeInputs === "function" + ? cmd.deserializeInputs(params.M!) + : params.toJSObject(); + + // run command + const outputs = await cmd.run(context, convertedInputs); + + // serialize outputs + const convertedOutputs: Record = + typeof cmd.serializeOutputs === "function" + ? cmd.serializeOutputs(outputs) + : new Value(outputs).M!; + + data = { Ok: convertedOutputs }; + success = true; + } catch (error) { + data = { Err: error.toString() }; + success = false; + } + + // wait for all logs to be inserted before responding + // because the ServiceProxy will be dropped then. + await logPromises.wait(); + + const resp: Response = { + envelope: req.envelope, + success, + data, + }; + ctx.response.body = resp; + ctx.response.type = "application/json"; + ctx.response.status = Status.OK; + } else { + ctx.response.body = { + envelope: req.envelope, + success: false, + data: "not found", + }; + ctx.response.type = "application/json"; + ctx.response.status = Status.OK; + } + }); + const app = new Application(); + app.addEventListener("listen", (ev) => { + Deno.stdout.writeSync(new TextEncoder().encode(ev.port.toString() + "\n")); + }); + app.use(router.routes()); + app.use(router.allowedMethods()); + await app.listen({ + hostname: listenOptions.hostname, + port: listenOptions.port, + }); +} diff --git a/@space-operator/deno-command-rpc/src/utils.ts b/@space-operator/deno-command-rpc/src/utils.ts new file mode 100644 index 00000000..50e5d833 --- /dev/null +++ b/@space-operator/deno-command-rpc/src/utils.ts @@ -0,0 +1,157 @@ +import type { ServiceProxy } from "./deps.ts"; + +type Console = typeof globalThis.console; +type Level = "INFO" | "ERROR" | "WARN" | "DEBUG" | "TRACE"; + +export class PromiseSet { + #counter: bigint = 0n; + #promises: Map> = new Map(); + + push(p: Promise) { + const id = this.#counter; + this.#counter += 1n; + this.#promises.set( + id, + p.then(() => { + this.#promises.delete(id); + }) + ); + } + + async wait(): Promise { + const promises = [...this.#promises.values()]; + this.#promises = new Map(); + await Promise.allSettled(promises); + } +} + +export class CaptureLog implements Console { + #realConsole: Console; + #service: ServiceProxy; + #promises: PromiseSet; + constructor(original: Console, service: ServiceProxy, promises: PromiseSet) { + this.#realConsole = original; + this.#service = service; + this.#promises = promises; + } + + #formatLogContent(data: any[]): string { + let str = ""; + data.forEach((value, index) => { + if (index > 0) str += " "; + switch (typeof value) { + case "string": + str += value; + break; + case "number": + str += value.toString(); + break; + case "bigint": + str += value.toString(); + break; + case "boolean": + str += value.toString(); + break; + case "symbol": + str += value.description; + break; + case "undefined": + str += "undefined"; + break; + case "object": + str += JSON.stringify(value); + break; + case "function": + str += "[Function]"; + break; + } + }); + return str; + } + + #call(level: Level, data: any[]) { + const promise = fetch(new URL("call", this.#service.base_url), { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + envelope: "", + svc_name: this.#service.name, + svc_id: this.#service.id, + input: { + level, + content: this.#formatLogContent(data), + }, + }), + }); + this.#promises.push(promise); + } + + clear(): void {} + debug(...data: any[]): void { + this.#call("DEBUG", data); + } + error(...data: any[]): void { + this.#call("ERROR", data); + } + info(...data: any[]): void { + this.#call("INFO", data); + } + log(...data: any[]): void { + this.#call("INFO", data); + } + trace(...data: any[]): void { + this.#call("TRACE", data); + } + warn(...data: any[]): void { + this.#call("WARN", data); + } + + assert(condition?: boolean | undefined, ...data: any[]): void { + return this.#realConsole.assert(condition, ...data); + } + + count(label?: string | undefined): void { + return this.#realConsole.count(label); + } + countReset(label?: string | undefined): void { + return this.#realConsole.countReset(label); + } + dir(item?: any, options?: any): void { + return this.#realConsole.dir(item, options); + } + dirxml(...data: any[]): void { + return this.#realConsole.dirxml(...data); + } + group(...data: any[]): void { + return this.#realConsole.group(...data); + } + groupCollapsed(...data: any[]): void { + return this.#realConsole.groupCollapsed(...data); + } + groupEnd(): void { + return this.#realConsole.groupEnd(); + } + profile(label?: string | undefined): void { + return this.#realConsole.profile(label); + } + profileEnd(label?: string | undefined): void { + return this.#realConsole.profileEnd(label); + } + table(tabularData?: any, properties?: string[] | undefined): void { + return this.#realConsole.table(tabularData, properties); + } + time(label?: string | undefined): void { + return this.#realConsole.time(label); + } + timeEnd(label?: string | undefined): void { + return this.#realConsole.timeEnd(label); + } + timeLog(label?: string | undefined, ...data: any[]): void { + return this.#realConsole.timeLog(label, ...data); + } + timeStamp(label?: string | undefined): void { + return this.#realConsole.timeStamp(label); + } +} diff --git a/@space-operator/deno.json b/@space-operator/deno.json new file mode 100644 index 00000000..e69de29b diff --git a/@space-operator/deno.lock b/@space-operator/deno.lock new file mode 100644 index 00000000..325e7648 --- /dev/null +++ b/@space-operator/deno.lock @@ -0,0 +1,346 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@space-operator/flow-lib@0.10.0": "jsr:@space-operator/flow-lib@0.10.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@1.94.0": "npm:@solana/web3.js@1.94.0", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.94.0", + "npm:buffer@6.0.3": "npm:buffer@6.0.3" + }, + "jsr": { + "@space-operator/flow-lib@0.10.0": { + "integrity": "d6f74303435982c9b70319bf9c75b419e1c15549c675468a75a1918b19a41fe1", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + } + }, + "npm": { + "@babel/runtime@7.24.7": { + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.2": { + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.94.0": { + "integrity": "sha512-wMiBebzu5I2fTSz623uj6VXpWFhl0d7qJKqPFK2I4IBLTNUdv+bOeA4H7OBM7Gworv7sOvB3xibRql6l61MeqA==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.7", + "@noble/curves": "@noble/curves@1.4.2", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.10", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@9.0.2_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@1.0.4" + } + }, + "@swc/helpers@0.5.11": { + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "tslib@2.6.3" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/uuid@8.3.4": { + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/ws@8.5.10": { + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.10": { + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.10" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.1" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@5.0.1": { + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.10": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.10" + } + }, + "jayson@4.1.0_ws@7.5.10": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.10", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.10" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.1": { + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@9.0.2_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw==", + "dependencies": { + "@swc/helpers": "@swc/helpers@0.5.11", + "@types/uuid": "@types/uuid@8.3.4", + "@types/ws": "@types/ws@8.5.10", + "buffer": "buffer@6.0.3", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@5.0.1", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.18.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@1.0.4": { + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "tslib@2.6.3": { + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.1" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.10": { + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dependencies": {} + }, + "ws@8.18.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/@space-operator/flow-lib/deno.json b/@space-operator/flow-lib/deno.json new file mode 100644 index 00000000..c14dc72f --- /dev/null +++ b/@space-operator/flow-lib/deno.json @@ -0,0 +1,12 @@ +{ + "name": "@space-operator/flow-lib", + "version": "0.10.0", + "exports": { + ".": "./src/mod.ts", + "./common": "./src/common.ts", + "./context": "./src/context.ts", + "./value": "./src/value.ts", + "./command": "./src/command.ts" + }, + "imports": {} +} \ No newline at end of file diff --git a/@space-operator/flow-lib/deno.lock b/@space-operator/flow-lib/deno.lock new file mode 100644 index 00000000..57315812 --- /dev/null +++ b/@space-operator/flow-lib/deno.lock @@ -0,0 +1,400 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/assert": "jsr:@std/assert@0.223.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/encoding@^0.220.1": "jsr:@std/encoding@0.220.1", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/fmt@^0.223.0": "jsr:@std/fmt@0.223.0", + "jsr:@std/msgpack": "jsr:@std/msgpack@0.221.0", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@1.94.0": "npm:@solana/web3.js@1.94.0", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.91.4", + "npm:@solana/web3.js@^1.94.0": "npm:@solana/web3.js@1.94.0", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:buffer@6.0.3": "npm:buffer@6.0.3" + }, + "jsr": { + "@std/assert@0.223.0": { + "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24", + "dependencies": [ + "jsr:@std/fmt@^0.223.0" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/encoding@0.220.1": { + "integrity": "8dc38dd72e36cd68857a5837e24eb09a64bb296b96c295239c75eec17d45d23f" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/fmt@0.223.0": { + "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + } + }, + "npm": { + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@babel/runtime@7.24.7": { + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.4": { + "integrity": "sha512-zconqecIcBqEF6JiM4xYF865Xc4aas+iWK5qnu7nwKPq9ilRYcn+2GiwpYXqUqqBUe0XCO17w18KO0F8h+QATg==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@solana/web3.js@1.94.0": { + "integrity": "sha512-wMiBebzu5I2fTSz623uj6VXpWFhl0d7qJKqPFK2I4IBLTNUdv+bOeA4H7OBM7Gworv7sOvB3xibRql6l61MeqA==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.7", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@9.0.2_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@1.0.4" + } + }, + "@swc/helpers@0.5.11": { + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "tslib@2.6.3" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/uuid@8.3.4": { + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/ws@8.5.10": { + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eventemitter3@5.0.1": { + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "rpc-websockets@9.0.2_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw==", + "dependencies": { + "@swc/helpers": "@swc/helpers@0.5.11", + "@types/uuid": "@types/uuid@8.3.4", + "@types/ws": "@types/ws@8.5.10", + "buffer": "buffer@6.0.3", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@5.0.1", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "superstruct@1.0.4": { + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "tslib@2.6.3": { + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/@space-operator/flow-lib/src/command.ts b/@space-operator/flow-lib/src/command.ts new file mode 100644 index 00000000..dc9c3565 --- /dev/null +++ b/@space-operator/flow-lib/src/command.ts @@ -0,0 +1,234 @@ +/** + * @module + * - CommandTrait: An interface that commands has to implement. + * - BaseCommand: A class implementing CommandTrait, providing default behaviors. + */ + +import type { Context } from "./context.ts"; +import { Value } from "./mod.ts"; + +/** + * To write a node, write a class that implements this interface and make it the default export of your module. + * + * ```ts + * import { Context, CommandTrait } from "jsr:@space-operator/flow-lib"; + * + * export default class MyCommand implements CommandTrait { + * async run( + * _: Context, + * params: Record + * ): Promise> { + * return { c: params.a + params.b }; + * } + * } + * ``` + */ +export interface CommandTrait { + /** + * Deserialize each inputs from `Value` to the type of your choice. + * This function will be called before passing inputs to `run()`. + * If not implemented, `Value.toJSObject()` will be called for each inputs. + */ + deserializeInputs?(inputs: Record): Record; + /** + * Serialize each output to a `Value`. + * This function will be called after each `run()`. + * If not implemented, `new Value(output)` will be called for each outputs. + */ + serializeOutputs?(outputs: Record): Record; + /** + * This function will be called every time the command is run. + * @param ctx Context + * @param params Map of input_name => input_value. + */ + run(ctx: Context, params: Record): Promise>; +} + +export type CommandType = "native" | "deno" | "WASM" | "mock"; + +export type ValueTypeBound = string; + +export interface Source { + id: string; + name: string; + type: ValueTypeBound; + optional: boolean; +} + +export interface Target { + id: string; + name: string; + type_bounds: ValueTypeBound[]; + required: boolean; + passthrough: boolean; +} + +export interface NodeData { + type: CommandType; + node_id: string; + sources: Source[]; + targets: Target[]; + // targets_form: any; +} + +export function deserializeInput( + port: Pick, + input: Value +): any | undefined { + for (const type of port.type_bounds) { + switch (type) { + case "bool": + return input.asBool(); + case "u8": + return input.asNumber(); + case "u16": + return input.asNumber(); + case "u32": + return input.asNumber(); + case "u64": + return input.asBigInt(); + case "u128": + return input.asBigInt(); + case "i8": + return input.asNumber(); + case "i16": + return input.asNumber(); + case "i32": + return input.asNumber(); + case "i64": + return input.asBigInt(); + case "i128": + return input.asBigInt(); + case "f32": + return input.asNumber(); + case "f64": + return input.asNumber(); + case "number": + return input.asNumber(); + case "decimal": + return input.asNumber(); + case "pubkey": + return input.asPubkey(); + case "address": + return input.asString(); + case "keypair": + return input.asKeypair(); + case "signature": + return input.asBytes(); + case "string": + return input.asString(); + case "bytes": + return input.asBytes(); + case "array": + case "object": + case "json": + case "free": + } + } + return undefined; +} + +export function serializeOutput( + port: Pick, + output: any +): Value | undefined { + switch (port.type) { + case "bool": + return Value.Boolean(Boolean(output)); + case "u8": + return Value.U64(parseInt(output)); + case "u16": + return Value.U64(parseInt(output)); + case "u32": + return Value.U64(parseInt(output)); + case "u64": + return Value.U64(BigInt(output)); + case "u128": + return Value.U128(BigInt(output)); + case "i8": + return Value.I64(parseInt(output)); + case "i16": + return Value.I64(parseInt(output)); + case "i32": + return Value.I64(parseInt(output)); + case "i64": + return Value.I64(BigInt(output)); + case "i128": + return Value.I128(BigInt(output)); + case "f32": + return Value.Float(parseFloat(output)); + case "f64": + return Value.Float(parseFloat(output)); + case "number": + return Value.Decimal(output); + case "decimal": + return Value.Decimal(output); + case "pubkey": + return Value.PublicKey(output); + case "address": + return Value.String(output); + case "keypair": + return Value.Keypair(output); + case "signature": + return Value.Signature(output); + case "string": + return Value.String(output); + case "bytes": + return Value.Bytes(output); + case "array": + case "object": + case "json": + case "free": + } + + return undefined; +} + +export class BaseCommand implements CommandTrait { + protected nd: NodeData; + constructor(nd: NodeData) { + this.nd = nd; + } + + deserializeInputs(inputs: Record): Record { + return Object.fromEntries( + Object.entries(inputs).map(([k, v]) => { + const port = this.nd.targets.find((v) => v.name === k); + if (port !== undefined) { + const de = deserializeInput(port, v); + if (de !== undefined) { + return [k, de]; + } + return [k, v.toJSObject()]; + } else { + return [k, v.toJSObject()]; + } + }) + ); + } + + serializeOutputs(outputs: Record): Record { + return Object.fromEntries( + Object.entries(outputs).map(([k, v]) => { + const port = this.nd.sources.find((v) => v.name === k); + if (port !== undefined) { + const ser = serializeOutput(port, v); + if (ser !== undefined) { + return [k, ser]; + } else { + return [k, new Value(v)]; + } + } else { + return [k, new Value(v)]; + } + }) + ); + } + + run( + _ctx: Context, + _params: Record + ): Promise> { + throw new Error("unimplemented"); + } +} diff --git a/@space-operator/flow-lib/src/common.ts b/@space-operator/flow-lib/src/common.ts new file mode 100644 index 00000000..d347de8b --- /dev/null +++ b/@space-operator/flow-lib/src/common.ts @@ -0,0 +1,7 @@ +export type FlowId = number; +export type FlowRunId = string; +export type NodeId = string; +export type UserId = string; +export interface User { + id: UserId; +} diff --git a/@space-operator/flow-lib/src/context.ts b/@space-operator/flow-lib/src/context.ts new file mode 100644 index 00000000..f2eeadaf --- /dev/null +++ b/@space-operator/flow-lib/src/context.ts @@ -0,0 +1,247 @@ +/** + * Providing services and information about the current invocation for nodes to use. + */ + +import type { FlowRunId, NodeId, User } from "./common.ts"; +import { msgpack } from "./deps.ts"; +import { Buffer, base64, bs58, web3 } from "./deps.ts"; +import { Value } from "./mod.ts"; + +export interface CommandContext { + flow_run_id: FlowRunId; + node_id: NodeId; + times: number; + svc: ServiceProxy; + log: ServiceProxy; +} + +export interface ServiceProxy { + name: string; + id: string; + base_url: string; +} + +/** + * ContextData is Context when serialized and sent over the network. + */ +export interface ContextData { + flow_owner: User; + started_by: User; + cfg: ContextConfig; + environment: Record; + endpoints: Endpoints; + command?: CommandContext; + signer: ServiceProxy; +} + +export interface RequestSignatureResponse { + signature: Buffer; + new_message?: Buffer; +} + +export interface ExecuteResponse { + signature?: Buffer; +} + +function isPubkey(x: web3.PublicKey | web3.Keypair): x is web3.PublicKey { + return (x as any)._bn !== undefined; +} + +export class Instructions { + #data: { + fee_payer: Uint8Array; + signers: Uint8Array[]; + instructions: msgpack.ValueMap[]; + }; + + constructor( + feePayer: web3.PublicKey, + signers: Array, + instructions: web3.TransactionInstruction[] + ) { + this.#data = { + fee_payer: feePayer.toBytes(), + signers: signers.map((x) => { + if (isPubkey(x)) { + const bytes = new Uint8Array(64); + bytes.set(x.toBytes(), 32); + return bytes; + } else { + return x.secretKey; + } + }), + instructions: instructions.map((i) => ({ + program_id: i.programId.toBytes(), + accounts: i.keys.map((k) => ({ + pubkey: k.pubkey.toBytes(), + is_signer: k.isSigner, + is_writable: k.isWritable, + })), + data: new Uint8Array(i.data), + })), + }; + } + + encode(): string { + return base64.encodeBase64(msgpack.encode(this.#data)); + } +} + +/** + * Providing services and information about the current invocation for nodes to use. + */ +export class Context { + /** + * Owner of current flow. + */ + flow_owner: User; + /** + * Who started the invocation. + */ + started_by: User; + private _cfg: ContextConfig; + /** + * Environment variables. + */ + environment: Record; + /** + * URLs to call other services. + */ + endpoints: Endpoints; + /** + * Context of the current node. + */ + command?: CommandContext; + /** + * Solana RPC client. + */ + solana: web3.Connection; + + private _signer: ServiceProxy; + + constructor(data: ContextData) { + this.flow_owner = data.flow_owner; + this.started_by = data.started_by; + this._cfg = data.cfg; + this.environment = data.environment; + this.endpoints = data.endpoints; + this.command = data.command; + this._signer = data.signer; + this.solana = new web3.Connection(this._cfg.solana_client.url); + } + + /** + * Request a signature from user. + * The backend with automaticaly find out who is the owner of the specified public key. + * + * Message data should be a serialized Solana message, produced by + * [Transaction.serializeMessage](https://solana-labs.github.io/solana-web3.js/classes/Transaction.html#serializeMessage) + * or [Message.serialize](https://solana-labs.github.io/solana-web3.js/classes/Message.html#serialize). + * We only support legacy transaction at the moment. + * + * + * @param pubkey Public key + * @param data Message data + * @returns Signature and the (optional) [updated transaction message](https://docs.phantom.app/developer-powertools/solana-priority-fees#how-phantom-applies-priority-fees-to-dapp-transactions) + */ + async requestSignature( + pubkey: web3.PublicKey, + data: Buffer + ): Promise { + const resp = await fetch(new URL("call", this._signer.base_url), { + method: "POST", + body: JSON.stringify({ + envelope: "", + svc_name: this._signer.name, + svc_id: this._signer.id, + input: { + id: null, + time: Date.now(), + pubkey: pubkey.toBase58(), + message: base64.encodeBase64(data), + timeout: 60 * 2, + flow_run_id: this.command?.flow_run_id, + signatures: null, + }, + }), + headers: { + "content-type": "application/json", + }, + }); + + const result = await resp.json(); + if (result.success === false) { + throw new Error(String(result.data)); + } + const output = result.data; + const signature = Buffer.from(bs58.decodeBase58(output.signature)); + const new_message = output.new_message + ? Buffer.from(base64.decodeBase64(output.new_message)) + : undefined; + return { + signature, + new_message, + }; + } + + async execute( + instructions: Instructions, + output: Record + ): Promise { + const svc = this.command?.svc; + if (!svc) throw new Error("service not available"); + + const resp = await fetch(new URL("call", svc.base_url), { + method: "POST", + body: JSON.stringify({ + envelope: "", + svc_name: svc.name, + svc_id: svc.id, + input: { + instructions: instructions.encode(), + output: new Value(output).M!, + }, + }), + headers: { + "content-type": "application/json", + }, + }); + + const result = await resp.json(); + if (result.success === false) { + throw new Error(String(result.data)); + } + const data = result.data; + const signature = data.signature + ? Buffer.from(bs58.decodeBase58(data.signature)) + : undefined; + return { + signature, + }; + } +} + +export interface Endpoints { + flow_server: string; + supabase: string; + supabase_anon_key: string; +} + +export interface ContextConfig { + http_client: HttpClientConfig; + solana_client: SolanaClientConfig; + environment: Record; + endpoints: Endpoints; +} + +export interface HttpClientConfig { + timeout_in_secs: number; + gzip: boolean; +} + +export interface SolanaClientConfig { + url: string; + cluster: SolanaNet; +} + +export type SolanaNet = "devnet" | "testnet" | "mainnet-beta"; diff --git a/@space-operator/flow-lib/src/deps.ts b/@space-operator/flow-lib/src/deps.ts new file mode 100644 index 00000000..219ad564 --- /dev/null +++ b/@space-operator/flow-lib/src/deps.ts @@ -0,0 +1,5 @@ +export * as bs58 from "jsr:@std/encoding@^0.221.0/base58"; +export * as base64 from "jsr:@std/encoding@^0.221.0/base64"; +export * as web3 from "npm:@solana/web3.js@1.94.0"; +export * as msgpack from "jsr:@std/msgpack@0.221.0"; +export { Buffer } from "node:buffer"; diff --git a/@space-operator/flow-lib/src/mod.ts b/@space-operator/flow-lib/src/mod.ts new file mode 100644 index 00000000..e9047c25 --- /dev/null +++ b/@space-operator/flow-lib/src/mod.ts @@ -0,0 +1,14 @@ +export type { + CommandContext, + Endpoints, + HttpClientConfig, + ContextConfig, + SolanaClientConfig, + SolanaNet, + ContextData, + ServiceProxy, +} from "./context.ts"; +export { Context } from "./context.ts"; +export { type CommandTrait, BaseCommand } from "./command.ts"; +export { Value, type IValue } from "./value.ts"; +export type { FlowId, FlowRunId, NodeId, User, UserId } from "./common.ts"; diff --git a/@space-operator/flow-lib/src/value.ts b/@space-operator/flow-lib/src/value.ts new file mode 100644 index 00000000..00e6f654 --- /dev/null +++ b/@space-operator/flow-lib/src/value.ts @@ -0,0 +1,479 @@ +/** + * @module + * Node's inputs and outputs are encoded as `IValue`. + */ + +import { bs58, base64, web3, type Buffer } from "./deps.ts"; + +/** + * JSON representation of `Value`. + */ +export interface IValue { + S?: string; + D?: string; + I?: string; + U?: string; + I1?: string; + U1?: string; + F?: string; + B?: boolean; + N?: 0; + B3?: string; + B6?: string; + BY?: string; + A?: IValue[]; + M?: Record; +} + +export class Map { + [key: string]: IValue; +} + +export function isIValue(v: any): v is IValue { + if (typeof v !== "object" || v === null) return false; + const keys = Object.keys(v); + if (keys.length !== 1) return false; + if (v.S !== undefined) return typeof v.S === "string"; + if (v.D !== undefined) return typeof v.D === "string"; + if (v.I !== undefined) return typeof v.I === "string"; + if (v.U !== undefined) return typeof v.U === "string"; + if (v.I1 !== undefined) return typeof v.I1 === "string"; + if (v.U1 !== undefined) return typeof v.U1 === "string"; + if (v.F !== undefined) return typeof v.F === "string"; + if (v.B !== undefined) return typeof v.B === "boolean"; + if (v.N !== undefined) return v.N === 0; + if (v.B3 !== undefined) return typeof v.B3 === "string"; + if (v.B6 !== undefined) return typeof v.B6 === "string"; + if (v.BY !== undefined) return typeof v.BY === "string"; + if (v.A !== undefined) return Array.isArray(v.A) && v.A.every(isIValue); + if (v.M !== undefined) return Object.values(v.M).every(isIValue); + return false; +} + +interface MaybePubkey { + toBase58(): string; + toBuffer(): Buffer; +} + +interface MaybeKeypair { + publicKey: MaybePubkey; + secretKey: Uint8Array; +} + +function maybePublicKey(x: MaybePubkey): x is web3.PublicKey { + if (x == null) return false; + return ( + typeof x.toBase58 === "function" && + typeof x.toBuffer === "function" && + x.toBuffer()?.byteLength === 32 + ); +} + +function maybeKeypair(x: MaybeKeypair): x is web3.Keypair { + if (x == null) return false; + return maybePublicKey(x.publicKey) && x.secretKey?.byteLength === 32; +} + +function validateFloat(s: string) { + if (!s.match(/^\-?[0-9]+(e[0-9]+)?(\.[0-9]+)?$/)) { + throw new SyntaxError(`invalid number: ${s}`); + } +} + +function validateInt(s: string) { + BigInt(s); +} + +function validateUInt(s: string) { + if (BigInt(s) < 0n) { + throw new SyntaxError("number is negative"); + } +} + +function validateBase58(s: string, length: number) { + const bytes = bs58.decodeBase58(s); + if (bytes.byteLength !== length) { + throw new SyntaxError(`bytes length ${bytes.byteLength} != ${length}`); + } +} + +function validateBase64(s: string) { + base64.decodeBase64(s); +} + +export class Value implements IValue { + S?: string; + D?: string; + I?: string; + U?: string; + I1?: string; + U1?: string; + F?: string; + B?: boolean; + N?: 0; + B3?: string; + B6?: string; + BY?: string; + A?: Value[]; + M?: Record; + + constructor(x?: any) { + if (x === undefined) { + return Value.Null(); + } + + const value = Value.#inferFromJSType(x); + if (value === undefined) throw TypeError("undefined"); + return value; + } + + toFlowValue(): Value { + return this; + } + + static #inferFromJSType(x: any): Value | undefined { + if (x instanceof Value) { + return x; + } + if (typeof x?.toFlowValue === "function") { + return x.toFlowValue(); + } + switch (typeof x) { + case "function": + return undefined; + case "number": + return Value.Decimal(x); + case "boolean": + return Value.Boolean(x); + case "string": + return Value.String(x); + case "undefined": + return Value.Null(); + case "bigint": + if (x < 0n) { + return Value.I128(x); + } else { + return Value.U128(x); + } + case "symbol": + return Value.String(x.toString()); + case "object": + if (x === null) { + return Value.Null(); + } + if (maybePublicKey(x)) { + return Value.fromJSON({ B3: x.toBase58() }); + } + if (maybeKeypair(x)) { + return Value.fromJSON({ + B6: bs58.encodeBase58( + new Uint8Array([...x.secretKey, ...x.publicKey.toBuffer()]) + ), + }); + } + if (typeof x.byteLength === "number") { + switch (x.byteLength) { + case 32: + return Value.fromJSON({ + B3: bs58.encodeBase58(x), + }); + case 64: + return Value.fromJSON({ + B6: bs58.encodeBase58(x), + }); + default: + return Value.fromJSON({ + BY: base64.encodeBase64(x), + }); + } + } + if (Object.prototype.isPrototypeOf.call(Array.prototype, x)) { + return Value.fromJSON({ + A: Array.from(x) + .map((x) => Value.#inferFromJSType(x)) + .filter((x) => x !== undefined) as IValue[], + }); + } + return Value.fromJSON({ + M: Object.fromEntries( + Object.entries(x) + .map(([k, v]) => [k, Value.#inferFromJSType(v)]) + .filter(([_k, v]) => v !== undefined) + ), + }); + } + } + + public validate() { + if (!isIValue(this)) throw SyntaxError("invalid JSON data"); + if (this.S !== undefined) return; + if (this.D !== undefined) validateFloat(this.D); + if (this.I !== undefined) validateInt(this.I); + if (this.U !== undefined) validateUInt(this.U); + if (this.I1 !== undefined) validateInt(this.I1); + if (this.U1 !== undefined) validateUInt(this.U1); + if (this.F !== undefined) validateFloat(this.F); + if (this.B !== undefined) return; + if (this.N !== undefined) return; + if (this.B3 !== undefined) validateBase58(this.B3, 32); + if (this.B6 !== undefined) validateBase58(this.B6, 64); + if (this.BY !== undefined) validateBase64(this.BY); + if (this.A !== undefined) this.A.forEach((v) => v.validate()); + if (this.M !== undefined) + Object.values(this.M).forEach((v) => v.validate()); + } + + static #fromJSONUnchecked(obj: IValue): Value { + if (obj instanceof Value) { + return obj; + } + + if (obj.A !== undefined) { + obj.A = obj.A.map(Value.#fromJSONUnchecked); + } else if (obj.M !== undefined) { + obj.M = Object.fromEntries( + Object.entries(obj.M).map(([k, v]) => [k, Value.#fromJSONUnchecked(v)]) + ); + } + return Object.assign(Object.create(Value.prototype), obj); + } + + /** + * New Value from JSON data. + */ + public static fromJSON(obj: IValue): Value { + const value = Value.#fromJSONUnchecked(obj); + value.validate(); + return value; + } + + /** + * Construct a U64 value. + */ + public static U64(x: string | number | bigint): Value { + const i = BigInt(x); + if (i < 0n || i > 18446744073709551615n) { + throw new Error("value out of range"); + } + return Value.#fromJSONUnchecked({ U: i.toString() }); + } + + /** + * Construct an I64 value. + */ + public static I64(x: string | number | bigint): Value { + const i = BigInt(x); + if (i < -9223372036854775808n || i > 9223372036854775807n) { + throw new Error("value out of range"); + } + return Value.#fromJSONUnchecked({ I: i.toString() }); + } + + /** + * Construct a String value. + */ + public static String(x: string): Value { + return Value.#fromJSONUnchecked({ S: x }); + } + + /** + * Construct a 64-bit Float value. + */ + public static Float(x: number): Value { + return Value.#fromJSONUnchecked({ F: x.toString() }); + } + + /** + * Construct a [Decimal](https://docs.rs/rust_decimal) value. + */ + public static Decimal(x: number | string | bigint): Value { + return Value.fromJSON({ D: x.toString() }); + } + + /** + * Construct a Null value. + */ + public static Null(): Value { + return Value.#fromJSONUnchecked({ N: 0 }); + } + + /** + * Construct a Boolean value. + */ + public static Boolean(x: boolean): Value { + return Value.#fromJSONUnchecked({ B: x }); + } + + /** + * Construct a U128 value. + */ + public static U128(x: string | number | bigint): Value { + const i = BigInt(x); + if (i < 0n || i > 340282366920938463463374607431768211455n) { + throw new Error("value out of range"); + } + return Value.#fromJSONUnchecked({ U1: i.toString() }); + } + + /** + * Construct an I128 value. + */ + public static I128(x: string | number | bigint): Value { + const i = BigInt(x); + if ( + i < -170141183460469231731687303715884105728n || + i > 170141183460469231731687303715884105727n + ) { + throw new Error("value out of range"); + } + return Value.#fromJSONUnchecked({ I1: i.toString() }); + } + + /** + * Construct a Solana Public Key value. + */ + public static PublicKey(x: web3.PublicKeyInitData): Value { + return Value.#fromJSONUnchecked({ B3: new web3.PublicKey(x).toBase58() }); + } + + /** + * Construct a Solana Signature value. + */ + public static Signature( + x: string | Buffer | Uint8Array | ArrayBuffer + ): Value { + if (typeof x === "string") return Value.fromJSON({ B6: x }); + else return Value.#fromJSONUnchecked({ B6: bs58.encodeBase58(x) }); + } + + /** + * Construct a Solana Keypair value. + */ + public static Keypair( + x: string | Buffer | Uint8Array | ArrayBuffer | web3.Keypair + ): Value { + if (typeof x === "string") { + return Value.fromJSON({ B6: x }); + } + if ((x as web3.Keypair).secretKey !== undefined) { + return Value.fromJSON({ + B6: bs58.encodeBase58((x as web3.Keypair).secretKey), + }); + } + return Value.fromJSON({ + B6: bs58.encodeBase58(x as any), + }); + } + + /** + * Construct a Bytes value. + */ + public static Bytes(x: Buffer | Uint8Array | ArrayBuffer): Value { + switch (x.byteLength) { + case 32: + return Value.#fromJSONUnchecked({ B3: bs58.encodeBase58(x) }); + case 64: + return Value.#fromJSONUnchecked({ B6: bs58.encodeBase58(x) }); + default: + return Value.#fromJSONUnchecked({ BY: base64.encodeBase64(x) }); + } + } + + public asBool(): boolean | undefined { + if (this.B !== undefined) return this.B; + return undefined; + } + + public asNumber(): number | undefined { + if (this.U !== undefined) return parseInt(this.U); + if (this.I !== undefined) return parseInt(this.I); + if (this.F !== undefined) return parseFloat(this.F); + if (this.U1 !== undefined) return parseInt(this.U1); + if (this.I1 !== undefined) return parseInt(this.I1); + return undefined; + } + + public asBigInt(): bigint | undefined { + if (this.U !== undefined) return BigInt(this.U); + if (this.I !== undefined) return BigInt(this.I); + if (this.U1 !== undefined) return BigInt(this.U1); + if (this.I1 !== undefined) return BigInt(this.I1); + return undefined; + } + + public asString(): string | undefined { + if (this.S !== undefined) return this.S; + return undefined; + } + + public asPubkey(): web3.PublicKey | undefined { + const x = this.S ? bs58.decodeBase58(this.S) : this.asBytes(); + if (x !== undefined) { + if (x.byteLength === 32) { + return new web3.PublicKey(x); + } else if (x.byteLength === 64) { + return new web3.PublicKey(x.slice(32)); + } + } + return undefined; + } + + public asKeypair(): web3.Keypair | undefined { + if (this.S !== undefined) { + return web3.Keypair.fromSecretKey(bs58.decodeBase58(this.S)); + } + const x = this.asBytes(); + if (x !== undefined) { + return web3.Keypair.fromSecretKey(x); + } + return undefined; + } + + public asBytes(): Uint8Array | undefined { + if (this.B3 !== undefined) return bs58.decodeBase58(this.B3); + if (this.B6 !== undefined) return bs58.decodeBase58(this.B6); + if (this.BY !== undefined) return base64.decodeBase64(this.BY); + if (this.A !== undefined) { + const x = this.A.map((v) => v.asNumber()); + if ( + x.every( + (v) => v !== undefined && 0 <= v && v <= 255 && Number.isInteger(v) + ) + ) { + return new Uint8Array(x as number[]); + } + } + return undefined; + } + + public asArray(): Value[] | undefined { + if (this.A !== undefined) return this.A; + return undefined; + } + + public asMap(): Record | undefined { + if (this.M !== undefined) return this.M; + return undefined; + } + + public toJSObject(): any { + if (this.S != undefined) return this.S; + if (this.D != undefined) return parseFloat(this.D); + if (this.I != undefined) return parseFloat(this.I); + if (this.U != undefined) return parseFloat(this.U); + if (this.I1 != undefined) return BigInt(this.I1); + if (this.U1 != undefined) return BigInt(this.U1); + if (this.F != undefined) return parseFloat(this.F); + if (this.B != undefined) return this.B; + if (this.N !== undefined) return null; + if (this.B3 !== undefined) return bs58.decodeBase58(this.B3); + if (this.B6 !== undefined) return bs58.decodeBase58(this.B6); + if (this.BY !== undefined) return new TextEncoder().encode(atob(this.BY)); + if (this.A !== undefined) return this.A.map((x) => x.toJSObject()); + if (this.M !== undefined) + return Object.fromEntries( + Object.entries(this.M).map(([k, v]) => [k, v.toJSObject()]) + ); + throw "invalid value"; + } +} diff --git a/@space-operator/flow-lib/tests/value.ts b/@space-operator/flow-lib/tests/value.ts new file mode 100644 index 00000000..5fbf3ca5 --- /dev/null +++ b/@space-operator/flow-lib/tests/value.ts @@ -0,0 +1,54 @@ +import { Value, isIValue } from "../src/value.ts"; +import { assertStrictEquals, assert, assertThrows } from "jsr:@std/assert"; + +Deno.test("isIValue", () => { + assert(isIValue({ S: "" })); + assert(isIValue({ D: "" })); + assert(isIValue({ I: "" })); + assert(isIValue({ U: "" })); + assert(isIValue({ I1: "" })); + assert(isIValue({ U1: "" })); + assert(isIValue({ F: "" })); + assert(isIValue({ B: false })); + assert(isIValue({ N: 0 })); + assert(isIValue({ B3: "" })); + assert(isIValue({ B6: "" })); + assert(isIValue({ BY: "" })); + assert(isIValue({ A: [{ S: "" }] })); + assert(isIValue({ M: { key: { N: 0 } } })); + assert(!isIValue({})); + assert(!isIValue({ X: 100 })); + assert(!isIValue({ S: "", B: false })); + assert(!isIValue("")); + assert(!isIValue(false)); + assert(!isIValue(() => 0)); + assert(!isIValue(2)); + assert(!isIValue(2n)); + assert(!isIValue([])); + assert(!isIValue(null)); + assert(!isIValue(undefined)); + assert(isIValue(Value.Null())); + assert(isIValue(Value.String("100"))); + assert(isIValue(Value.I64(100))); + assert(isIValue(Value.U64(100))); + assert(isIValue(Value.I128(100))); + assert(isIValue(Value.U128(100))); + assert(isIValue(Value.Boolean(false))); + assert(isIValue(Value.Decimal(1.2))); + assert(isIValue(Value.Float(1.2))); + assert(isIValue(Value.Bytes(new Uint8Array(32)))); + assert(isIValue(Value.Bytes(new Uint8Array(64)))); + assert(isIValue(Value.Bytes(new Uint8Array(100)))); + assert(isIValue(new Value({ foo: "hello" }))); + assert(isIValue(new Value([Value.I64("10000"), "hello"]))); +}); + +Deno.test("Value.fromJSON", () => { + const x = new Value(); + assertStrictEquals(Value.fromJSON(x), x); + assertThrows(() => Value.fromJSON({ S: "string", B: false })); + assertThrows(() => Value.fromJSON({ B3: "" })); + assertThrows(() => Value.fromJSON({ F: "" })); + assertThrows(() => Value.fromJSON({ I: "1.1" })); + assertThrows(() => Value.fromJSON({ I1: "1.1" })); +}); diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..3efd6260 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,10942 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.7.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot 0.12.3", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util 0.7.13", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.7.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util 0.7.13", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.11", + "base64 0.22.1", + "bitflags 2.7.0", + "brotli 6.0.0", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1 0.10.6", + "smallvec", + "tokio", + "tokio-util 0.7.13", + "tracing", + "zstd 0.13.2", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.96", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.8.11", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.37", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util 0.7.13", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli 0.31.1", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.10.3" +source = "git+https://github.com/RustCrypto/AEADs?rev=555ae1d82d000f01899498f969e6dc1d0a4fe467#555ae1d82d000f01899498f969e6dc1d0a4fe467" +dependencies = [ + "aead 0.4.3", + "aes", + "cipher 0.3.0", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anchor-attribute-access-control" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fe28365b33e8334dd70ae2f34a43892363012fe239cf37d2ee91693575b1f8" +dependencies = [ + "anchor-syn 0.30.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" +dependencies = [ + "anchor-syn 0.29.0", + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c288d496168268d198d9b53ee9f4f9d260a55ba4df9877ea1d4486ad6109e0f" +dependencies = [ + "anchor-syn 0.30.1", + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" +dependencies = [ + "anchor-syn 0.29.0", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b77b6948d0eeaaa129ce79eea5bbbb9937375a9241d909ca8fb9e006bb6e90" +dependencies = [ + "anchor-syn 0.30.1", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" +dependencies = [ + "anchor-syn 0.29.0", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d20bb569c5a557c86101b944721d865e1fd0a4c67c381d31a44a84f07f84828" +dependencies = [ + "anchor-syn 0.30.1", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" +dependencies = [ + "anchor-syn 0.29.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cebd8d0671a3a9dc3160c48598d652c34c77de6be4d44345b8b514323284d57" +dependencies = [ + "anchor-syn 0.30.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" +dependencies = [ + "anchor-syn 0.29.0", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb2a5eb0860e661ab31aff7bb5e0288357b176380e985bade4ccb395981b42d" +dependencies = [ + "anchor-lang-idl", + "anchor-syn 0.30.1", + "anyhow", + "bs58 0.5.1", + "heck 0.3.3", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" +dependencies = [ + "anchor-syn 0.29.0", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04368b5abef4266250ca8d1d12f4dff860242681e4ec22b885dcfe354fd35aa1" +dependencies = [ + "anchor-syn 0.30.1", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" +dependencies = [ + "anchor-syn 0.29.0", + "borsh-derive-internal 0.10.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bb0e0911ad4a70cab880cdd6287fe1e880a1a9d8e4e6defa8e9044b9796a6c" +dependencies = [ + "anchor-syn 0.30.1", + "borsh-derive-internal 0.10.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef415ff156dc82e9ecb943189b0cb241b3a6bfc26a180234dc21bd3ef3ce0cb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" +dependencies = [ + "anchor-attribute-access-control 0.29.0", + "anchor-attribute-account 0.29.0", + "anchor-attribute-constant 0.29.0", + "anchor-attribute-error 0.29.0", + "anchor-attribute-event 0.29.0", + "anchor-attribute-program 0.29.0", + "anchor-derive-accounts 0.29.0", + "anchor-derive-serde 0.29.0", + "anchor-derive-space 0.29.0", + "anchor-syn 0.29.0", + "arrayref", + "base64 0.13.1", + "bincode 1.3.3", + "borsh 0.10.4", + "bytemuck", + "getrandom 0.2.15", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6620c9486d9d36a4389cab5e37dc34a42ed0bfaa62e6a75a2999ce98f8f2e373" +dependencies = [ + "anchor-attribute-access-control 0.30.1", + "anchor-attribute-account 0.30.1", + "anchor-attribute-constant 0.30.1", + "anchor-attribute-error 0.30.1", + "anchor-attribute-event 0.30.1", + "anchor-attribute-program 0.30.1", + "anchor-derive-accounts 0.30.1", + "anchor-derive-serde 0.30.1", + "anchor-derive-space 0.30.1", + "arrayref", + "base64 0.21.7", + "bincode 1.3.3", + "borsh 0.10.4", + "bytemuck", + "getrandom 0.2.15", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31cf97b4e6f7d6144a05e435660fcf757dbc3446d38d0e2b851d11ed13625bba" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck 0.3.3", + "serde", + "serde_json", + "sha2 0.10.8", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-spl" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04bd077c34449319a1e4e0bc21cea572960c9ae0d0fefda0dd7c52fcc3c647a3" +dependencies = [ + "anchor-lang 0.30.1", + "spl-associated-token-account 3.0.4", + "spl-pod 0.2.5", + "spl-token", + "spl-token-2022 3.0.4", + "spl-token-group-interface 0.2.5", + "spl-token-metadata-interface 0.3.5", +] + +[[package]] +name = "anchor-syn" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" +dependencies = [ + "anyhow", + "bs58 0.5.1", + "heck 0.3.3", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-syn" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99daacb53b55cfd37ce14d6c9905929721137fd4c67bbab44a19802aecb622f" +dependencies = [ + "anyhow", + "bs58 0.5.1", + "heck 0.3.3", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time 0.3.37", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "brotli 7.0.0", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "async-trait" +version = "0.1.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "avro-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece550dd6710221de9bcdc1697424d8eee4fc4ca7e017479ea9d50c348465e37" +dependencies = [ + "byteorder", + "digest 0.9.0", + "lazy_static", + "libflate", + "num-bigint 0.2.6", + "rand 0.7.3", + "serde", + "serde_json", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.69", + "typed-builder", + "uuid 0.8.2", + "zerocopy 0.3.2", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" +dependencies = [ + "base64 0.13.1", + "blowfish", + "getrandom 0.2.15", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb50c5a2ef4b9b1e7ae73e3a73b52ea24b20312d629f9c4df28260b7ad2c3c4" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +dependencies = [ + "serde", +] + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + +[[package]] +name = "bon" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7acc34ff59877422326db7d6f2d845a582b16396b6b08194942bf34c6528ab" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4159dd617a7fbc9be6a692fe69dc2954f8e6bb6bb5e4d7578467441390d77fd0" +dependencies = [ + "darling 0.20.10", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +dependencies = [ + "borsh-derive 1.5.3", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal 0.10.4", + "borsh-schema-derive-internal 0.10.4", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "regex-automata 0.4.9", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bundlr-sdk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a32d9dd57076311dbd4e27a2335949f75b8414cbe365af28523426e2de8da0b" +dependencies = [ + "anyhow", + "async-recursion", + "async-stream", + "avro-rs", + "bs58 0.4.0", + "bytes", + "data-encoding", + "derive_builder", + "derive_more", + "ed25519-dalek 1.0.1", + "futures", + "lazy_static", + "num-derive 0.3.3", + "num-traits", + "pipe", + "primitive-types 0.11.1", + "rand 0.8.5", + "reqwest 0.11.27", + "ring 0.16.20", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror 1.0.69", + "tokio", + "tokio-util 0.6.10", +] + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +dependencies = [ + "serde", +] + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "caps" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.24", + "serde", + "serde_json", + "thiserror 2.0.11", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width 0.1.14", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.16.1", +] + +[[package]] +name = "clap" +version = "4.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +dependencies = [ + "anstyle", + "clap_lex 0.7.4", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "client" +version = "0.1.0" +dependencies = [ + "chrono", + "flow-lib", + "serde", + "serde_with 3.12.0", +] + +[[package]] +name = "cmds-deno" +version = "0.0.0" +dependencies = [ + "actix-web", + "anyhow", + "command-rpc", + "flow-lib", + "flow-value", + "home", + "serde", + "serde_json", + "srpc", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "uuid 1.11.1", +] + +[[package]] +name = "cmds-pdg" +version = "0.0.0" +dependencies = [ + "flow-lib", + "futures", + "once_cell", + "pdg-common", + "rand 0.8.5", + "rand_chacha 0.3.1", + "reqwest 0.12.12", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.20.1", + "tracing", + "uuid 1.11.1", +] + +[[package]] +name = "cmds-solana" +version = "0.0.0" +dependencies = [ + "anchor-lang 0.30.1", + "anchor-spl", + "anyhow", + "async-trait", + "base64 0.13.1", + "bincode 1.3.3", + "borsh 0.10.4", + "bs58 0.4.0", + "bundlr-sdk", + "bytemuck", + "bytemuck_derive", + "byteorder", + "bytes", + "chrono", + "ed25519-dalek 1.0.1", + "flow-lib", + "flow-value", + "futures", + "hex", + "inventory", + "jupiter-swap-api-client", + "mime_guess", + "mpl-bubblegum", + "mpl-candy-guard", + "mpl-candy-machine-core", + "mpl-core", + "mpl-core-candy-guard", + "mpl-core-candy-machine-core", + "mpl-token-auth-rules", + "mpl-token-metadata 4.1.2", + "once_cell", + "primitive-types 0.9.1", + "pyth-sdk-solana", + "rand 0.8.5", + "reqwest 0.12.12", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "serde_with 3.12.0", + "serde_wormhole 0.1.0 (git+https://github.com/space-operator/wormhole?rev=b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1)", + "solana-account-decoder", + "solana-client", + "solana-program 1.18.26", + "solana-sdk", + "solana-transaction-status", + "spl-account-compression", + "spl-associated-token-account 2.3.0", + "spl-memo", + "spl-noop", + "spl-pod 0.2.5", + "spl-token", + "spl-token-2022 3.0.4", + "struct-convert", + "thiserror 1.0.69", + "tiny-bip39", + "tokio", + "tracing", + "tracing-subscriber", + "wormhole-sdk", +] + +[[package]] +name = "cmds-std" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "bs58 0.4.0", + "bytes", + "criterion", + "flow-lib", + "flow-value", + "futures-executor", + "futures-util", + "hyper 0.14.32", + "mime_guess", + "once_cell", + "reqwest 0.12.12", + "rust_decimal", + "serde", + "serde_json", + "spo-postgrest", + "thiserror 1.0.69", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "command-rpc" +version = "0.0.0" +dependencies = [ + "actix", + "async-trait", + "flow-lib", + "futures", + "inventory", + "serde", + "serde_with 3.12.0", + "srpc", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.24.0", + "tower 0.4.13", + "tracing", + "url", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const_fn" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.37", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "corosensei" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80128832c58ea9cbd041d2a759ec449224487b2c1e400453d99d244eead87a8e" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.33.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529ffacce2249ac60edba2941672dfedf3d96558b415d0d8083cd007456e0f55" +dependencies = [ + "cranelift-entity 0.86.1", +] + +[[package]] +name = "cranelift-bforest" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2ab4512dfd3a6f4be184403a195f76e81a8a9f9e6c898e19d2dc3ce20e0115" +dependencies = [ + "cranelift-entity 0.91.1", +] + +[[package]] +name = "cranelift-codegen" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427d105f617efc8cb55f8d036a7fded2e227892d8780b4985e5551f8d27c4a92" +dependencies = [ + "cranelift-bforest 0.86.1", + "cranelift-codegen-meta 0.86.1", + "cranelift-codegen-shared 0.86.1", + "cranelift-entity 0.86.1", + "cranelift-isle 0.86.1", + "gimli 0.26.2", + "log", + "regalloc2 0.3.2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b022ed2a5913a38839dfbafe6cf135342661293b08049843362df4301261dc" +dependencies = [ + "arrayvec", + "bumpalo", + "cranelift-bforest 0.91.1", + "cranelift-codegen-meta 0.91.1", + "cranelift-codegen-shared 0.91.1", + "cranelift-egraph", + "cranelift-entity 0.91.1", + "cranelift-isle 0.91.1", + "gimli 0.26.2", + "log", + "regalloc2 0.5.1", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551674bed85b838d45358e3eab4f0ffaa6790c70dc08184204b9a54b41cdb7d1" +dependencies = [ + "cranelift-codegen-shared 0.86.1", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639307b45434ad112a98f8300c0f0ab085cbefcd767efcdef9ef19d4c0756e74" +dependencies = [ + "cranelift-codegen-shared 0.91.1", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b3a63ae57498c3eb495360944a33571754241e15e47e3bcae6082f40fec5866" + +[[package]] +name = "cranelift-codegen-shared" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278e52e29c53fcf32431ef08406c295699a70306d05a0715c5b1bf50e33a9ab7" + +[[package]] +name = "cranelift-egraph" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624b54323b06e675293939311943ba82d323bb340468ce1889be5da7932c8d73" +dependencies = [ + "cranelift-entity 0.91.1", + "fxhash", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "log", + "smallvec", +] + +[[package]] +name = "cranelift-entity" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11aa8aa624c72cc1c94ea3d0739fa61248260b5b14d3646f51593a88d67f3e6e" + +[[package]] +name = "cranelift-entity" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a59bcbca89c3f1b70b93ab3cbba5e5e0cbf3e63dadb23c7525cb142e21a9d4c" + +[[package]] +name = "cranelift-frontend" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "544ee8f4d1c9559c9aa6d46e7aaeac4a13856d620561094f35527356c7d21bd0" +dependencies = [ + "cranelift-codegen 0.86.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.86.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed16b14363d929b8c37e3c557d0a7396791b383ecc302141643c054343170aad" + +[[package]] +name = "cranelift-isle" +version = "0.91.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393bc73c451830ff8dbb3a07f61843d6cb41a084f9996319917c0b291ed785bb" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.26", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "git+https://github.com/dalek-cryptography/curve25519-dalek?rev=8274d5cbb6fc3f38cdc742b4798173895cd2a290#8274d5cbb6fc3f38cdc742b4798173895cd2a290" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core 0.12.4", + "darling_macro 0.12.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.96", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core 0.12.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "db" +version = "0.0.0" +dependencies = [ + "actix-web", + "anyhow", + "async-trait", + "base64 0.13.1", + "bcrypt", + "blake3", + "bs58 0.4.0", + "bytes", + "chacha20poly1305", + "chrono", + "csv", + "deadpool-postgres", + "ed25519-dalek 2.1.1", + "either", + "flow", + "flow-lib", + "flow-value", + "futures-util", + "hashbrown 0.14.5", + "kv", + "rand 0.8.5", + "reqwest 0.12.12", + "rustls 0.20.9", + "rustls-native-certs", + "rustls-pemfile 1.0.4", + "serde", + "serde_bytes", + "serde_json", + "serde_with 3.12.0", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tokio-postgres", + "tokio-postgres-rustls", + "toml", + "tower 0.4.13", + "tracing", + "tracing-subscriber", + "url", + "utils", + "uuid 1.11.1", + "zeroize", +] + +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b75ba49590d27f677d3bebaf76cd15889ca8b308bc7ba99bfa25f1d7269c13" +dependencies = [ + "deadpool", + "tokio", + "tokio-postgres", + "tracing", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling 0.12.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.96", +] + +[[package]] +name = "dialoguer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "dynasm" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dynasmrt" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fba5a42bd76a17cad4bfa00de168ee1cbfa06a5e8ce992ae880218c05641a9" +dependencies = [ + "byteorder", + "dynasm", + "memmap2", +] + +[[package]] +name = "eager" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe71d579d1812060163dff96056261deb5bf6729b100fa2e36a68b9649ba3d3" + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.1", + "ed25519 1.5.3", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek 1.0.1", + "hmac 0.12.1", + "sha2 0.10.8", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive 0.7.0", +] + +[[package]] +name = "enum-iterator" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94" +dependencies = [ + "enum-iterator-derive 1.4.0", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "enumset" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b4f62f0f8ca357f93ae90c8c2dd1041a1f665fde2f889ea9b1787903829015" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flow" +version = "0.0.0" +dependencies = [ + "actix", + "actix-web", + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode 1.3.3", + "bon", + "bs58 0.4.0", + "bytes", + "chrono", + "cmds-deno", + "cmds-solana", + "cmds-std", + "command-rpc", + "crossbeam-channel", + "derive_more", + "flow-lib", + "flow-value", + "futures", + "getset", + "hashbrown 0.14.5", + "home", + "indexmap 2.7.0", + "inventory", + "mime_guess", + "petgraph", + "reqwest 0.12.12", + "rhai-script", + "rust_decimal", + "serde", + "serde_json", + "solana-sdk", + "space-wasm", + "srpc", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tokio-util 0.7.13", + "tower 0.4.13", + "tracing", + "tracing-subscriber", + "url", + "utils", + "uuid 1.11.1", +] + +[[package]] +name = "flow-lib" +version = "0.1.0" +dependencies = [ + "actix", + "anyhow", + "async-trait", + "base64 0.21.7", + "bincode 1.3.3", + "bon", + "borsh 0.10.4", + "borsh 1.5.3", + "bs58 0.4.0", + "bytes", + "chrono", + "ed25519-dalek 2.1.1", + "five8", + "flow-value", + "futures", + "inventory", + "nom", + "once_cell", + "pin-project-lite", + "reqwest 0.12.12", + "rmp-serde", + "serde", + "serde_json", + "serde_with 3.12.0", + "solana-client", + "solana-sdk", + "solana-transaction-status", + "spo-helius", + "thiserror 1.0.69", + "tokio", + "tower 0.4.13", + "tracing", + "uuid 1.11.1", +] + +[[package]] +name = "flow-server" +version = "0.0.0" +dependencies = [ + "actix", + "actix-cors", + "actix-web", + "actix-web-actors", + "anyhow", + "async-trait", + "base64 0.13.1", + "bincode 2.0.0-rc.2", + "blake3", + "bs58 0.4.0", + "bytes", + "chrono", + "cmds-pdg", + "cmds-solana", + "cmds-std", + "criterion", + "db", + "ed25519-dalek 2.1.1", + "either", + "five8", + "flow", + "flow-lib", + "flow-value", + "futures-channel", + "futures-util", + "getset", + "hashbrown 0.14.5", + "hex", + "hmac 0.12.1", + "inventory", + "once_cell", + "rand 0.8.5", + "regex", + "reqwest 0.12.12", + "rhai-script", + "serde", + "serde_json", + "serde_with 3.12.0", + "sha2 0.10.8", + "solana-client", + "solana-sdk", + "space-wasm", + "thiserror 1.0.69", + "tokio", + "tokio-util 0.7.13", + "toml", + "tower 0.4.13", + "tracing", + "tracing-log", + "tracing-subscriber", + "url", + "utils", + "uuid 1.11.1", +] + +[[package]] +name = "flow-value" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "bs58 0.4.0", + "bytes", + "five8", + "indexmap 2.7.0", + "itoa", + "rust_decimal", + "rust_decimal_macros", + "ryu", + "serde", + "serde_json", + "serde_with 3.12.0", + "solana-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generational-arena" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +dependencies = [ + "fallible-iterator", + "indexmap 1.9.3", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "goblin" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7666983ed0dd8d21a6f6576ee00053ca0926fb281a5522577a4dbd0f1b54143" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util 0.7.13", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util 0.7.13", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.5.2", + "hyper-util", + "rustls 0.23.21", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", + "webpki-roots 0.26.7", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.2", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integration-tests" +version = "0.1.0" +dependencies = [ + "anyhow", + "cargo_metadata", + "dotenv", + "xshell", +] + +[[package]] +name = "inventory" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b31349d02fe60f80bbbab1a9402364cad7460626d6030494b08ac4a2075bf81" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "jupiter-swap-api-client" +version = "0.1.0" +source = "git+https://github.com/jup-ag/jupiter-swap-api-client.git?rev=e3c162b921f46eacc867ca9c55e7d610eefabe25#e3c162b921f46eacc867ca9c55e7d610eefabe25" +dependencies = [ + "anyhow", + "base64 0.22.1", + "reqwest 0.11.27", + "rust_decimal", + "serde", + "serde_json", + "serde_qs", + "solana-account-decoder", + "solana-sdk", +] + +[[package]] +name = "kaigan" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dd100976df9dd59d0c3fecf6f9ad3f161a087374d1b2a77ebb4ad8920f11bb" +dependencies = [ + "borsh 0.10.4", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kv" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620727085ac39ee9650b373fe6d8073a0aee6f99e52a9c72b25f7671078039ab" +dependencies = [ + "bincode 1.3.3", + "pin-project-lite", + "rmp-serde", + "serde", + "serde_json", + "sled", + "thiserror 1.0.69", + "toml", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint 0.4.6", + "thiserror 1.0.69", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "more-asserts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" + +[[package]] +name = "mpl-bubblegum" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9eff5ae5cafd1acdf7e7c93359da1eec91dcaede318470d9f68b78e8b7469f4" +dependencies = [ + "borsh 0.10.4", + "kaigan", + "num-derive 0.3.3", + "num-traits", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "mpl-candy-guard" +version = "3.0.0" +source = "git+https://github.com/space-operator/mpl-candy-machine?rev=6096215a779702fb7954103e0d3199d89e120e6f#6096215a779702fb7954103e0d3199d89e120e6f" +dependencies = [ + "anchor-lang 0.30.1", + "arrayref", + "mpl-candy-guard-derive", + "mpl-candy-machine-core", + "mpl-token-metadata 3.2.3", + "solana-gateway", + "solana-program 1.18.26", + "spl-associated-token-account 2.3.0", + "spl-token", + "spl-token-2022 3.0.4", +] + +[[package]] +name = "mpl-candy-guard-derive" +version = "0.2.0" +source = "git+https://github.com/space-operator/mpl-candy-machine?rev=6096215a779702fb7954103e0d3199d89e120e6f#6096215a779702fb7954103e0d3199d89e120e6f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpl-candy-machine-core" +version = "3.0.1" +source = "git+https://github.com/space-operator/mpl-candy-machine?rev=6096215a779702fb7954103e0d3199d89e120e6f#6096215a779702fb7954103e0d3199d89e120e6f" +dependencies = [ + "anchor-lang 0.30.1", + "arrayref", + "mpl-token-metadata 3.2.3", + "mpl-utils", + "solana-program 1.18.26", + "spl-associated-token-account 2.3.0", + "spl-token", +] + +[[package]] +name = "mpl-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299e6db3f7d9ab8fba383e3c6e88f8869063e694cbeb56da8f1bdd355e255f8f" +dependencies = [ + "base64 0.22.1", + "borsh 0.10.4", + "modular-bitfield", + "num-derive 0.3.3", + "num-traits", + "rmp-serde", + "serde", + "serde_json", + "serde_with 3.12.0", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "mpl-core-candy-guard" +version = "0.2.1" +source = "git+https://github.com/space-operator/mpl-core-candy-machine?rev=e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226#e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226" +dependencies = [ + "anchor-lang 0.30.1", + "arrayref", + "mpl-core", + "mpl-core-candy-guard-derive", + "mpl-core-candy-machine-core", + "mpl-token-metadata 3.2.3", + "regex-lite", + "solana-gateway", + "solana-program 1.18.26", + "spl-associated-token-account 2.3.0", + "spl-token", + "spl-token-2022 3.0.4", +] + +[[package]] +name = "mpl-core-candy-guard-derive" +version = "0.2.1" +source = "git+https://github.com/space-operator/mpl-core-candy-machine?rev=e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226#e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpl-core-candy-machine-core" +version = "0.2.1" +source = "git+https://github.com/space-operator/mpl-core-candy-machine?rev=e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226#e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226" +dependencies = [ + "anchor-lang 0.30.1", + "arrayref", + "mpl-core", + "mpl-token-metadata 3.2.3", + "mpl-utils", + "solana-program 1.18.26", + "spl-associated-token-account 2.3.0", + "spl-token", +] + +[[package]] +name = "mpl-token-auth-rules" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8637dd3d13d045a43a2410dbb57f0257d506838aa71461faf0eaf2806e6d5071" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "mpl-token-metadata-context-derive", + "num-derive 0.3.3", + "num-traits", + "rmp-serde", + "serde", + "shank", + "solana-program 1.18.26", + "solana-zk-token-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "mpl-token-metadata" +version = "3.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8ee05284d79b367ae8966d558e1a305a781fc80c9df51f37775169117ba64f" +dependencies = [ + "borsh 0.10.4", + "num-derive 0.3.3", + "num-traits", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "mpl-token-metadata" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf0f61b553e424a6234af1268456972ee66c2222e1da89079242251fa7479e5" +dependencies = [ + "borsh 0.10.4", + "num-derive 0.3.3", + "num-traits", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "mpl-token-metadata-context-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12989bc45715b0ee91944855130131479f9c772e198a910c3eb0ea327d5bffc3" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpl-utils" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b753edea723ac98ea87dea53932c1a9ba6990776a45e9230d6703682a37621b" +dependencies = [ + "arrayref", + "solana-program 1.18.26", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive 0.7.3", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.7.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parity-scale-codec" +version = "3.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.8", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pdg-common" +version = "0.0.0" +dependencies = [ + "derive_more", + "indexmap 2.7.0", + "rand 0.8.5", + "serde", + "serde_json", + "serde_repr", + "strum 0.24.1", + "thiserror 1.0.69", + "uuid 1.11.1", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "percentage" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.7.0", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pipe" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7b8f27da217eb966df4c58d4159ea939431950ca03cf782c22bd7c5c1d8d75" +dependencies = [ + "crossbeam-channel", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.9", + "spki 0.7.3", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash 0.4.0", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "postgres-protocol" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac 0.12.1", + "md-5", + "memchr", + "rand 0.8.5", + "sha2 0.10.8", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator", + "postgres-protocol", + "serde", + "serde_json", + "uuid 1.11.1", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] + +[[package]] +name = "primitive-types" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "primitive-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "pyth-sdk" +version = "0.8.0" +source = "git+https://github.com/space-operator/pyth-sdk-rs?rev=84cc6fe07acbb8a81b216228be244bf039621560#84cc6fe07acbb8a81b216228be244bf039621560" +dependencies = [ + "borsh 0.10.4", + "borsh-derive 0.10.4", + "getrandom 0.2.15", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-solana" +version = "0.10.2" +source = "git+https://github.com/space-operator/pyth-sdk-rs?rev=84cc6fe07acbb8a81b216228be244bf039621560#84cc6fe07acbb8a81b216228be244bf039621560" +dependencies = [ + "borsh 0.10.4", + "borsh-derive 0.10.4", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "pyth-sdk", + "serde", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "qualifier_attr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto 0.10.6", + "quinn-udp 0.4.1", + "rustc-hash 1.1.0", + "rustls 0.21.12", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto 0.11.9", + "quinn-udp 0.5.9", + "rustc-hash 2.1.0", + "rustls 0.23.21", + "socket2", + "thiserror 2.0.11", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring 0.16.20", + "rustc-hash 1.1.0", + "rustls 0.21.12", + "rustls-native-certs", + "slab", + "thiserror 1.0.69", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand 0.8.5", + "ring 0.17.8", + "rustc-hash 2.1.0", + "rustls 0.23.21", + "rustls-pki-types", + "slab", + "thiserror 2.0.11", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "quinn-udp" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" +dependencies = [ + "pem", + "ring 0.16.20", + "time 0.3.37", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.7.0", +] + +[[package]] +name = "regalloc2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43a209257d978ef079f3d446331d0f1794f5e0fc19b306a199983857833a779" +dependencies = [ + "fxhash", + "log", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regalloc2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300d4fbfb40c1c66a78ba3ddd41c1110247cf52f97b87d0f2fc9209bd49b030c" +dependencies = [ + "fxhash", + "log", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "async-compression", + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tokio-util 0.7.13", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-rustls 0.27.5", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn 0.11.6", + "rustls 0.23.21", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.1", + "tokio-util 0.7.13", + "tower 0.5.2", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.7", + "windows-registry", +] + +[[package]] +name = "rhai" +version = "1.17.2" +source = "git+https://github.com/space-operator/rhai?rev=b39d2bfb#b39d2bfb1b74127783baddbebedc612311c86c7b" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.7.0", + "indexmap 2.7.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "rust_decimal", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai-rand" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4314e7e2a1f5d5de224ae3bc9ce2af4c0146d07d3c3aadf9840f08d80ef28800" +dependencies = [ + "rand 0.8.5", + "rhai", + "rust_decimal", + "serde", + "serde_json", +] + +[[package]] +name = "rhai-script" +version = "0.0.0" +dependencies = [ + "anyhow", + "bs58 0.4.0", + "chrono", + "flow-lib", + "rhai", + "rhai-rand", + "rust_decimal", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "rhai_codegen" +version = "2.0.0" +source = "git+https://github.com/space-operator/rhai?rev=b39d2bfb#b39d2bfb1b74127783baddbebedc612311c86c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid 1.11.1", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh 1.5.3", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da991f231869f34268415a49724c6578e740ad697ba0999199d6f22b3949332c" +dependencies = [ + "quote", + "rust_decimal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.24", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags 2.7.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "log", + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.96", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.7.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +dependencies = [ + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +dependencies = [ + "indexmap 2.7.0", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "serde", + "serde_with_macros 2.3.3", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros 3.12.0", + "time 0.3.37", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_wormhole" +version = "0.1.0" +source = "git+https://github.com/space-operator/wormhole?rev=b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1#b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1" +dependencies = [ + "base64 0.13.1", + "itoa", + "serde", + "serde_bytes", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_wormhole" +version = "0.1.0" +source = "git+https://github.com/space-operator/wormhole?rev=ee7d0765f9a3b4deb7450c9ff05e920b764ea486#ee7d0765f9a3b4deb7450c9ff05e920b764ea486" +dependencies = [ + "base64 0.13.1", + "itoa", + "serde", + "serde_bytes", + "thiserror 1.0.69", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shank" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c9395612d493b69a522725eef78a095f199d43eeb847f4a4b77ec0cacab535" +dependencies = [ + "shank_macro", +] + +[[package]] +name = "shank_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abef069c02e15f62233679b1e71f3152fac10f90b3ff89ebbad6a25b7497754" +dependencies = [ + "proc-macro2", + "quote", + "shank_macro_impl", + "shank_render", + "syn 1.0.109", +] + +[[package]] +name = "shank_macro_impl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d3d92bfcc6e08f882f2264d774d1a2f46dc36122adc1b76416ba6405a29a9c" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "shank_render" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2ea9c6dd95ea311b3b81e63cf4e9c808ed04b098819e6d2c4b1a467d587203" +dependencies = [ + "proc-macro2", + "quote", + "shank_macro_impl", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "solana-account-decoder" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b109fd3a106e079005167e5b0e6f6d2c88bbedec32530837b584791a8b5abf36" +dependencies = [ + "Inflector", + "base64 0.21.7", + "bincode 1.3.3", + "bs58 0.4.0", + "bv", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-config-program", + "solana-sdk", + "spl-token", + "spl-token-2022 1.0.0", + "spl-token-group-interface 0.1.0", + "spl-token-metadata-interface 0.2.0", + "thiserror 1.0.69", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "solana-account-info" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e053b991f91fd274df53e070c77a0a6a33681a5102c6421a0ca9ffaa0040368a" +dependencies = [ + "bincode 1.3.3", + "serde", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "966dce88672728380c476d5d3e54c02025875100b8246db05669961806c9575e" +dependencies = [ + "parking_lot 0.12.3", +] + +[[package]] +name = "solana-bincode" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117b9646b1e9e6c4b48f363ad4c5af25c4ab35754ff307714e5fec2c3c4bb6b" +dependencies = [ + "bincode 1.3.3", + "serde", + "solana-instruction", +] + +[[package]] +name = "solana-borsh" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c55b83c305eac62095b6f24ea2ae729f17de47e5a4e866ee4ddd0dc501b351e" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.3", +] + +[[package]] +name = "solana-clap-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074ef478856a45d5627270fbc6b331f91de9aae7128242d9e423931013fb8a2a" +dependencies = [ + "chrono", + "clap 2.34.0", + "rpassword", + "solana-remote-wallet", + "solana-sdk", + "thiserror 1.0.69", + "tiny-bip39", + "uriparse", + "url", +] + +[[package]] +name = "solana-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a9f32c42402c4b9484d5868ac74b7e0a746e3905d8bfd756e1203e50cbb87e" +dependencies = [ + "async-trait", + "bincode 1.3.3", + "dashmap", + "futures", + "futures-util", + "indexmap 2.7.0", + "indicatif", + "log", + "quinn 0.10.2", + "rayon", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-pubsub-client", + "solana-quic-client", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-sdk", + "solana-streamer", + "solana-thin-client", + "solana-tpu-client", + "solana-udp-client", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "solana-clock" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2387b936492cab0649c2a3e3fcfb282077029b533fa8454c88c41dff3bc2552" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-macro 2.1.8", + "solana-sysvar-id", +] + +[[package]] +name = "solana-config-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d75b803860c0098e021a26f0624129007c15badd5b0bc2fbd9f0e1a73060d3b" +dependencies = [ + "bincode 1.3.3", + "chrono", + "serde", + "serde_derive", + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-connection-cache" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9306ede13e8ceeab8a096bcf5fa7126731e44c201ca1721ea3c38d89bcd4111" +dependencies = [ + "async-trait", + "bincode 1.3.3", + "crossbeam-channel", + "futures-util", + "indexmap 2.7.0", + "log", + "rand 0.8.5", + "rayon", + "rcgen", + "solana-measure", + "solana-metrics", + "solana-sdk", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "solana-cpi" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bae0591481827ac9cfce5573aad2918bb01f91289b811ea531df4fcb73d136" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8880dc18fb97c6205214d1f3ce2f1152e997ecc6f6da4bb458fbf6e6207a0693" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6452c4a8fc77cc60ad2934b19f2d75691067f17355b34462d52285395c1c99db" + +[[package]] +name = "solana-epoch-schedule" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e783a735416c534228f24f18f18ade0b189c2c8a93be486c9a26bd314517d93" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-macro 2.1.8", + "solana-sysvar-id", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fa18582732d94369263c42eeee967ff919e99b9b15ba747fb7534aa24fbbc0" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-frozen-abi" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ab2c30c15311b511c0d1151e4ab6bc9a3e080a37e7c6e7c2d96f5784cf9434" +dependencies = [ + "block-buffer 0.10.4", + "bs58 0.4.0", + "bv", + "either", + "generic-array", + "im", + "lazy_static", + "log", + "memmap2", + "rustc_version 0.4.1", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.10.8", + "solana-frozen-abi-macro", + "subtle", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c142f779c3633ac83c84d04ff06c70e1f558c876f13358bed77ba629c7417932" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.96", +] + +[[package]] +name = "solana-gateway" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a11e5afc65ef1845214987e9724aa6406916d4a2d67ed66e4583cb75368bb645" +dependencies = [ + "borsh 1.5.3", + "num-derive 0.4.2", + "num-traits", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-hash" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e35f984e3d60a58184743446250cf724afb34ed65f794da0dc4b462f9c1929" +dependencies = [ + "borsh 1.5.3", + "bs58 0.5.1", + "bytemuck", + "bytemuck_derive", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fc69f7f75df0b11e99c03393b24a7443aec0430518054de14715c59cfa716d" +dependencies = [ + "bincode 1.3.3", + "borsh 1.5.3", + "getrandom 0.2.15", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee98cc25000ee8bab1a4f63c7516d9521bc8a9747d8287ebb05e5d9b1d32ee1" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-macro 2.1.8", + "solana-sysvar-id", +] + +[[package]] +name = "solana-logger" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121d36ffb3c6b958763312cbc697fbccba46ee837d3a0aa4fc0e90fcb3b884f3" +dependencies = [ + "env_logger", + "lazy_static", + "log", +] + +[[package]] +name = "solana-measure" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a7f9cdc9d9d37a3d5651b2fe7ec9d433c2a3470b9f35897e373b421f0737" +dependencies = [ + "log", + "solana-sdk", +] + +[[package]] +name = "solana-metrics" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e36052aff6be1536bdf6f737c6e69aca9dbb6a2f3f582e14ecb0ddc0cd66ce" +dependencies = [ + "crossbeam-channel", + "gethostname", + "lazy_static", + "log", + "reqwest 0.11.27", + "solana-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-msg" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac7a109b0c7a0ed26c1fbf3b0fec8809b5d4c74b5d597f0252d45255fd0d309" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-native-token" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7246817ae265f5a67be25f32ee52267f1c2fe29767ab601ef03c5086bfc64992" + +[[package]] +name = "solana-net-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1f5c6be9c5b272866673741e1ebc64b2ea2118e5c6301babbce526fdfb15f4" +dependencies = [ + "bincode 1.3.3", + "clap 3.2.25", + "crossbeam-channel", + "log", + "nix", + "rand 0.8.5", + "serde", + "serde_derive", + "socket2", + "solana-logger", + "solana-sdk", + "solana-version", + "tokio", + "url", +] + +[[package]] +name = "solana-perf" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28acaf22477566a0fbddd67249ea5d859b39bacdb624aff3fadd3c5745e2643c" +dependencies = [ + "ahash 0.8.11", + "bincode 1.3.3", + "bv", + "caps", + "curve25519-dalek 3.2.1", + "dlopen2", + "fnv", + "lazy_static", + "libc", + "log", + "nix", + "rand 0.8.5", + "rayon", + "rustc_version 0.4.1", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-vote-program", +] + +[[package]] +name = "solana-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10f4588cefd716b24a1a40dd32c278e43a560ab8ce4de6b5805c9d113afdfa1" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "base64 0.21.7", + "bincode 1.3.3", + "bitflags 2.7.0", + "blake3", + "borsh 0.10.4", + "borsh 0.9.3", + "borsh 1.5.3", + "bs58 0.4.0", + "bv", + "bytemuck", + "cc", + "console_error_panic_hook", + "console_log", + "curve25519-dalek 3.2.1", + "getrandom 0.2.15", + "itertools 0.10.5", + "js-sys", + "lazy_static", + "libc", + "libsecp256k1", + "light-poseidon", + "log", + "memoffset 0.9.1", + "num-bigint 0.4.6", + "num-derive 0.4.2", + "num-traits", + "parking_lot 0.12.3", + "rand 0.8.5", + "rustc_version 0.4.1", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.8", + "sha3 0.10.8", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro 1.18.26", + "thiserror 1.0.69", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-program" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05f5ffadb039285ee82efd9a593e0873220f840f0eac7069d962f9eb29a407" +dependencies = [ + "base64 0.22.1", + "bincode 1.3.3", + "bitflags 2.7.0", + "blake3", + "borsh 0.10.4", + "borsh 1.5.3", + "bs58 0.5.1", + "bv", + "bytemuck", + "bytemuck_derive", + "console_error_panic_hook", + "console_log", + "curve25519-dalek 4.1.3", + "five8_const", + "getrandom 0.2.15", + "js-sys", + "lazy_static", + "log", + "memoffset 0.9.1", + "num-bigint 0.4.6", + "num-derive 0.4.2", + "num-traits", + "parking_lot 0.12.3", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.10.8", + "sha3 0.10.8", + "solana-account-info", + "solana-atomic-u64", + "solana-bincode", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-last-restart-slot", + "solana-msg", + "solana-native-token", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-macro 2.1.8", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-sysvar-id", + "solana-transaction-error", + "thiserror 1.0.69", + "wasm-bindgen", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f6148e740c6deed55fe343355f0cb3ec158d221e11aa8bb93a392fa62c4137" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87e99e4299728f450194b6adf946dde512d79d82275b1c73f6faea7e9075cef" +dependencies = [ + "borsh 1.5.3", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3691cdd84c0a4753b484f468aac19e0943fab1e71705b21d00d561ac6eea6449" +dependencies = [ + "num-traits", + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99a3e016363a95cdbe23aaa2a68578ffa2ce8e37c4a642962201af6376ffc37" + +[[package]] +name = "solana-program-pack" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eba980dec9d5403ea299a3cdf27cd794e6b1a188acc8c5e3ae7d067b629eb24" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-program-runtime" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf0c3eab2a80f514289af1f422c121defb030937643c43b117959d6f1932fb5" +dependencies = [ + "base64 0.21.7", + "bincode 1.3.3", + "eager", + "enum-iterator 1.5.0", + "itertools 0.10.5", + "libc", + "log", + "num-derive 0.4.2", + "num-traits", + "percentage", + "rand 0.8.5", + "rustc_version 0.4.1", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-measure", + "solana-metrics", + "solana-sdk", + "solana_rbpf", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-pubkey" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dba2b19db8b73ab96b309b6d2a9f26386e45e2af3618a27b92389da9a3df1f1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.3", + "bs58 0.5.1", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "five8_const", + "getrandom 0.2.15", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-pubsub-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b064e76909d33821b80fdd826e6757251934a52958220c92639f634bea90366d" +dependencies = [ + "crossbeam-channel", + "futures-util", + "log", + "reqwest 0.11.27", + "semver 1.0.24", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-rpc-client-api", + "solana-sdk", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.20.1", + "tungstenite 0.20.1", + "url", +] + +[[package]] +name = "solana-quic-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a90e40ee593f6e9ddd722d296df56743514ae804975a76d47e7afed4e3da244" +dependencies = [ + "async-mutex", + "async-trait", + "futures", + "itertools 0.10.5", + "lazy_static", + "log", + "quinn 0.10.2", + "quinn-proto 0.10.6", + "rcgen", + "rustls 0.21.12", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-rpc-client-api", + "solana-sdk", + "solana-streamer", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66468f9c014992167de10cc68aad6ac8919a8c8ff428dc88c0d2b4da8c02b8b7" +dependencies = [ + "lazy_static", + "num_cpus", +] + +[[package]] +name = "solana-remote-wallet" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c191019f4d4f84281a6d0dd9a43181146b33019627fc394e42e08ade8976b431" +dependencies = [ + "console", + "dialoguer", + "log", + "num-derive 0.4.2", + "num-traits", + "parking_lot 0.12.3", + "qstring", + "semver 1.0.24", + "solana-sdk", + "thiserror 1.0.69", + "uriparse", +] + +[[package]] +name = "solana-rent" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138b60a6683d14d63b4cee532d50afcb54999679b5c53013969fd51977455e14" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-macro 2.1.8", + "solana-sysvar-id", +] + +[[package]] +name = "solana-rpc-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ed4628e338077c195ddbf790693d410123d17dec0a319b5accb4aaee3fb15c" +dependencies = [ + "async-trait", + "base64 0.21.7", + "bincode 1.3.3", + "bs58 0.4.0", + "indicatif", + "log", + "reqwest 0.11.27", + "semver 1.0.24", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-rpc-client-api", + "solana-sdk", + "solana-transaction-status", + "solana-version", + "solana-vote-program", + "tokio", +] + +[[package]] +name = "solana-rpc-client-api" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c913551faa4a1ae4bbfef6af19f3a5cf847285c05b4409e37c8993b3444229" +dependencies = [ + "base64 0.21.7", + "bs58 0.4.0", + "jsonrpc-core", + "reqwest 0.11.27", + "semver 1.0.24", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-sdk", + "solana-transaction-status", + "solana-version", + "spl-token-2022 1.0.0", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a47b6bb1834e6141a799db62bbdcf80d17a7d58d7bc1684c614e01a7293d7cf" +dependencies = [ + "clap 2.34.0", + "solana-clap-utils", + "solana-rpc-client", + "solana-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-sanitize" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f71b885b953e9157b66eaba9a34507f2f840712ef54f483725ba510ee1bd89" + +[[package]] +name = "solana-sdk" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580ad66c2f7a4c3cb3244fe21440546bd500f5ecb955ad9826e92a78dded8009" +dependencies = [ + "assert_matches", + "base64 0.21.7", + "bincode 1.3.3", + "bitflags 2.7.0", + "borsh 1.5.3", + "bs58 0.4.0", + "bytemuck", + "byteorder", + "chrono", + "derivation-path", + "digest 0.10.7", + "ed25519-dalek 1.0.1", + "ed25519-dalek-bip32", + "generic-array", + "hmac 0.12.1", + "itertools 0.10.5", + "js-sys", + "lazy_static", + "libsecp256k1", + "log", + "memmap2", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.3", + "pbkdf2 0.11.0", + "qstring", + "qualifier_attr", + "rand 0.7.3", + "rand 0.8.5", + "rustc_version 0.4.1", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "serde_with 2.3.3", + "sha2 0.10.8", + "sha3 0.10.8", + "siphasher 0.3.11", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-program 1.18.26", + "solana-sdk-macro 1.18.26", + "thiserror 1.0.69", + "uriparse", + "wasm-bindgen", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b75d0f193a27719257af19144fdaebec0415d1c9e9226ae4bd29b791be5e9bd" +dependencies = [ + "bs58 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f0b358f336ceac3827881915e5293f121c023cbd2150115046356c66898cb8" +dependencies = [ + "bs58 0.5.1", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "solana-secp256k1-recover" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460c2e36586bcce843cdeaaf2364f3db7fbd9f266325e93d5e9af33f2605dd7d" +dependencies = [ + "libsecp256k1", + "solana-define-syscall", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + +[[package]] +name = "solana-serde-varint" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98449030e53dcc2c4f160acab99b2bdb3e24ea8bff8ca6e71a6e539a54bf3d7" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d659aac218580fc3fb3e8350669db9bb01bc1bc849c90f0741cbfccb6663eb94" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db90ad6643d4d626f923159eaa876000c09f8c2e9aa7ff59b803e8328712582" +dependencies = [ + "sha2 0.10.8", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-short-vec" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f7de721a6c50cb3a41e027a623496be39e45c452fbf897f657cd1f2f67dbbd" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3840867aa6d0fac65d3a4c1f14fff650a8e148732a16c06ebd8a2389d79d4745" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "101583a12fcce9b52f845b3c773f4ae6c3f4ca6a46177dadbd83e276baf82326" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b923e1c9e42b6c98b1786ca003af6a0366932f08d63432e984fcc394b7b5e" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-streamer" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8476e41ad94fe492e8c06697ee35912cf3080aae0c9e9ac6430835256ccf056" +dependencies = [ + "async-channel", + "bytes", + "crossbeam-channel", + "futures-util", + "histogram", + "indexmap 2.7.0", + "itertools 0.10.5", + "libc", + "log", + "nix", + "pem", + "percentage", + "pkcs8 0.8.0", + "quinn 0.10.2", + "quinn-proto 0.10.6", + "rand 0.8.5", + "rcgen", + "rustls 0.21.12", + "smallvec", + "solana-metrics", + "solana-perf", + "solana-sdk", + "thiserror 1.0.69", + "tokio", + "x509-parser", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59351de877a7cf0cea0e436424ecf4ea0c08c59ff01ef0575436972b920b818c" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-thin-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c02245d0d232430e79dc0d624aa42d50006097c3aec99ac82ac299eaa3a73f" +dependencies = [ + "bincode 1.3.3", + "log", + "rayon", + "solana-connection-cache", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", +] + +[[package]] +name = "solana-tpu-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67251506ed03de15f1347b46636b45c47da6be75015b4a13f0620b21beb00566" +dependencies = [ + "async-trait", + "bincode 1.3.3", + "futures-util", + "indexmap 2.7.0", + "indicatif", + "log", + "rayon", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-pubsub-client", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "solana-transaction-error" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c3d2147cfaad2a5518b8e15621008699e28d32d6233cd7a6b27a506e01f1515" +dependencies = [ + "solana-instruction", + "solana-sanitize", +] + +[[package]] +name = "solana-transaction-status" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3d36db1b2ab2801afd5482aad9fb15ed7959f774c81a77299fdd0ddcf839d4" +dependencies = [ + "Inflector", + "base64 0.21.7", + "bincode 1.3.3", + "borsh 0.10.4", + "bs58 0.4.0", + "lazy_static", + "log", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-sdk", + "spl-associated-token-account 2.3.0", + "spl-memo", + "spl-token", + "spl-token-2022 1.0.0", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-udp-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a754a3c2265eb02e0c35aeaca96643951f03cee6b376afe12e0cf8860ffccd1" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-net-utils", + "solana-sdk", + "solana-streamer", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "solana-version" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44776bd685cc02e67ba264384acc12ef2931d01d1a9f851cb8cdbd3ce455b9e" +dependencies = [ + "log", + "rustc_version 0.4.1", + "semver 1.0.24", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", +] + +[[package]] +name = "solana-vote-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25810970c91feb579bd3f67dca215fce971522e42bfd59696af89c5dfebd997c" +dependencies = [ + "bincode 1.3.3", + "log", + "num-derive 0.4.2", + "num-traits", + "rustc_version 0.4.1", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-program 1.18.26", + "solana-program-runtime", + "solana-sdk", + "thiserror 1.0.69", +] + +[[package]] +name = "solana-zk-token-sdk" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbdf4249b6dfcbba7d84e2b53313698043f60f8e22ce48286e6fbe8a17c8d16" +dependencies = [ + "aes-gcm-siv", + "base64 0.21.7", + "bincode 1.3.3", + "bytemuck", + "byteorder", + "curve25519-dalek 3.2.1", + "getrandom 0.1.16", + "itertools 0.10.5", + "lazy_static", + "merlin", + "num-derive 0.4.2", + "num-traits", + "rand 0.7.3", + "serde", + "serde_json", + "sha3 0.9.1", + "solana-program 1.18.26", + "solana-sdk", + "subtle", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "solana_rbpf" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5d083187e3b3f453e140f292c09186881da8a02a7b5e27f645ee26de3d9cc5" +dependencies = [ + "byteorder", + "combine", + "goblin", + "hash32", + "libc", + "log", + "rand 0.8.5", + "rustc-demangle", + "scroll", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "space-lib" +version = "0.5.1" +dependencies = [ + "rmp-serde", + "serde", + "serde_json", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "space-wasm" +version = "0.0.0" +dependencies = [ + "anyhow", + "byteorder", + "cranelift-codegen 0.91.1", + "pretty_assertions", + "rmp-serde", + "serde", + "serde_json", + "space-lib", + "ureq", + "wasmer", + "wasmer-cache", + "wasmer-wasi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.9", +] + +[[package]] +name = "spl-account-compression" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8314ec6ae26084ec7c6c0802c3dc173ee86aee5f5d5026a3f82c52cfe1c07" +dependencies = [ + "anchor-lang 0.29.0", + "bytemuck", + "solana-program 2.1.8", + "spl-concurrent-merkle-tree", + "spl-noop", +] + +[[package]] +name = "spl-associated-token-account" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" +dependencies = [ + "assert_matches", + "borsh 0.10.4", + "num-derive 0.4.2", + "num-traits", + "solana-program 1.18.26", + "spl-token", + "spl-token-2022 1.0.0", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-associated-token-account" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143109d789171379e6143ef23191786dfaac54289ad6e7917cfb26b36c432b10" +dependencies = [ + "assert_matches", + "borsh 1.5.3", + "num-derive 0.4.2", + "num-traits", + "solana-program 1.18.26", + "spl-token", + "spl-token-2022 3.0.4", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-concurrent-merkle-tree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14033366e14117679851c7759c3d66c6430a495f0523bd88076d3a275828931" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-discriminator" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce5d563b58ef1bb2cdbbfe0dfb9ffdc24903b10ae6a4df2d8f425ece375033f" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator-derive 0.1.2", +] + +[[package]] +name = "spl-discriminator" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210101376962bb22bb13be6daea34656ea1cbc248fce2164b146e39203b55e03" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator-derive 0.2.0", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fd7858fc4ff8fb0e34090e41d7eb06a823e1057945c26d480bfc21d2338a93" +dependencies = [ + "quote", + "spl-discriminator-syn 0.1.2", + "syn 2.0.96", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" +dependencies = [ + "quote", + "spl-discriminator-syn 0.2.0", + "syn 2.0.96", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fea7be851bd98d10721782ea958097c03a0c2a07d8d4997041d0ece6319a63" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-memo" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f180b03318c3dbab3ef4e1e4d46d5211ae3c780940dd0a28695aba4b59a75a" +dependencies = [ + "solana-program 1.18.26", +] + +[[package]] +name = "spl-noop" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd67ea3d0070a12ff141f5da46f9695f49384a03bce1203a5608f5739437950" +dependencies = [ + "solana-program 1.18.26", +] + +[[package]] +name = "spl-pod" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2881dddfca792737c0706fa0175345ab282b1b0879c7d877bad129645737c079" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "solana-program 1.18.26", + "solana-zk-token-sdk", + "spl-program-error 0.3.0", +] + +[[package]] +name = "spl-pod" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52d84c55efeef8edcc226743dc089d7e3888b8e3474569aa3eff152b37b9996" +dependencies = [ + "base64 0.22.1", + "borsh 1.5.3", + "bytemuck", + "serde", + "solana-program 1.18.26", + "solana-zk-token-sdk", + "spl-program-error 0.4.4", +] + +[[package]] +name = "spl-program-error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249e0318493b6bcf27ae9902600566c689b7dfba9f1bdff5893e92253374e78c" +dependencies = [ + "num-derive 0.4.2", + "num-traits", + "solana-program 1.18.26", + "spl-program-error-derive 0.3.2", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45a49acb925db68aa501b926096b2164adbdcade7a0c24152af9f0742d0a602" +dependencies = [ + "num-derive 0.4.2", + "num-traits", + "solana-program 1.18.26", + "spl-program-error-derive 0.4.1", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1845dfe71fd68f70382232742e758557afe973ae19e6c06807b2c30f5d5cb474" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.96", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615d381f48ddd2bb3c57c7f7fb207591a2a05054639b18a62e785117dd7a8683" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", + "spl-type-length-value 0.3.0", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab8edfd37be5fa17c9e42c1bff86abbbaf0494b031b37957f2728ad2ff842ba" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.2.5", + "spl-pod 0.2.5", + "spl-program-error 0.4.4", + "spl-type-length-value 0.4.6", +] + +[[package]] +name = "spl-token" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08459ba1b8f7c1020b4582c4edf0f5c7511a5e099a7a97570c9698d4f2337060" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.3.3", + "num-traits", + "num_enum 0.6.1", + "solana-program 1.18.26", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-2022" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.3", + "solana-program 1.18.26", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod 0.1.0", + "spl-token", + "spl-token-group-interface 0.1.0", + "spl-token-metadata-interface 0.2.0", + "spl-transfer-hook-interface 0.4.1", + "spl-type-length-value 0.3.0", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-2022" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d1b2851964e257187c0bca43a0de38d0af59192479ca01ac3e2b58b1bd95a" +dependencies = [ + "arrayref", + "base64 0.22.1", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.3", + "serde", + "serde_with 3.12.0", + "solana-program 1.18.26", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod 0.2.5", + "spl-token", + "spl-token-group-interface 0.2.5", + "spl-token-metadata-interface 0.3.5", + "spl-transfer-hook-interface 0.6.5", + "spl-type-length-value 0.4.6", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "014817d6324b1e20c4bbc883e8ee30a5faa13e59d91d1b2b95df98b920150c17" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.2.5", + "spl-pod 0.2.5", + "spl-program-error 0.4.4", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c16ce3ba6979645fb7627aa1e435576172dd63088dc7848cb09aa331fa1fe4f" +dependencies = [ + "borsh 0.10.4", + "solana-program 1.18.26", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", + "spl-type-length-value 0.3.0", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3da00495b602ebcf5d8ba8b3ecff1ee454ce4c125c9077747be49c2d62335ba" +dependencies = [ + "borsh 1.5.3", + "solana-program 1.18.26", + "spl-discriminator 0.2.5", + "spl-pod 0.2.5", + "spl-program-error 0.4.4", + "spl-type-length-value 0.4.6", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", + "spl-tlv-account-resolution 0.5.1", + "spl-type-length-value 0.3.0", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b5c08a89838e5a2931f79b17f611857f281a14a2100968a3ccef352cb7414b" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.2.5", + "spl-pod 0.2.5", + "spl-program-error 0.4.4", + "spl-tlv-account-resolution 0.6.5", + "spl-type-length-value 0.4.6", +] + +[[package]] +name = "spl-type-length-value" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a468e6f6371f9c69aae760186ea9f1a01c2908351b06a5e0026d21cfc4d7ecac" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", +] + +[[package]] +name = "spl-type-length-value" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c872f93d0600e743116501eba2d53460e73a12c9a496875a42a7d70e034fe06d" +dependencies = [ + "bytemuck", + "solana-program 1.18.26", + "spl-discriminator 0.2.5", + "spl-pod 0.2.5", + "spl-program-error 0.4.4", +] + +[[package]] +name = "spo-helius" +version = "0.1.0" +dependencies = [ + "anyhow", + "bs58 0.4.0", + "reqwest 0.12.12", + "serde", + "serde_json", + "serde_with 3.12.0", + "tracing", +] + +[[package]] +name = "spo-postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2f6cfdcd676f2b74c661287ff37df9e947f14259f2eed9519fc43fd4faa8c" +dependencies = [ + "reqwest 0.12.12", + "serde", +] + +[[package]] +name = "srpc" +version = "0.0.0" +dependencies = [ + "actix", + "actix-web", + "actix-web-actors", + "criterion", + "futures-channel", + "futures-util", + "hashbrown 0.14.5", + "reqwest 0.12.12", + "serde", + "serde_json", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tower 0.4.13", + "tracing", + "tungstenite 0.24.0", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "struct-convert" +version = "1.4.0" +source = "git+https://github.com/juchiast/struct-convert?rev=83b132915519515e364ce612ef2ac92441dc7bc9#83b132915519515e364ce612ef2ac92441dc7bc9" +dependencies = [ + "anyhow", + "darling 0.14.4", + "derivative", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 1.0.109", + "time 0.3.37", +] + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.7.0", + "core-foundation", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.2.15", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros 0.2.19", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash 1.1.0", + "sha2 0.9.9", + "thiserror 1.0.69", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot 0.12.3", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.3", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2", + "tokio", + "tokio-util 0.7.13", + "whoami", +] + +[[package]] +name = "tokio-postgres-rustls" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606f2b73660439474394432239c82249c0d45eb5f23d91f401be1e33590444a7" +dependencies = [ + "futures", + "ring 0.16.20", + "rustls 0.20.9", + "tokio", + "tokio-postgres", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.21", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", + "webpki-roots 0.25.4", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.21", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tungstenite 0.24.0", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.7.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.7.0", + "toml_datetime", + "winnow 0.6.24", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util 0.7.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.12", + "sha1 0.10.6", + "thiserror 1.0.69", + "url", + "utf-8", + "webpki-roots 0.24.0", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.23.21", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cea224ddd4282dfc40d1edabbd0c020a12e946e3a48e2c2b8f6ff167ad29fe" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls 0.23.21", + "rustls-pki-types", + "url", + "webpki-roots 0.26.7", +] + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utils" +version = "0.0.0" +dependencies = [ + "actix", + "base64 0.21.7", + "bs58 0.4.0", + "bytes", + "futures-util", + "hashbrown 0.14.5", + "serde", + "thiserror 1.0.69", + "tower 0.4.13", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "uuid" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-downcast" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dac026d43bcca6e7ce1c0956ba68f59edf6403e8e930a5d891be72c31a44340" +dependencies = [ + "js-sys", + "once_cell", + "wasm-bindgen", + "wasm-bindgen-downcast-macros", +] + +[[package]] +name = "wasm-bindgen-downcast-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5020cfa87c7cecefef118055d44e3c1fc122c7ec25701d528ee458a0b45f38f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmer" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840af6d21701220cb805dc7201af301cb99e9b4f646f48a41befbc1d949f0f90" +dependencies = [ + "bytes", + "cfg-if", + "indexmap 1.9.3", + "js-sys", + "more-asserts", + "serde", + "serde-wasm-bindgen", + "target-lexicon", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-downcast", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-compiler-singlepass", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "winapi", +] + +[[package]] +name = "wasmer-cache" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08890d685fc8ab91936f1463707adcd21e886b24383487fae7d19fc681c31358" +dependencies = [ + "blake3", + "hex", + "thiserror 1.0.69", + "wasmer", +] + +[[package]] +name = "wasmer-compiler" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86fab98beaaace77380cb04e681773739473860d1b8499ea6b14f920923e0c5" +dependencies = [ + "backtrace", + "cfg-if", + "enum-iterator 0.7.0", + "enumset", + "lazy_static", + "leb128", + "memmap2", + "more-asserts", + "region", + "rustc-demangle", + "smallvec", + "thiserror 1.0.69", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "winapi", +] + +[[package]] +name = "wasmer-compiler-cranelift" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015eef629fc84889540dc1686bd7fa524b93da9fd2d275b16c49dbe96268e58f" +dependencies = [ + "cranelift-codegen 0.86.1", + "cranelift-entity 0.86.1", + "cranelift-frontend", + "gimli 0.26.2", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon", + "tracing", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-compiler-singlepass" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e235ccc192d5f39147e8a430f48040dcfeebc1f1b0d979d2232ec1618d255c" +dependencies = [ + "byteorder", + "dynasm", + "dynasmrt", + "enumset", + "gimli 0.26.2", + "lazy_static", + "more-asserts", + "rayon", + "smallvec", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff577b7c1cfcd3d7c5b3a09fe1a499b73f7c17084845ff71225c8250a6a63a9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wasmer-types" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9600f9da966abae3be0b0a4560e7d1f2c88415a2d01ce362ac06063cb1c473" +dependencies = [ + "enum-iterator 0.7.0", + "enumset", + "indexmap 1.9.3", + "more-asserts", + "rkyv", + "target-lexicon", + "thiserror 1.0.69", +] + +[[package]] +name = "wasmer-vbus" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b42f76b9f09c68084de3a35fdf4907609f4b5005ecf3767fa1839a669dcbdb" +dependencies = [ + "thiserror 1.0.69", + "wasmer-vfs", +] + +[[package]] +name = "wasmer-vfs" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34bfbd243503d64aed4fc8a194657a561cae6c2d782dbcf649211d7f4db9e413" +dependencies = [ + "libc", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "wasmer-vm" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc68a7f0a003e6cb63845b7510065097d289553201d64afb9a5e1744da3c6a0" +dependencies = [ + "backtrace", + "cc", + "cfg-if", + "corosensei", + "enum-iterator 0.7.0", + "indexmap 1.9.3", + "lazy_static", + "libc", + "mach", + "memoffset 0.6.5", + "more-asserts", + "region", + "scopeguard", + "thiserror 1.0.69", + "wasmer-types", + "winapi", +] + +[[package]] +name = "wasmer-vnet" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bc4fe3b48ccc620901bdcdfac98d8a76ef3487412c221752814750c2e7db4c1" +dependencies = [ + "bytes", + "thiserror 1.0.69", + "wasmer-vfs", +] + +[[package]] +name = "wasmer-wasi" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e893ecd57c63db83b17dacfaee90f660e1d7f5b26d2f9d88ea6aa2e8c4bc301d" +dependencies = [ + "bytes", + "cfg-if", + "derivative", + "generational-arena", + "getrandom 0.2.15", + "libc", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen", + "wasmer", + "wasmer-vbus", + "wasmer-vfs", + "wasmer-vnet", + "wasmer-wasi-types", + "winapi", +] + +[[package]] +name = "wasmer-wasi-types" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1afdec83c62d22bf7110b83d662a08f708332fd728a213399919a045a1061d4" +dependencies = [ + "byteorder", + "time 0.2.27", + "wasmer", + "wasmer-derive", + "wasmer-types", + "wasmer-wit-bindgen-gen-core", + "wasmer-wit-bindgen-gen-rust-wasm", + "wasmer-wit-bindgen-rust", + "wasmer-wit-parser", +] + +[[package]] +name = "wasmer-wit-bindgen-gen-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8aa5be5ae5d61f5e151dc2c0e603093fe28395d2083b65ef7a3547844054fe" +dependencies = [ + "anyhow", + "wasmer-wit-parser", +] + +[[package]] +name = "wasmer-wit-bindgen-gen-rust" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438bce7c4589842bf100cc9b312443a9b5fc6440e58ab0b8c114e460219c3c3b" +dependencies = [ + "heck 0.3.3", + "wasmer-wit-bindgen-gen-core", +] + +[[package]] +name = "wasmer-wit-bindgen-gen-rust-wasm" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505f5168cfee591840e13e158a5c5e2f95d6df1df710839021564f36bee7bafc" +dependencies = [ + "heck 0.3.3", + "wasmer-wit-bindgen-gen-core", + "wasmer-wit-bindgen-gen-rust", +] + +[[package]] +name = "wasmer-wit-bindgen-rust" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968747f1271f74aab9b70d9c5d4921db9bd13b4ec3ba5506506e6e7dc58c918c" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "wasmer-wit-bindgen-rust-impl", +] + +[[package]] +name = "wasmer-wit-bindgen-rust-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd26fe00d08bd2119870b017d13413dfbd51e7750b6634d649fc7a7bbc057b85" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wasmer-wit-bindgen-gen-core", + "wasmer-wit-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wasmer-wit-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46c9a15086be8a2eb3790613902b9d3a9a687833b17cd021de263a20378585a" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "wasmparser" +version = "0.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a" + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" +dependencies = [ + "rustls-webpki 0.101.7", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.8", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43dbb096663629518eb1dfa72d80243ca5a6aca764cae62a2df70af760a9be75" +dependencies = [ + "windows_aarch64_msvc 0.33.0", + "windows_i686_gnu 0.33.0", + "windows_i686_msvc 0.33.0", + "windows_x86_64_gnu 0.33.0", + "windows_x86_64_msvc 0.33.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd761fd3eb9ab8cc1ed81e56e567f02dd82c4c837e48ac3b2181b9ffc5060807" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab0cf703a96bab2dc0c02c0fa748491294bf9b7feb27e1f4f96340f208ada0e" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfdbe89cc9ad7ce618ba34abc34bbb6c36d99e96cae2245b7943cd75ee773d0" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9b0c0e9ece7bb22e84d70d01b71c6d6248b81a3c60d11869451b4cb24784" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff1e4aa646495048ec7f3ffddc411e1d829c026a2ec62b39da15c1055e406eaa" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wormhole-sdk" +version = "0.1.0" +source = "git+https://github.com/space-operator/wormhole?rev=b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1#b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1" +dependencies = [ + "anyhow", + "bstr", + "schemars", + "serde", + "serde_wormhole 0.1.0 (git+https://github.com/space-operator/wormhole?rev=ee7d0765f9a3b4deb7450c9ff05e920b764ea486)", + "sha3 0.10.8", + "thiserror 1.0.69", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time 0.3.37", +] + +[[package]] +name = "xshell" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.37", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure 0.13.1", +] + +[[package]] +name = "zerocopy" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da091bab2bd35db397c46f5b81748b56f28f8fda837087fab9b6b07b6d66e3f1" +dependencies = [ + "byteorder", + "zerocopy-derive 0.2.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure 0.13.1", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe 7.2.1", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..d5dac1c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[workspace] +members = ["lib/*", "crates/*"] +exclude = ["crates/space-wasm/tests", "lib/space-operator-cli"] +resolver = "2" + +[patch.crates-io] +rhai = { git = "https://github.com/space-operator/rhai", rev = "b39d2bfb" } +# fix zeroize and subtle dep conflict +aes-gcm-siv = { git = "https://github.com/RustCrypto/AEADs", rev = "555ae1d82d000f01899498f969e6dc1d0a4fe467" } +curve25519-dalek = { git = "https://github.com/dalek-cryptography/curve25519-dalek", rev = "8274d5cbb6fc3f38cdc742b4798173895cd2a290" } +# branch: update-deps +mpl-candy-machine-core = { git = "https://github.com/space-operator/mpl-candy-machine", rev = "6096215a779702fb7954103e0d3199d89e120e6f" } +mpl-candy-guard = { git = "https://github.com/space-operator/mpl-candy-machine", rev = "6096215a779702fb7954103e0d3199d89e120e6f" } +mpl-core-candy-machine-core = { git = "https://github.com/space-operator/mpl-core-candy-machine", rev = "e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226" } +mpl-core-candy-guard = { git = "https://github.com/space-operator/mpl-core-candy-machine", rev = "e5f4d6c60e6d556ef4a3d18f4518d76ac2c01226" } + +[profile.dev] +debug = 0 +opt-level = 1 + +[profile.release] +debug = 0 +lto = "thin" + +[profile.dev.build-override] +opt-level = 2 +codegen-units = 256 +debug = false + +[profile.release.build-override] +opt-level = 2 +codegen-units = 256 + +[workspace.dependencies] +# crates/ +flow = { path = "crates/flow", version = "0.0.0" } +utils = { path = "crates/utils", version = "0.0.0" } +space-wasm = { path = "crates/space-wasm", version = "0.0.0" } +db = { path = "crates/db", version = "0.0.0" } +pdg-common = { path = "crates/pdg-common", version = "0.0.0" } +cmds-std = { path = "crates/cmds-std", version = "0.0.0" } +cmds-pdg = { path = "crates/cmds-pdg", version = "0.0.0" } +cmds-solana = { path = "crates/cmds-solana", version = "0.0.0" } +rhai-script = { path = "crates/rhai-script", version = "0.0.0" } +srpc = { path = "crates/srpc", version = "0.0.0" } +command-rpc = { path = "crates/command-rpc", version = "0.0.0" } +cmds-deno = { path = "crates/cmds-deno", version = "0.0.0" } + +# lib/ +value = { path = "lib/flow-value", version = "0.1.0", package = "flow-value" } +flow-lib = { path = "lib/flow-lib", version = "0.1.0" } +space-lib = { path = "lib/space-lib", version = "0.5.0" } +space-macro = { path = "lib/space-macro", version = "0.2.1" } +spo-helius = { path = "./lib/spo-helius", version = "0.1.0" } + +# Non-local crates +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } +anyhow = "1" +bs58 = "0.4" +postgrest = { package = "spo-postgrest", version = "1.6.0" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0e583f09 --- /dev/null +++ b/LICENSE @@ -0,0 +1,668 @@ +Open source license: AGPLv3 + +For commercial projects and to keep your source code proprietary, +please get a license at www.spaceoperator.com + +////////////////////////////////////////////////////////////////////// + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..4ced4e6f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# flow-backend +Space Operator Backend + +## Running a guest server + +You can run a local instance of flow-server and have it push results to our site's database. +Set required values in [guest.toml](https://github.com/space-operator/flow-backend/blob/main/guest.toml) +and run with: +```bash +RUST_LOG=info cargo run -p flow-server -- guest.toml +``` + +This server can be used to run flows belonging to you. Toggle "Remote 🌐" button in flow editor to switch to `localhost` server. +Please note that some browsers such as Brave will block requests to `localhost` by default, disable protection if you encounter network errors. \ No newline at end of file diff --git a/admin.toml b/admin.toml new file mode 100644 index 00000000..1acaade5 --- /dev/null +++ b/admin.toml @@ -0,0 +1,37 @@ +host = "0.0.0.0" +port = 8080 + +# Path to store local storage +local_storage = "_data/guest_local_storage" + +# Allow CORS from these origins +cors_origins = [ + "https://spaceoperator.com", + "https://www.spaceoperator.com", + "http://localhost:3000", +] + +[supabase] +# Supabase project's ID +project_id = "" +# Supabase JWT secret +jwt_key = "" +# Supabase service_role key +service_key = "" +# Supabase anon key +anon_key = "" +# (Optional) Custom URL for Supabase project +endpoint = "https://base.spaceoperator.com" +# Allow all pubkeys +open_whitelists = false + +[db] +host = "" +port = 5432 +user = "" +password = "" +dbname = "" + +[db.ssl] +# (Optional) Path to certificate file for TLS connection +cert = "prod-ca-2021.crt" diff --git a/archive/cmds-solana/src/nft/need_new_command_interface/auction_house_sell.rs b/archive/cmds-solana/src/nft/need_new_command_interface/auction_house_sell.rs new file mode 100644 index 00000000..e1f9f8af --- /dev/null +++ b/archive/cmds-solana/src/nft/need_new_command_interface/auction_house_sell.rs @@ -0,0 +1,264 @@ +use std::path::PathBuf; +use std::time::Duration; +use std::{collections::HashMap, sync::Arc}; + +use super::super::Ctx; +use anchor_lang::{InstructionData, ToAccountMetas}; +use dashmap::DashMap; +use maplit::hashmap; +use mpl_token_metadata::state::{Collection, Creator, UseMethod, Uses}; +use serde::{Deserialize, Serialize}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::instruction::Instruction; +use solana_sdk::{pubkey::Pubkey, signer::keypair::Keypair, signer::Signer}; +use spl_token::instruction::transfer_checked; +use uuid::Uuid; + +use sunshine_core::msg::NodeId; + +use crate::commands::solana::instructions::execute; +use crate::commands::solana::SolanaNet; +use crate::{Error, NftMetadata, Value}; + +use solana_sdk::signer::keypair::write_keypair_file; + +use bundlr_sdk::{tags::Tag, Bundlr, Signer as BundlrSigner, SolanaSigner}; + +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuctionHouseSell { + pub treasury_mint_account: Option>, + pub fee_payer: Option, + pub auction_house_authority: Option, + pub seller: Option, + pub seller_token_account: Option, + pub seller_token_mint_account: Option, + pub sale_price: Option, +} + +impl AuctionHouseSell { + pub(crate) async fn run( + &self, + ctx: Arc, + mut inputs: HashMap, + ) -> Result, Error> { + let treasury_mint_account = match self.treasury_mint_account { + Some(s) => match s { + Some(treasury_mint_account) => { + Some(ctx.get_pubkey_by_id(treasury_mint_account).await?) + } + None => None, + }, + None => match inputs.remove("treasury_mint_account") { + Some(Value::NodeIdOpt(s)) => match s { + Some(treasury_mint_account) => { + Some(ctx.get_pubkey_by_id(treasury_mint_account).await?) + } + None => None, + }, + Some(Value::Keypair(k)) => Some(Keypair::from(k).pubkey()), + Some(Value::Pubkey(p)) => Some(p.into()), + Some(Value::Empty) => None, + None => None, + _ => return Err(Error::ArgumentNotFound("treasury_mint_account".to_string())), + }, + }; + + let fee_payer = match self.fee_payer { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("fee_payer") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => return Err(Error::ArgumentNotFound("fee_payer".to_string())), + }, + }; + + let auction_house_authority = match self.auction_house_authority { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("auction_house_authority") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => { + return Err(Error::ArgumentNotFound( + "auction_house_authority".to_string(), + )) + } + }, + }; + + let seller = match self.seller { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("seller") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => return Err(Error::ArgumentNotFound("seller".to_string())), + }, + }; + + let seller_token_account = match self.seller_token_account { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("seller_token_account") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => return Err(Error::ArgumentNotFound("seller_token_account".to_string())), + }, + }; + + let seller_token_mint_account = match self.seller_token_mint_account { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("seller_token_mint_account") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => { + return Err(Error::ArgumentNotFound( + "seller_token_mint_account".to_string(), + )) + } + }, + }; + + let sale_price = match self.sale_price { + Some(s) => s, + None => match inputs.remove("sale_price") { + Some(Value::U64(s)) => s, + _ => return Err(Error::ArgumentNotFound("sale_price".to_string())), + }, + }; + + let treasury_mint_account = + treasury_mint_account.unwrap_or_else(spl_token::native_mint::id); + + let (minimum_balance_for_rent_exemption, instructions) = command_auction_house_sell( + &ctx.client, + treasury_mint_account, + auction_house_authority.pubkey(), + seller.pubkey(), + seller_token_account, + seller_token_mint_account, + sale_price, + )?; + + let fee_payer_pubkey = fee_payer.pubkey(); + + let signers: Vec<&dyn Signer> = vec![&fee_payer, &seller]; + + let signature = execute( + &signers, + &ctx.client, + &fee_payer_pubkey, + &instructions, + minimum_balance_for_rent_exemption, + )?; + + let outputs = hashmap! { + "signature".to_owned() => Value::Success(signature), + "fee_payer".to_owned() => Value::Keypair(fee_payer.into()), + "auction_house_authority".to_owned() => Value::Keypair(auction_house_authority.into()), + "treasury_mint_account".to_owned() => Value::Pubkey(treasury_mint_account.into()), + }; + + Ok(outputs) + } +} + +pub fn command_auction_house_sell( + rpc_client: &RpcClient, + treasury_mint_account: Pubkey, + auction_house_authority: Pubkey, + seller: Pubkey, + seller_token_account: Pubkey, + seller_token_mint_account: Pubkey, + sale_price: u64, +) -> Result<(u64, Vec), Error> { + let minimum_balance_for_rent_exemption = rpc_client.get_minimum_balance_for_rent_exemption( + mpl_auction_house::TRADE_STATE_SIZE + mpl_auction_house::receipt::LISTING_RECEIPT_SIZE, + )?; + + let (seller_token_metadata_account, _) = + mpl_token_metadata::pda::find_metadata_account(&seller_token_mint_account); + + let program_id = mpl_auction_house::id(); + + let (auction_house_address, bump) = mpl_auction_house::pda::find_auction_house_address( + &auction_house_authority, + &treasury_mint_account, + ); + + let (auction_fee_account_key, _) = + mpl_auction_house::pda::find_auction_house_fee_account_address(&auction_house_address); + + let (seller_trade_state, sts_bump) = mpl_auction_house::pda::find_trade_state_address( + &seller, + &auction_house_address, + &seller_token_account, + &treasury_mint_account, + &seller_token_mint_account, + sale_price, + 1, + ); + + let (free_seller_trade_state, free_sts_bump) = mpl_auction_house::pda::find_trade_state_address( + &seller, + &auction_house_address, + &seller_token_account, + &treasury_mint_account, + &seller_token_mint_account, + 0, + 1, + ); + + let (listing_receipt, receipt_bump) = + mpl_auction_house::pda::find_listing_receipt_address(&seller_trade_state); + + let (program_as_signer, pas_bump) = mpl_auction_house::pda::find_program_as_signer_address(); + + let accounts = mpl_auction_house::accounts::Sell { + wallet: seller, + token_account: seller_token_account, + metadata: seller_token_metadata_account, + authority: auction_house_authority, + auction_house: auction_house_address, + auction_house_fee_account: auction_fee_account_key, + seller_trade_state, + free_seller_trade_state, + token_program: spl_token::id(), + system_program: solana_sdk::system_program::id(), + program_as_signer, + rent: solana_sdk::sysvar::rent::id(), + } + .to_account_metas(None); + + let data = mpl_auction_house::instruction::Sell { + trade_state_bump: sts_bump, + _free_trade_state_bump: free_sts_bump, + _program_as_signer_bump: pas_bump, + token_size: 1, + buyer_price: sale_price, + } + .data(); + + let sell_instruction = Instruction { + program_id, + data, + accounts, + }; + + let listing_receipt_accounts = mpl_auction_house::accounts::PrintListingReceipt { + receipt: listing_receipt, + bookkeeper: seller, + system_program: solana_sdk::system_program::id(), + rent: solana_sdk::sysvar::rent::id(), + instruction: solana_sdk::sysvar::instructions::id(), + }; + + let print_receipt_instruction = Instruction { + program_id, + data: mpl_auction_house::instruction::PrintListingReceipt { receipt_bump }.data(), + accounts: listing_receipt_accounts.to_account_metas(None), + }; + + let instructions = vec![sell_instruction, print_receipt_instruction]; + + Ok((minimum_balance_for_rent_exemption, instructions)) +} diff --git a/archive/cmds-solana/src/nft/need_new_command_interface/create_auction_house.rs b/archive/cmds-solana/src/nft/need_new_command_interface/create_auction_house.rs new file mode 100644 index 00000000..15f75025 --- /dev/null +++ b/archive/cmds-solana/src/nft/need_new_command_interface/create_auction_house.rs @@ -0,0 +1,268 @@ +use std::path::PathBuf; +use std::time::Duration; +use std::{collections::HashMap, sync::Arc}; + +use super::super::Ctx; +use anchor_lang::{InstructionData, ToAccountMetas}; + +use dashmap::DashMap; +use maplit::hashmap; +use mpl_token_metadata::state::{Collection, Creator, UseMethod, Uses}; +use serde::{Deserialize, Serialize}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::instruction::Instruction; +use solana_sdk::{pubkey::Pubkey, signer::keypair::Keypair, signer::Signer}; +use spl_token::instruction::transfer_checked; +use uuid::Uuid; + +use sunshine_core::msg::NodeId; + +use crate::commands::solana::instructions::execute; +use crate::commands::solana::SolanaNet; +use crate::{Error, NftMetadata, Value}; + +use solana_sdk::signer::keypair::write_keypair_file; + +use bundlr_sdk::{tags::Tag, Bundlr, Signer as BundlrSigner, SolanaSigner}; + +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateAuctionHouse { + pub treasury_mint_account: Option>, + pub fee_payer: Option, + pub fee_withdrawal_destination: Option, + pub auction_house_authority: Option, + pub treasury_withdrawal_destination: Option, + pub treasury_withdrawal_destination_owner: Option, + pub seller_fee_basis_points: Option, + pub requires_sign_off: Option, + pub can_change_sale_price: Option, +} + +impl CreateAuctionHouse { + pub(crate) async fn run( + &self, + ctx: Arc, + mut inputs: HashMap, + ) -> Result, Error> { + let treasury_mint_account = match self.treasury_mint_account { + Some(s) => match s { + Some(treasury_mint_account) => { + Some(ctx.get_pubkey_by_id(treasury_mint_account).await?) + } + None => None, + }, + None => match inputs.remove("treasury_mint_account") { + Some(Value::NodeIdOpt(s)) => match s { + Some(treasury_mint_account) => { + Some(ctx.get_pubkey_by_id(treasury_mint_account).await?) + } + None => None, + }, + Some(Value::Keypair(k)) => Some(Keypair::from(k).pubkey()), + Some(Value::Pubkey(p)) => Some(p.into()), + Some(Value::Empty) => None, + None => None, + _ => return Err(Error::ArgumentNotFound("treasury_mint_account".to_string())), + }, + }; + + let fee_payer = match self.fee_payer { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("fee_payer") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => return Err(Error::ArgumentNotFound("fee_payer".to_string())), + }, + }; + + let fee_withdrawal_destination = match self.fee_withdrawal_destination { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("fee_withdrawal_destination") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => { + return Err(Error::ArgumentNotFound( + "fee_withdrawal_destination".to_string(), + )) + } + }, + }; + + let auction_house_authority = match self.auction_house_authority { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("auction_house_authority") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => { + return Err(Error::ArgumentNotFound( + "auction_house_authority".to_string(), + )) + } + }, + }; + + let treasury_withdrawal_destination = match self.treasury_withdrawal_destination { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("treasury_withdrawal_destination") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => { + return Err(Error::ArgumentNotFound( + "treasury_withdrawal_destination".to_string(), + )) + } + }, + }; + + let treasury_withdrawal_destination_owner = match self.treasury_withdrawal_destination_owner + { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("treasury_withdrawal_destination_owner") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => { + return Err(Error::ArgumentNotFound( + "treasury_withdrawal_destination_owner".to_string(), + )) + } + }, + }; + + let seller_fee_basis_points = match self.seller_fee_basis_points { + Some(s) => s, + None => match inputs.remove("seller_fee_basis_points") { + Some(Value::U16(s)) => s, + _ => { + return Err(Error::ArgumentNotFound( + "seller_fee_basis_points".to_string(), + )) + } + }, + }; + + let requires_sign_off = match self.requires_sign_off { + Some(s) => s, + None => match inputs.remove("requires_sign_off") { + Some(Value::Bool(s)) => s, + Some(Value::Empty) => false, + None => false, + _ => return Err(Error::ArgumentNotFound("requires_sign_off".to_string())), + }, + }; + + let can_change_sale_price = match self.can_change_sale_price { + Some(s) => s, + None => match inputs.remove("can_change_sale_price") { + Some(Value::Bool(s)) => s, + Some(Value::Empty) => true, + None => true, + _ => return Err(Error::ArgumentNotFound("can_change_sale_price".to_string())), + }, + }; + + let treasury_mint_account = + treasury_mint_account.unwrap_or_else(spl_token::native_mint::id); + + let (minimum_balance_for_rent_exemption, instructions) = command_create_auction_house( + &ctx.client, + treasury_mint_account, + fee_payer.pubkey(), + fee_withdrawal_destination, + auction_house_authority, + treasury_withdrawal_destination, + treasury_withdrawal_destination_owner, + seller_fee_basis_points, + requires_sign_off, + can_change_sale_price, + )?; + + let fee_payer_pubkey = fee_payer.pubkey(); + + let signers: Vec<&dyn Signer> = vec![&fee_payer]; + + let signature = execute( + &signers, + &ctx.client, + &fee_payer_pubkey, + &instructions, + minimum_balance_for_rent_exemption, + )?; + + let outputs = hashmap! { + "treasury_withdrawal_destination".to_owned()=> Value::Pubkey(treasury_withdrawal_destination.into()), + "signature".to_owned() => Value::Success(signature), + "fee_payer".to_owned() => Value::Keypair(fee_payer.into()), + "auction_house_authority".to_owned() => Value::Pubkey(auction_house_authority.into()), + "treasury_mint_account".to_owned() => Value::Pubkey(treasury_mint_account.into()), + }; + + Ok(outputs) + } +} + +pub fn command_create_auction_house( + rpc_client: &RpcClient, + treasury_mint_account: Pubkey, + fee_payer: Pubkey, + fee_withdrawal_destination: Pubkey, + auction_house_authority: Pubkey, + treasury_withdrawal_destination: Pubkey, + treasury_withdrawal_destination_owner: Pubkey, + seller_fee_basis_points: u16, + requires_sign_off: bool, + can_change_sale_price: bool, +) -> Result<(u64, Vec), Error> { + let minimum_balance_for_rent_exemption = + rpc_client.get_minimum_balance_for_rent_exemption(mpl_auction_house::AUCTION_HOUSE_SIZE)?; + + let (auction_house_address, bump) = mpl_auction_house::pda::find_auction_house_address( + &auction_house_authority, + &treasury_mint_account, + ); + + let (auction_fee_account_key, fee_payer_bump) = + mpl_auction_house::pda::find_auction_house_fee_account_address(&auction_house_address); + + let (auction_house_treasury_key, treasury_bump) = + mpl_auction_house::pda::find_auction_house_treasury_address(&auction_house_address); + + let accounts = mpl_auction_house::accounts::CreateAuctionHouse { + treasury_mint: treasury_mint_account, + payer: fee_payer, + authority: auction_house_authority, + fee_withdrawal_destination, + treasury_withdrawal_destination, + treasury_withdrawal_destination_owner, + // internal/derived + auction_house: auction_house_address, + auction_house_fee_account: auction_fee_account_key, + auction_house_treasury: auction_house_treasury_key, + token_program: spl_token::id(), + system_program: solana_sdk::system_program::id(), + ata_program: spl_associated_token_account::id(), + rent: solana_sdk::sysvar::rent::id(), + } + .to_account_metas(None); + + let data = mpl_auction_house::instruction::CreateAuctionHouse { + _bump: bump, + fee_payer_bump, + treasury_bump, + seller_fee_basis_points, + requires_sign_off, + can_change_sale_price, + } + .data(); + + let instruction = Instruction { + program_id: mpl_auction_house::id(), + data, + accounts, + }; + + let instructions = vec![instruction]; + + Ok((minimum_balance_for_rent_exemption, instructions)) +} diff --git a/archive/cmds-solana/src/nft/need_new_command_interface/utilize.rs b/archive/cmds-solana/src/nft/need_new_command_interface/utilize.rs new file mode 100644 index 00000000..81021088 --- /dev/null +++ b/archive/cmds-solana/src/nft/need_new_command_interface/utilize.rs @@ -0,0 +1,168 @@ +use std::{collections::HashMap, sync::Arc}; + +use super::super::Ctx; +use maplit::hashmap; +use mpl_token_metadata::state::{Collection, Creator, UseMethod, Uses}; +use serde::{Deserialize, Serialize}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{pubkey::Pubkey, signer::keypair::Keypair, signer::Signer}; + +use sunshine_core::msg::NodeId; + +use crate::{commands::solana::instructions::execute, CommandResult, Error, NftCreator, Value}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Utilize { + pub mint_account: Option, + pub use_authority: Option, // keypair + pub fee_payer: Option, // keypair + pub account: Option>, + pub owner: Option, + pub burner: Option, + pub number_of_uses: Option, +} + +impl Utilize { + pub(crate) async fn run( + &self, + ctx: Arc, + mut inputs: HashMap, + ) -> Result, Error> { + let mint_account = match self.mint_account { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("mint_account") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => return Err(Error::ArgumentNotFound("mint_account".to_string())), + }, + }; + + let use_authority = match self.use_authority { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("use_authority") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => return Err(Error::ArgumentNotFound("use_authority".to_string())), + }, + }; + + let fee_payer = match self.fee_payer { + Some(s) => ctx.get_keypair_by_id(s).await?, + None => match inputs.remove("fee_payer") { + Some(Value::NodeId(s)) => ctx.get_keypair_by_id(s).await?, + Some(Value::Keypair(k)) => k.into(), + _ => return Err(Error::ArgumentNotFound("fee_payer".to_string())), + }, + }; + + let account = match self.account { + Some(s) => match s { + Some(account) => Some(ctx.get_pubkey_by_id(account).await?), + None => None, + }, + None => match inputs.remove("account") { + Some(Value::NodeIdOpt(s)) => match s { + Some(account) => Some(ctx.get_pubkey_by_id(account).await?), + None => None, + }, + Some(Value::Keypair(k)) => Some(Keypair::from(k).pubkey()), + Some(Value::Pubkey(p)) => Some(p.into()), + Some(Value::Empty) => None, + None => None, + _ => return Err(Error::ArgumentNotFound("account".to_string())), + }, + }; + + let owner = match self.owner { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("owner") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => return Err(Error::ArgumentNotFound("owner".to_string())), + }, + }; + + let burner = match self.burner { + Some(s) => ctx.get_pubkey_by_id(s).await?, + None => match inputs.remove("burner") { + Some(Value::NodeId(id)) => ctx.get_pubkey_by_id(id).await?, + Some(v) => v.try_into()?, + _ => return Err(Error::ArgumentNotFound("burner".to_string())), + }, + }; + + let number_of_uses = match self.number_of_uses { + Some(s) => s, + None => match inputs.remove("number_of_uses") { + Some(Value::U64(s)) => s, + _ => return Err(Error::ArgumentNotFound("number_of_uses".to_string())), + }, + }; + + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&mint_account); + + let account = account.unwrap_or_else(|| { + spl_associated_token_account::get_associated_token_address(&owner, &mint_account) + }); + + let (minimum_balance_for_rent_exemption, instructions) = command_utilize( + metadata_account, + account, + mint_account, + use_authority.pubkey(), + owner, + Some(burner), + number_of_uses, + )?; + + let fee_payer_pubkey = fee_payer.pubkey(); + + let signers: Vec<&dyn Signer> = vec![&use_authority, &fee_payer]; + + let res = execute( + &signers, + &ctx.client, + &fee_payer_pubkey, + &instructions, + minimum_balance_for_rent_exemption, + ); + + let signature = res?; + + let outputs = hashmap! { + "signature".to_owned()=>Value::Success(signature), + "fee_payer".to_owned() => Value::Keypair(fee_payer.into()), + "mint_account".to_owned()=> Value::Pubkey(mint_account.into()), + "use_authority".to_owned() => Value::Keypair(use_authority.into()), + "owner".to_owned() => Value::Pubkey(owner.into()), + "account".to_owned() => Value::Pubkey(account.into()), + "burner".to_owned() => Value::Pubkey(burner.into()), + }; + + Ok(outputs) + } +} + +pub fn command_utilize( + metadata_pubkey: Pubkey, + token_account: Pubkey, + mint: Pubkey, + use_authority: Pubkey, + owner: Pubkey, + burner: Option, + number_of_uses: u64, +) -> CommandResult { + let instructions = vec![mpl_token_metadata::instruction::utilize( + mpl_token_metadata::id(), + metadata_pubkey, + token_account, + mint, + None, + use_authority, + owner, + burner, + number_of_uses, + )]; + + Ok((0, instructions)) +} diff --git a/archive/crates/cmds-solana/src/clockwork/mod.rs b/archive/crates/cmds-solana/src/clockwork/mod.rs new file mode 100644 index 00000000..b231dfce --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/mod.rs @@ -0,0 +1,2 @@ +// pub mod payments; +pub mod threads; diff --git a/archive/crates/cmds-solana/src/clockwork/payments/disburse_payment_ix.rs b/archive/crates/cmds-solana/src/clockwork/payments/disburse_payment_ix.rs new file mode 100644 index 00000000..1ae32b89 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/payments/disburse_payment_ix.rs @@ -0,0 +1,166 @@ +use crate::{prelude::*, utils::anchor_sighash}; +use anchor_spl::{associated_token, token}; +use clockwork_utils::PAYER_PUBKEY; +use payments::state::Payment as ClockworkPayment; +use serde_json::{to_value, Value as JsonValue}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + system_program, sysvar, +}; + +#[derive(Debug, Clone)] +pub struct DisbursePaymentIx; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + payer: Pubkey, + #[serde(with = "value::pubkey")] + authority_token_account: Pubkey, + #[serde(with = "value::pubkey")] + mint: Pubkey, + #[serde(with = "value::pubkey")] + payment: Pubkey, + #[serde(with = "value::pubkey")] + thread: Pubkey, + #[serde(with = "value::pubkey")] + recipient: Pubkey, + #[serde(with = "value::pubkey")] + recipient_ata: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + instruction: JsonValue, +} + +// Name +const DISBURSE_PAYMENT_IX: &str = "disburse_payment_ix"; + +// Inputs +const PAYER: &str = "payer"; +const AUTHORITY_TOKEN_ACCOUNT: &str = "authority_token_account"; +const MINT: &str = "mint"; +const PAYMENT: &str = "payment"; +const THREAD: &str = "thread"; +const RECIPIENT: &str = "recipient"; +const RECIPIENT_ATA: &str = "recipient_ata"; + +// Outputs +const INSTRUCTION: &str = "instruction"; + +#[async_trait] +impl CommandTrait for DisbursePaymentIx { + fn name(&self) -> Name { + DISBURSE_PAYMENT_IX.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: PAYER.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: AUTHORITY_TOKEN_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: MINT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: PAYMENT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: THREAD.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: RECIPIENT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: RECIPIENT_ATA.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: INSTRUCTION.into(), + r#type: ValueType::Json, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, inputs: ValueSet) -> Result { + let Input { + payer, + authority_token_account, + mint, + payment: _, + thread, + recipient, + recipient_ata, + } = value::from_map::(inputs)?; + + let payment = ClockworkPayment::pubkey(payer, mint, recipient); + + // Get recipient's Associated Token Account + let recipient_ata_pubkey = get_associated_token_address(&input.recipient, &input.token_mint); + + let program_id = payments::ID; + let accounts = vec![ + AccountMeta::new_readonly(associated_token::ID, false), + AccountMeta::new_readonly(payer, false), + AccountMeta::new(authority_token_account, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(PAYER_PUBKEY, true), + AccountMeta::new(payment, false), + AccountMeta::new(thread, true), + AccountMeta::new_readonly(recipient, false), + AccountMeta::new(recipient_ata, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(token::ID, false), + ]; + let data = anchor_sighash("disburse_payment").to_vec(); + + let instruction = Instruction::new_with_bytes(program_id, &data, accounts); + + // TODO: don't call to_value? + // TODO: submit instruction + let instruction = to_value(instruction).unwrap(); + + Ok(value::to_map(&Output { instruction })?) + } +} + +flow_lib::submit!(CommandDescription::new(DISBURSE_PAYMENT_IX, |_| Ok( + Box::new(DisbursePaymentIx {}) +))); + +#[cfg(test)] +mod tests { + + #[tokio::test] + async fn test_valid() {} +} diff --git a/archive/crates/cmds-solana/src/clockwork/payments/mod.rs b/archive/crates/cmds-solana/src/clockwork/payments/mod.rs new file mode 100644 index 00000000..5844f56e --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/payments/mod.rs @@ -0,0 +1,13 @@ +// pub mod disburse_payment_ix; +pub mod payment; +pub mod update_payment; + +pub fn anchor_sighash(name: &str) -> [u8; 8] { + let namespace = "global"; + let preimage = format!("{}:{}", namespace, name); + let mut sighash = [0u8; 8]; + sighash.copy_from_slice( + &anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8], + ); + sighash +} diff --git a/archive/crates/cmds-solana/src/clockwork/payments/payment.rs b/archive/crates/cmds-solana/src/clockwork/payments/payment.rs new file mode 100644 index 00000000..e896add9 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/payments/payment.rs @@ -0,0 +1,187 @@ +use crate::prelude::*; +use anchor_lang_26::{solana_program::sysvar, InstructionData, ToAccountMetas}; +use anchor_spl_26::{associated_token, token}; +use clockwork_client::thread::{ + instruction::thread_create, + state::{Thread, Trigger}, +}; + +use clockwork_utils::thread::SerializableInstruction as ClockWorkInstruction; +use payments::state::Payment as ClockworkPayment; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; +use spl_associated_token_account::get_associated_token_address; + +const CREATE_PAYMENT: &str = "create_payment"; + +const DEFINITION: &str = flow_lib::node_definition!("clockwork/payments/create_payment.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(CREATE_PAYMENT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(CREATE_PAYMENT, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum TriggerInput { + IsImmediate { + is_immediate: bool, + }, + Schedule { + schedule: String, + is_skippable: bool, + }, + MonitorAccount { + #[serde(with = "value::pubkey")] + monitor_account: Pubkey, + offset: u64, + size: u64, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::pubkey")] + pub token_account: Pubkey, + #[serde(with = "value::pubkey")] + pub token_mint: Pubkey, + #[serde(with = "value::pubkey")] + pub recipient: Pubkey, + pub amount: u64, + pub trigger: TriggerInput, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let trigger = match input.trigger { + TriggerInput::IsImmediate { is_immediate: _ } => Trigger::Now, + TriggerInput::Schedule { + schedule, + is_skippable, + } => Trigger::Cron { + schedule, + skippable: is_skippable, + }, + TriggerInput::MonitorAccount { + monitor_account, + offset, + size, + } => Trigger::Account { + address: monitor_account, + offset, + size, + }, + }; + + // Thread Authority is the Payer + let thread_authority = input.payer.pubkey(); + + // Derive PDAs + let payment = ClockworkPayment::pubkey(input.payer.pubkey(), input.token_mint, input.recipient); + let thread = Thread::pubkey(thread_authority, "payment".into()); + let recipient_ata_pubkey = get_associated_token_address(&input.recipient, &input.token_mint); + + // FIXME - check size + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(816 + 608 + 512) + .await?; + + // Create Payment Instruction + let accounts = payments::accounts::CreatePayment { + associated_token_program: associated_token::ID, + authority: input.payer.pubkey(), + authority_token_account: input.token_account, + mint: input.token_mint, + payment, + recipient: input.recipient, + rent: sysvar::rent::ID, + system_program: system_program::ID, + token_program: token::ID, + } + .to_account_metas(None); + + let data = payments::instruction::CreatePayment { + amount: input.amount, + } + .data(); + + let payment_instruction = Instruction { + program_id: payments::ID, + accounts, + data, + }; + + // Create Disbursement Instruction + let accounts = payments::accounts::DisbursePayment { + associated_token_program: associated_token::ID, + authority: input.payer.pubkey(), + authority_token_account: input.token_account, + mint: input.token_mint, + payer: input.payer.pubkey(), + payment, + thread, + recipient: input.recipient, + recipient_token_account: recipient_ata_pubkey, + rent: sysvar::rent::ID, + system_program: system_program::ID, + token_program: token::ID, + } + .to_account_metas(None); + + let distribute_payment_ix: ClockWorkInstruction = Instruction { + program_id: payments::ID, + accounts, + data: payments::instruction::DisbursePayment.data(), + } + .into(); + + // Create Thread Instruction with Disbursement as the first instruction + let thread_create_instruction = thread_create( + input.amount, + input.payer.pubkey(), + "payment".into(), + vec![distribute_payment_ix], + input.payer.pubkey(), + thread, + trigger, + ); + + // Bundle it all up + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.payer.clone_keypair()].into(), + instructions: [payment_instruction, thread_create_instruction].into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "thread" => thread + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/clockwork/payments/update_payment.rs b/archive/crates/cmds-solana/src/clockwork/payments/update_payment.rs new file mode 100644 index 00000000..8fdb2ca4 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/payments/update_payment.rs @@ -0,0 +1,157 @@ +use crate::prelude::*; +use anchor_lang_26::InstructionData; +use payments::state::Payment as ClockworkPayment; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug)] +pub struct UpdatePayment; + +// update disbursement amount +fn update_payment(payment_pubkey: Pubkey, payer: Pubkey, amount: u64) -> Instruction { + // create instruction + Instruction { + program_id: payments::ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(payment_pubkey, false), + ], + data: payments::instruction::UpdatePayment { + amount: Some(amount), + } + .data(), + } +} + +impl UpdatePayment { + #[allow(clippy::too_many_arguments)] + async fn command_update_payment( + &self, + rpc_client: &RpcClient, + payer: Pubkey, + payment: Pubkey, + amount: u64, + ) -> crate::Result<(u64, Vec)> { + // FIXME min rent + let minimum_balance_for_rent_exemption = rpc_client + .get_minimum_balance_for_rent_exemption(80) + .await?; + + let instructions = vec![update_payment(payment, payer, amount)]; + + Ok((minimum_balance_for_rent_exemption, instructions)) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::pubkey")] + pub token_mint: Pubkey, + #[serde(with = "value::pubkey")] + pub recipient: Pubkey, + pub amount: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +// Command Name +const UPDATE_PAYMENT: &str = "update_payment"; + +// Inputs +const PAYER: &str = "payer"; +const TOKEN_MINT: &str = "token_mint"; +const RECIPIENT: &str = "recipient"; +const AMOUNT: &str = "amount"; + +// Outputs +const SIGNATURE: &str = "signature"; + +// TODO +// convert schedule +// /home/amir/.cargo/registry/src/github.com-1ecc6299db9ec823/clockwork-cron-1.4.0/src/schedule.rs + +#[async_trait] +impl CommandTrait for UpdatePayment { + fn name(&self) -> Name { + UPDATE_PAYMENT.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: TOKEN_MINT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: RECIPIENT.into(), + type_bounds: [ValueType::Keypair, ValueType::String, ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: AMOUNT.into(), + type_bounds: [ValueType::U64].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + payer, + token_mint, + recipient, + amount, + } = value::from_map(inputs.clone())?; + + // Derive PDAs + let payment = ClockworkPayment::pubkey(payer.pubkey(), token_mint, recipient); + + // Create Instructions + let (minimum_balance_for_rent_exemption, instructions) = self + .command_update_payment(&ctx.solana_client, payer.pubkey(), payment, amount) + .await?; + + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &payer.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet(&ctx, &mut transaction, &[&payer], recent_blockhash).await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_PAYMENT, |_| { + Ok(Box::new(UpdatePayment)) +})); diff --git a/archive/crates/cmds-solana/src/clockwork/threads/mod.rs b/archive/crates/cmds-solana/src/clockwork/threads/mod.rs new file mode 100644 index 00000000..7616200f --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/mod.rs @@ -0,0 +1,129 @@ +use crate::prelude::Pubkey; +use serde::{Deserialize, Serialize}; + +use clockwork_client::thread::state::ThreadSettings as ClockWorkThreadSettings; +use clockwork_utils::thread::SerializableAccount as ClockWorkAccount; +use clockwork_utils::thread::SerializableInstruction as ClockWorkInstruction; +use clockwork_utils::thread::Trigger as ClockWorkTrigger; + +pub mod thread_create; +pub mod thread_delete; +pub mod thread_pause; +pub mod thread_reset; +pub mod thread_resume; +pub mod thread_update; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Instruction { + pub program_id: Pubkey, + pub accounts: Vec, + pub data: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccountMeta { + pub pubkey: Pubkey, + pub is_signer: bool, + pub is_writable: bool, +} + +impl From for ClockWorkInstruction { + fn from(instruction: Instruction) -> Self { + ClockWorkInstruction { + program_id: instruction.program_id, + accounts: instruction + .accounts + .iter() + .map(|a| ClockWorkAccount { + pubkey: a.pubkey, + is_signer: a.is_signer, + is_writable: a.is_writable, + }) + .collect(), + data: instruction.data, + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum Trigger { + /// Allows a thread to be kicked off whenever the data of an account changes. + Account { + /// The address of the account to monitor. + address: Pubkey, + /// The byte offset of the account data to monitor. + offset: u64, + /// The size of the byte slice to monitor (must be less than 1kb) + size: u64, + }, + + /// Allows an thread to be kicked off according to a one-time or recurring schedule. + Cron { + /// The schedule in cron syntax. Value must be parsable by the `clockwork_cron` package. + schedule: String, + + /// Boolean value indicating whether triggering moments may be skipped if they are missed (e.g. due to network downtime). + /// If false, any "missed" triggering moments will simply be executed as soon as the network comes back online. + skippable: bool, + }, + + /// Allows an thread to be kicked off as soon as it's created. + Now, + + /// Allows a thread to be kicked off according to a slot. + Slot { slot: u64 }, + + /// Allows a thread to be kicked off according to an epoch number. + Epoch { epoch: u64 }, +} + +// Implement From Trigger to ClockWorkTrigger +impl From for ClockWorkTrigger { + fn from(trigger: Trigger) -> Self { + match trigger { + Trigger::Account { + address, + offset, + size, + } => ClockWorkTrigger::Account { + address, + offset, + size, + }, + Trigger::Cron { + schedule, + skippable, + } => ClockWorkTrigger::Cron { + schedule, + skippable, + }, + Trigger::Now => ClockWorkTrigger::Now, + Trigger::Slot { slot } => ClockWorkTrigger::Slot { slot }, + Trigger::Epoch { epoch } => ClockWorkTrigger::Epoch { epoch }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ThreadSettings { + pub fee: Option, + pub instructions: Option>, + pub name: Option, + pub rate_limit: Option, + pub trigger: Option, +} + +// Implement From ThreadSettings to ClockWorkThreadSettings +impl From for ClockWorkThreadSettings { + fn from(thread_settings: ThreadSettings) -> Self { + ClockWorkThreadSettings { + fee: thread_settings.fee, + instructions: thread_settings + .instructions + .map(|i| i.into_iter().map(|i| i.into()).collect()), + name: thread_settings.name, + rate_limit: thread_settings.rate_limit, + trigger: thread_settings.trigger.map(|t| t.into()), + } + } +} diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_create.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_create.rs new file mode 100644 index 00000000..edb2e953 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_create.rs @@ -0,0 +1,104 @@ +use super::Trigger; +use crate::prelude::*; +use clockwork_client::thread::instruction::thread_create; +use clockwork_client::thread::state::Thread; +use clockwork_utils::thread::SerializableInstruction as ClockWorkInstruction; +use clockwork_utils::thread::Trigger as ClockWorkTrigger; +use solana_program::instruction::Instruction; + +// Command Name +const THREAD_CREATE: &str = "thread_create"; + +const DEFINITION: &str = flow_lib::node_definition!("clockwork/threads/thread_create.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(THREAD_CREATE)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(THREAD_CREATE, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub amount: u64, + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + pub id: String, + pub instructions: Vec, + #[serde(with = "value::keypair")] + pub payer: Keypair, + pub trigger: Trigger, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} +async fn run(mut ctx: Context, input: Input) -> Result { + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadCreate, + >()) + .await?; + + // Instruction to ClockWork SerializableInstruction + let mut instruction_chain = vec![]; + for instruction in input.instructions { + let instruction = ClockWorkInstruction::from(instruction); + instruction_chain.push(instruction); + } + + // Trigger to ClockWork Trigger + let trigger = ClockWorkTrigger::from(input.trigger); + + let id = input.id.as_bytes().to_vec(); + + let thread = Thread::pubkey(input.thread_authority.pubkey(), id.clone()); + + // Create Instructions + let instruction = thread_create( + minimum_balance_for_rent_exemption + input.amount, + input.thread_authority.pubkey(), + id.clone(), + instruction_chain, + input.payer.pubkey(), + thread, + trigger, + ); + + // Bundle it all up + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [ + input.payer.clone_keypair(), + input.thread_authority.clone_keypair(), + ] + .into(), + instructions: [instruction].into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "thread" => thread + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_delete.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_delete.rs new file mode 100644 index 00000000..97e98544 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_delete.rs @@ -0,0 +1,121 @@ +use crate::prelude::*; +use clockwork_client::thread::{instruction::thread_delete, state::Thread}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const THREAD_DELETE: &str = "thread_delete"; + +#[derive(Debug)] +pub struct ThreadDelete; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + #[serde(default, with = "value::keypair::opt")] + pub payer: Option, + #[serde(default, with = "value::pubkey::opt")] + pub thread: Option, + #[serde(default, with = "value::pubkey::opt")] + pub close_to: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +#[async_trait] +impl CommandTrait for ThreadDelete { + fn name(&self) -> Name { + THREAD_DELETE.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: "THREAD_AUTHORITY".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: "PAYER".into(), + type_bounds: [ValueType::Keypair, ValueType::String].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: "THREAD".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: "CLOSE_TO".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: false, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: "SIGNATURE".into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + thread_authority, + payer, + thread, + close_to, + } = value::from_map(inputs.clone())?; + + // Get Inputs or pass thread_authority as default + let payer_input: Keypair = payer.unwrap_or(thread_authority.clone_keypair()); + + let thread_input: Pubkey = + thread.unwrap_or_else(|| Thread::pubkey(thread_authority.pubkey(), "payment".into())); + + let close_to_input: Pubkey = close_to.unwrap_or(thread_authority.pubkey()); + + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadDelete, + >()) + .await?; + + let instructions = vec![thread_delete( + payer_input.pubkey(), + close_to_input, + thread_input, + )]; + + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &payer_input.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet(&ctx, &mut transaction, &[&payer_input], recent_blockhash).await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(THREAD_DELETE, |_| { + Ok(Box::new(ThreadDelete)) +})); diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_pause.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_pause.rs new file mode 100644 index 00000000..12152671 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_pause.rs @@ -0,0 +1,101 @@ +use crate::prelude::*; + +use clockwork_client::thread::instruction::thread_pause; + +use solana_sdk::pubkey::Pubkey; + +// Command Name +const THREAD_PAUSE: &str = "thread_pause"; + +#[derive(Debug)] +pub struct ThreadPause; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + #[serde(with = "value::pubkey")] + pub thread: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +#[async_trait] +impl CommandTrait for ThreadPause { + fn name(&self) -> Name { + THREAD_PAUSE.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: "THREAD_AUTHORITY".into(), + type_bounds: [ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: "THREAD".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: "SIGNATURE".into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + thread_authority, + thread, + } = value::from_map(inputs.clone())?; + + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadPause, + >()) + .await?; + + // Create Instructions + let instructions = vec![thread_pause(thread_authority.pubkey(), thread)]; + + // + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &thread_authority.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&thread_authority], + recent_blockhash, + ) + .await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(THREAD_PAUSE, |_| { + Ok(Box::new(ThreadPause)) +})); diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_reset.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_reset.rs new file mode 100644 index 00000000..1e8abe6c --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_reset.rs @@ -0,0 +1,101 @@ +use crate::prelude::*; + +use clockwork_client::thread::instruction::thread_reset; + +use solana_sdk::pubkey::Pubkey; + +// Command Name +const THREAD_RESET: &str = "thread_reset"; + +#[derive(Debug)] +pub struct ThreadReset; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + #[serde(with = "value::pubkey")] + pub thread: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +#[async_trait] +impl CommandTrait for ThreadReset { + fn name(&self) -> Name { + THREAD_RESET.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: "THREAD_AUTHORITY".into(), + type_bounds: [ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: "THREAD".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: "SIGNATURE".into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + thread_authority, + thread, + } = value::from_map(inputs.clone())?; + + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadPause, + >()) + .await?; + + // Create Instructions + let instructions = vec![thread_reset(thread_authority.pubkey(), thread)]; + + // + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &thread_authority.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&thread_authority], + recent_blockhash, + ) + .await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(THREAD_RESET, |_| { + Ok(Box::new(ThreadReset)) +})); diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_resume.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_resume.rs new file mode 100644 index 00000000..b66206e2 --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_resume.rs @@ -0,0 +1,101 @@ +use crate::prelude::*; + +use clockwork_client::thread::instruction::thread_resume; + +use solana_sdk::pubkey::Pubkey; + +// Command Name +const THREAD_RESUME: &str = "thread_resume"; + +#[derive(Debug)] +pub struct ThreadResume; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + #[serde(with = "value::pubkey")] + pub thread: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +#[async_trait] +impl CommandTrait for ThreadResume { + fn name(&self) -> Name { + THREAD_RESUME.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: "THREAD_AUTHORITY".into(), + type_bounds: [ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: "THREAD".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: "SIGNATURE".into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + thread_authority, + thread, + } = value::from_map(inputs.clone())?; + + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadPause, + >()) + .await?; + + // Create Instructions + let instructions = vec![thread_resume(thread_authority.pubkey(), thread)]; + + // + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &thread_authority.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&thread_authority], + recent_blockhash, + ) + .await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(THREAD_RESUME, |_| { + Ok(Box::new(ThreadResume)) +})); diff --git a/archive/crates/cmds-solana/src/clockwork/threads/thread_update.rs b/archive/crates/cmds-solana/src/clockwork/threads/thread_update.rs new file mode 100644 index 00000000..82e3861d --- /dev/null +++ b/archive/crates/cmds-solana/src/clockwork/threads/thread_update.rs @@ -0,0 +1,124 @@ +use crate::prelude::*; + +use clockwork_client::thread::instruction::thread_update; + +use clockwork_thread_program::state::ThreadSettings as ClockWorkThreadSettings; +use solana_sdk::pubkey::Pubkey; + +use super::{Instruction, ThreadSettings, Trigger}; + +// Command Name +const THREAD_UPDATE: &str = "thread_update"; + +#[derive(Debug)] +pub struct ThreadUpdate; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub thread_authority: Keypair, + #[serde(with = "value::pubkey")] + pub thread: Pubkey, + pub instructions: Option>, + pub fee: Option, + pub name: Option, + pub trigger: Option, + pub rate_limit: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + signature: Signature, +} + +#[async_trait] +impl CommandTrait for ThreadUpdate { + fn name(&self) -> Name { + THREAD_UPDATE.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: "THREAD_AUTHORITY".into(), + type_bounds: [ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: "THREAD".into(), + type_bounds: [ValueType::Pubkey, ValueType::Keypair, ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: "SIGNATURE".into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + thread_authority, + thread, + fee, + name, + trigger, + rate_limit, + instructions, + } = value::from_map(inputs.clone())?; + + // FIXME + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + clockwork_thread_program::accounts::ThreadPause, + >()) + .await?; + + let settings = ThreadSettings { + fee, + instructions, + name, + rate_limit, + trigger, + }; + + let settings = ClockWorkThreadSettings::from(settings); + + // Create Instructions + let instructions = vec![thread_update(thread_authority.pubkey(), thread, settings)]; + + // + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &thread_authority.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&thread_authority], + recent_blockhash, + ) + .await?; + + let signature = submit_transaction(&ctx.solana_client, transaction).await?; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(THREAD_UPDATE, |_| { + Ok(Box::new(ThreadUpdate)) +})); diff --git a/archive/crates/cmds-solana/src/metaboss/burn.rs b/archive/crates/cmds-solana/src/metaboss/burn.rs new file mode 100644 index 00000000..4c5523dd --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/burn.rs @@ -0,0 +1,82 @@ +use crate::prelude::*; +use metaboss_utils::commands::burn::{burn, BurnArgs}; + +#[derive(Clone, Debug)] +pub struct Burn; + +const BURN: &str = "burn"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_PUBKEY: &str = "mint_account_pubkey"; + +// Output +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account_pubkey: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for Burn { + fn name(&self) -> Name { + BURN.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_PUBKEY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::Signature, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = BurnArgs { + client: &ctx.solana_client, + keypair: Arc::new(input.keypair.clone_keypair()), + mint_pubkey: input.mint_account_pubkey, + }; + + let mut tx = burn(&args).await.map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(BURN, |_| Ok(Box::new(Burn)))); diff --git a/archive/crates/cmds-solana/src/metaboss/burn_print.rs b/archive/crates/cmds-solana/src/metaboss/burn_print.rs new file mode 100644 index 00000000..afa2025f --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/burn_print.rs @@ -0,0 +1,94 @@ +use crate::prelude::*; +use metaboss_utils::commands::burn::{burn_print, BurnPrintArgs}; + +#[derive(Clone, Debug)] +pub struct BurnPrint; + +const BURN_PRINT: &str = "burn_print"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_PUBKEY: &str = "mint_account_pubkey"; +const MASTER_MINT_PUBKEY: &str = "master_mint_pubkey"; + +// Output +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account_pubkey: Pubkey, + #[serde(with = "value::pubkey")] + pub master_mint_pubkey: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for BurnPrint { + fn name(&self) -> Name { + BURN_PRINT.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_PUBKEY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MASTER_MINT_PUBKEY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::Signature, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = BurnPrintArgs { + client: &ctx.solana_client, + keypair: Arc::new(input.keypair.clone_keypair()), + mint_pubkey: input.mint_account_pubkey, + master_mint_pubkey: input.master_mint_pubkey, + }; + + let mut tx = burn_print(args).await.map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(BURN_PRINT, |_| Ok(Box::new( + BurnPrint +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/decode.rs b/archive/crates/cmds-solana/src/metaboss/decode.rs new file mode 100644 index 00000000..f96d7f6d --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/decode.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; +use metaboss_utils::commands::decode::decode; +use mpl_token_metadata::state::Metadata; + +#[derive(Clone, Debug)] +pub struct Decode; + +const DECODE: &str = "decode"; + +// Inputs +const MINT_ACCOUNT: &str = "mint_account"; + +// Outputs +const METADATA: &str = "metadata"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub metadata: Metadata, +} + +#[async_trait] +impl CommandTrait for Decode { + fn name(&self) -> Name { + DECODE.into() + } + + fn inputs(&self) -> Vec { + [CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: METADATA.into(), + r#type: ValueType::Json, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let metadata = decode(&ctx.solana_client, &input.mint_account) + .await + .map_err(crate::Error::custom)?; + + Ok(value::to_map(&Output { metadata })?) + } +} + +flow_lib::submit!(CommandDescription::new(DECODE, |_| Ok(Box::new(Decode)))); diff --git a/archive/crates/cmds-solana/src/metaboss/migrate_collection.rs b/archive/crates/cmds-solana/src/metaboss/migrate_collection.rs new file mode 100644 index 00000000..3bdf7aa8 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/migrate_collection.rs @@ -0,0 +1,92 @@ +use crate::prelude::*; +use metaboss_utils::commands::collections::{migrate_collection, MigrateArgs}; + +#[derive(Clone, Debug)] +pub struct MigrateCollection; + +const MIGRATE_COLLECTION: &str = "migrate_collection"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ADDRESS: &str = "mint_address"; +const CANDY_MACHINE_ID: &str = "candy_machine_id"; +const MINT_LIST: &str = "mint_list"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_address: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub candy_machine_id: Option, + pub mint_list: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output {} + +#[async_trait] +impl CommandTrait for MigrateCollection { + fn name(&self) -> Name { + MIGRATE_COLLECTION.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ADDRESS.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: CANDY_MACHINE_ID.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: MINT_LIST.into(), + type_bounds: [ValueType::Json].to_vec(), + required: false, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [].to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = MigrateArgs { + client: &ctx.solana_client, + keypair: input.keypair, + mint_address: input.mint_address.to_string(), + candy_machine_id: input.candy_machine_id.map(|a| a.to_string()), + mint_list: input.mint_list, + retries: 8, + batch_size: 10, + }; + + migrate_collection(&args) + .await + .map_err(crate::Error::custom)?; + + Ok(value::to_map(&Output {})?) + } +} + +flow_lib::submit!(CommandDescription::new(MIGRATE_COLLECTION, |_| Ok( + Box::new(MigrateCollection) +))); diff --git a/archive/crates/cmds-solana/src/metaboss/mod.rs b/archive/crates/cmds-solana/src/metaboss/mod.rs new file mode 100644 index 00000000..505e18c6 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/mod.rs @@ -0,0 +1,11 @@ +pub mod burn; +pub mod burn_print; +pub mod decode; +pub mod migrate_collection; +pub mod primary_sale_happened; +pub mod set_immutable; +pub mod snapshot_cm_accounts; +pub mod snapshot_holders; +pub mod snapshot_mints; +pub mod update; +pub mod update_authority; diff --git a/archive/crates/cmds-solana/src/metaboss/primary_sale_happened.rs b/archive/crates/cmds-solana/src/metaboss/primary_sale_happened.rs new file mode 100644 index 00000000..8c1f41f9 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/primary_sale_happened.rs @@ -0,0 +1,86 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::{set_primary_sale_happened, SetPrimarySaleHappenedArgs}; + +#[derive(Clone, Debug)] +pub struct PrimarySaleHappened; + +const PRIMARY_SALE_HAPPENED: &str = "primary_sale_happened"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for PrimarySaleHappened { + fn name(&self) -> Name { + PRIMARY_SALE_HAPPENED.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = SetPrimarySaleHappenedArgs { + client: &ctx.solana_client, + keypair: Arc::new(input.keypair.clone_keypair()), + mint_account: input.mint_account, + }; + + let mut tx = set_primary_sale_happened(&args) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(PRIMARY_SALE_HAPPENED, |_| Ok( + Box::new(PrimarySaleHappened) +))); diff --git a/archive/crates/cmds-solana/src/metaboss/set_immutable.rs b/archive/crates/cmds-solana/src/metaboss/set_immutable.rs new file mode 100644 index 00000000..06389a1f --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/set_immutable.rs @@ -0,0 +1,84 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::{set_immutable, SetImmutableArgs}; + +#[derive(Clone, Debug)] +pub struct SetImmutable; + +const SET_IMMUTABLE: &str = "set_immutable"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for SetImmutable { + fn name(&self) -> Name { + SET_IMMUTABLE.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = SetImmutableArgs { + client: &ctx.solana_client, + keypair: Arc::new(input.keypair.clone_keypair()), + mint_account: input.mint_account, + }; + + let mut tx = set_immutable(args).await.map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(SET_IMMUTABLE, |_| Ok(Box::new( + SetImmutable +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/snapshot_cm_accounts.rs b/archive/crates/cmds-solana/src/metaboss/snapshot_cm_accounts.rs new file mode 100644 index 00000000..18553946 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/snapshot_cm_accounts.rs @@ -0,0 +1,77 @@ +use crate::prelude::*; +use metaboss_utils::commands::snapshot::{snapshot_cm_accounts, CandyMachineAccount}; + +#[derive(Clone, Debug)] +pub struct SnapshotCMAccounts; + +impl SnapshotCMAccounts { + pub async fn snapshot_cm_accounts( + client: &RpcClient, + update_authority: &str, + ) -> crate::Result> { + let accounts = snapshot_cm_accounts(client, update_authority) + .await + .map_err(|_| crate::Error::FailedToFetchMintSnapshot)?; + + Ok(accounts.candy_machine_accounts) + } +} + +const SNAPSHOT_CM_ACCOUNTS: &str = "snapshot_cm_accounts"; + +// Inputs +const UPDATE_AUTHORITY: &str = "update_authority"; + +// Outputs +const ACCOUNTS: &str = "accounts"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub update_authority: Pubkey, +} + +#[derive(Serialize, Debug)] +pub struct Output { + pub accounts: Vec, +} + +#[async_trait] +impl CommandTrait for SnapshotCMAccounts { + fn name(&self) -> Name { + SNAPSHOT_CM_ACCOUNTS.into() + } + + fn inputs(&self) -> Vec { + [CmdInput { + name: UPDATE_AUTHORITY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: ACCOUNTS.into(), + r#type: ValueType::Json, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let holders = + Self::snapshot_cm_accounts(&ctx.solana_client, &input.update_authority.to_string()) + .await + .map_err(crate::Error::custom)?; + + Ok(value::to_map(&Output { accounts: holders })?) + } +} + +flow_lib::submit!(CommandDescription::new(SNAPSHOT_CM_ACCOUNTS, |_| Ok( + Box::new(SnapshotCMAccounts) +))); diff --git a/archive/crates/cmds-solana/src/metaboss/snapshot_holders.rs b/archive/crates/cmds-solana/src/metaboss/snapshot_holders.rs new file mode 100644 index 00000000..c926d891 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/snapshot_holders.rs @@ -0,0 +1,139 @@ +use crate::prelude::*; +use metaboss_utils::commands::snapshot::{snapshot_holders, Holder, SnapshotHoldersArgs}; + +#[derive(Clone, Debug)] +pub struct SnapshotHolders; + +impl SnapshotHolders { + pub async fn snapshot_holders( + client: &RpcClient, + args: SnapshotHoldersArgs, + ) -> crate::Result> { + let mut holders = snapshot_holders(client, args) + .await + .map_err(|_| crate::Error::FailedToFetchMintSnapshot)?; + + holders.sort_unstable(); + + Ok(holders) + } +} + +const SNAPSHOT_MINTS: &str = "snapshot_holders"; + +// Inputs +const VALUE: &str = "value"; +const POSITION: &str = "position"; +const VALUE_TYPE: &str = "value_type"; +const ALLOW_UNVERIFIED: &str = "allow_unverified"; +const V2: &str = "v2"; + +// Outputs +const HOLDERS: &str = "holders"; + +// Value types +const CREATOR: &str = "creator"; +const UPDATE_AUTHORITY: &str = "update_authority"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub value: String, + pub position: u64, + pub value_type: String, + pub allow_unverified: bool, + pub v2: bool, +} + +#[derive(Serialize, Debug)] +pub struct Output { + pub holders: Vec, +} + +#[async_trait] +impl CommandTrait for SnapshotHolders { + fn name(&self) -> Name { + SNAPSHOT_MINTS.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: VALUE.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: POSITION.into(), + type_bounds: [ValueType::U64].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: VALUE_TYPE.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: ALLOW_UNVERIFIED.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: V2.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: HOLDERS.into(), + r#type: ValueType::Json, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let mut args = SnapshotHoldersArgs { + creator: None, + position: input.position as usize, + update_authority: None, + mint_accounts_file: None, + v2: input.v2, + allow_unverified: input.allow_unverified, + output: String::new(), + }; + + match input.value_type.as_str() { + CREATOR => { + args.creator.replace(input.value); + } + UPDATE_AUTHORITY => { + args.update_authority.replace(input.value); + } + _ => { + return Err(crate::Error::ErrorSnapshottingMints( + "an invalid value type was provided!".to_string(), + ) + .into()) + } + } + let holders = Self::snapshot_holders(&ctx.solana_client, args) + .await + .map_err(crate::Error::custom)?; + + Ok(value::to_map(&Output { holders })?) + } +} + +flow_lib::submit!(CommandDescription::new(SNAPSHOT_MINTS, |_| Ok(Box::new( + SnapshotHolders +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/snapshot_mints.rs b/archive/crates/cmds-solana/src/metaboss/snapshot_mints.rs new file mode 100644 index 00000000..8a0c0ebe --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/snapshot_mints.rs @@ -0,0 +1,145 @@ +use crate::prelude::*; +use metaboss_utils::commands::snapshot::{get_mint_accounts, SnapshotMintsArgs}; + +#[derive(Debug)] +pub struct SnapshotMints; + +impl SnapshotMints { + pub async fn snapshot_mints( + client: &RpcClient, + args: SnapshotMintsArgs, + ) -> crate::Result> { + let mut mint_addresses = get_mint_accounts( + client, + &args.creator, + args.position, + args.update_authority, + args.allow_unverified, + args.v2, + ) + .await + .map_err(|_| crate::Error::FailedToFetchMintSnapshot)?; + + mint_addresses.sort_unstable(); + + Ok(mint_addresses) + } +} + +const SNAPSHOT_MINTS: &str = "snapshot_mints"; + +// Inputs +const VALUE: &str = "value"; +const POSITION: &str = "position"; +const VALUE_TYPE: &str = "value_type"; +const ALLOW_UNVERIFIED: &str = "allow_unverified"; +const V2: &str = "v2"; + +// Outputs +const MINTS: &str = "mints"; + +// Value types +const CREATOR: &str = "creator"; +const UPDATE_AUTHORITY: &str = "update_authority"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub value: String, + pub position: u64, + pub value_type: String, + pub allow_unverified: bool, + pub v2: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub mints: Vec, +} + +#[async_trait] +impl CommandTrait for SnapshotMints { + fn name(&self) -> Name { + SNAPSHOT_MINTS.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: VALUE.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: POSITION.into(), + type_bounds: [ValueType::U64].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: VALUE_TYPE.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: ALLOW_UNVERIFIED.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: V2.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: MINTS.into(), + r#type: ValueType::Json, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let mut args = SnapshotMintsArgs { + creator: None, + position: input.position as usize, + update_authority: None, + v2: input.v2, + allow_unverified: input.allow_unverified, + output: String::new(), + }; + + match input.value_type.as_str() { + CREATOR => { + args.creator.replace(input.value); + } + UPDATE_AUTHORITY => { + args.update_authority.replace(input.value); + } + _ => { + return Err(crate::Error::ErrorSnapshottingMints( + "an invalid value type was provided!".to_string(), + ) + .into()) + } + } + let mints = Self::snapshot_mints(&ctx.solana_client, args) + .await + .map_err(crate::Error::custom)?; + + Ok(value::to_map(&Output { mints })?) + } +} + +flow_lib::submit!(CommandDescription::new(SNAPSHOT_MINTS, |_| Ok(Box::new( + SnapshotMints +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/update/creator.rs b/archive/crates/cmds-solana/src/metaboss/update/creator.rs new file mode 100644 index 00000000..d29bf957 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update/creator.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::update_creator; + +#[derive(Clone, Debug)] +pub struct UpdateCreators; + +const UPDATE_CREATORS: &str = "update_creator"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; +const CREATOR: &str = "creator"; +const APPEND: &str = "append"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub creator: Pubkey, + pub append: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for UpdateCreators { + fn name(&self) -> Name { + UPDATE_CREATORS.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: CREATOR.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: APPEND.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let mut tx = update_creator( + &ctx.solana_client, + input.keypair.clone_keypair(), + input.mint_account, + input.creator.to_string(), + input.append, + ) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_CREATORS, |_| Ok(Box::new( + UpdateCreators +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/update/data.rs b/archive/crates/cmds-solana/src/metaboss/update/data.rs new file mode 100644 index 00000000..c27b6fba --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update/data.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::update_data; +use mpl_token_metadata::state::DataV2; + +#[derive(Clone, Debug)] +pub struct UpdateData; + +const UPDATE_DATA: &str = "update_data"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; +const DATA: &str = "data"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub data: DataV2, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for UpdateData { + fn name(&self) -> Name { + UPDATE_DATA.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: DATA.into(), + type_bounds: [ValueType::Json].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + let mut tx = update_data( + &ctx.solana_client, + &input.keypair, + &input.mint_account, + input.data, + ) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_DATA, |_| Ok(Box::new( + UpdateData +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/update/mod.rs b/archive/crates/cmds-solana/src/metaboss/update/mod.rs new file mode 100644 index 00000000..83064931 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update/mod.rs @@ -0,0 +1,4 @@ +pub mod creator; +pub mod data; +pub mod name; +pub mod symbol; diff --git a/archive/crates/cmds-solana/src/metaboss/update/name.rs b/archive/crates/cmds-solana/src/metaboss/update/name.rs new file mode 100644 index 00000000..87115839 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update/name.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::update_name; + +#[derive(Debug)] +pub struct UpdateName; + +const UPDATE_NAME: &str = "update_name"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; +const NAME: &str = "name"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for UpdateName { + fn name(&self) -> Name { + UPDATE_NAME.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: NAME.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let mut tx = update_name( + &ctx.solana_client, + input.keypair.clone_keypair(), + &input.mint_account, + &input.name, + ) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_NAME, |_| Ok(Box::new( + UpdateName +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/update/symbol.rs b/archive/crates/cmds-solana/src/metaboss/update/symbol.rs new file mode 100644 index 00000000..c164241c --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update/symbol.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::update_symbol; + +#[derive(Clone, Debug)] +pub struct UpdateSymbol; + +const UPDATE_SYMBOL: &str = "update_symbol"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const MINT_ACCOUNT: &str = "mint_account"; +const SYMBOL: &str = "symbol"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub symbol: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for UpdateSymbol { + fn name(&self) -> Name { + UPDATE_SYMBOL.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: SYMBOL.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let mut tx = update_symbol( + &ctx.solana_client, + input.keypair.clone_keypair(), + &input.mint_account, + &input.symbol, + ) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_SYMBOL, |_| Ok(Box::new( + UpdateSymbol +)))); diff --git a/archive/crates/cmds-solana/src/metaboss/update_authority.rs b/archive/crates/cmds-solana/src/metaboss/update_authority.rs new file mode 100644 index 00000000..8bcbb0e6 --- /dev/null +++ b/archive/crates/cmds-solana/src/metaboss/update_authority.rs @@ -0,0 +1,106 @@ +use crate::prelude::*; +use metaboss_utils::commands::update::{set_update_authority, SetUpdateAuthorityArgs}; + +#[derive(Clone, Debug)] +pub struct UpdateAuthority; + +const UPDATE_AUTHORITY: &str = "update_authority"; + +// Inputs +const KEYPAIR: &str = "keypair"; +const PAYER: &str = "payer"; +const MINT_ACCOUNT: &str = "mint_account"; +const NEW_UPDATE_AUTHORITY: &str = "new_update_authority"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub keypair: Keypair, + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub new_update_authority: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +#[async_trait] +impl CommandTrait for UpdateAuthority { + fn name(&self) -> Name { + UPDATE_AUTHORITY.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: KEYPAIR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: NEW_UPDATE_AUTHORITY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let input: Input = value::from_map(inputs)?; + + let args = SetUpdateAuthorityArgs { + client: &ctx.solana_client, + keypair: Arc::new(input.keypair.clone_keypair()), + payer: Arc::new(input.payer), + mint_account: input.mint_account, + new_authority: input.new_update_authority, + }; + + let mut tx = set_update_authority(&args) + .await + .map_err(crate::Error::custom)?; + + let recent_blockhash = ctx.solana_client.get_latest_blockhash().await?; + try_sign_wallet(&ctx, &mut tx, &[&input.keypair], recent_blockhash).await?; + + let sig = submit_transaction(&ctx.solana_client, tx).await?; + + Ok(value::to_map(&Output { signature: sig })?) + } +} + +flow_lib::submit!(CommandDescription::new(UPDATE_AUTHORITY, |_| Ok(Box::new( + UpdateAuthority +)))); diff --git a/archive/crates/cmds-solana/src/proxy_authority/create_proxy_authority.rs b/archive/crates/cmds-solana/src/proxy_authority/create_proxy_authority.rs new file mode 100644 index 00000000..cc3a4c6a --- /dev/null +++ b/archive/crates/cmds-solana/src/proxy_authority/create_proxy_authority.rs @@ -0,0 +1,132 @@ +use super::utils::find_proxy_authority_address; +use crate::prelude::*; +use anchor_lang::InstructionData; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + system_program, +}; +use solana_sdk::pubkey::Pubkey; +use space_wrapper::instruction::CreateProxyAuthority as Proxy; + +fn create_create_proxy_instruction(proxy_authority: &Pubkey, authority: &Pubkey) -> Instruction { + let accounts = [ + AccountMeta::new(*proxy_authority, false), + AccountMeta::new(*authority, true), + AccountMeta::new(system_program::ID, false), + ] + .to_vec(); + + Instruction { + program_id: space_wrapper::ID, + accounts, + data: Proxy.data(), + } +} + +#[derive(Debug)] +pub struct CreateProxyAuthority; + +impl CreateProxyAuthority { + async fn command_create_proxy_authority( + &self, + rpc_client: &RpcClient, + payer: Pubkey, + ) -> crate::Result<(u64, Vec)> { + // TODO: get size of proxy account from space-wrapper crate + let min_rent = rpc_client + .get_minimum_balance_for_rent_exemption(44) + .await?; + + let proxy_authority = find_proxy_authority_address(&payer); + + let instruction = create_create_proxy_instruction(&proxy_authority, &payer); + + Ok((min_rent, [instruction].to_vec())) + } +} + +// Command Name +const CREATE_PROXY_AUTHORITY: &str = "create_proxy_authority"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub authority: Keypair, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + #[serde(with = "value::pubkey")] + proxy_authority: Pubkey, +} + +// Inputs +const AUTHORITY: &str = "authority"; + +// Outputs +const SIGNATURE: &str = "signature"; +const PROXY_AUTHORITY: &str = "proxy_authority"; + +#[async_trait] +impl CommandTrait for CreateProxyAuthority { + fn name(&self) -> Name { + CREATE_PROXY_AUTHORITY.into() + } + + fn inputs(&self) -> Vec { + [CmdInput { + name: AUTHORITY.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [ + CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }, + CmdOutput { + name: PROXY_AUTHORITY.into(), + r#type: ValueType::Pubkey, + }, + ] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { authority } = value::from_map(inputs)?; + + let proxy_authority = find_proxy_authority_address(&authority.pubkey()); + + let (minimum_balance_for_rent_exemption, instructions) = self + .command_create_proxy_authority(&ctx.solana_client, authority.pubkey()) + .await?; + + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &authority.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet(&ctx, &mut transaction, &[&authority], recent_blockhash).await?; + + let signature = Some(submit_transaction(&ctx.solana_client, transaction).await?); + + Ok(value::to_map(&Output { + signature, + proxy_authority, + })?) + } +} + +flow_lib::submit!(CommandDescription::new(CREATE_PROXY_AUTHORITY, |_| { + Ok(Box::new(CreateProxyAuthority)) +})); diff --git a/archive/crates/cmds-solana/src/proxy_authority/mod.rs b/archive/crates/cmds-solana/src/proxy_authority/mod.rs new file mode 100644 index 00000000..9e69c045 --- /dev/null +++ b/archive/crates/cmds-solana/src/proxy_authority/mod.rs @@ -0,0 +1,3 @@ +pub mod utils; + +pub mod create_proxy_authority; diff --git a/archive/crates/cmds-solana/src/proxy_authority/utils.rs b/archive/crates/cmds-solana/src/proxy_authority/utils.rs new file mode 100644 index 00000000..a2cc9839 --- /dev/null +++ b/archive/crates/cmds-solana/src/proxy_authority/utils.rs @@ -0,0 +1,7 @@ +use solana_sdk::pubkey::Pubkey; + +pub fn find_proxy_authority_address(authority: &Pubkey) -> Pubkey { + let (expected_pda, _bump_seed) = + Pubkey::find_program_address(&[b"proxy", &authority.to_bytes()], &space_wrapper::ID); + expected_pda +} diff --git a/archive/crates/cmds-solana/src/xnft/create_install.rs b/archive/crates/cmds-solana/src/xnft/create_install.rs new file mode 100644 index 00000000..b8a1e900 --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/create_install.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const CREATE_INSTALL: &str = "create_install"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/create_install.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(CREATE_INSTALL)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(CREATE_INSTALL, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::keypair")] + pub authority: Keypair, + #[serde(with = "value::pubkey")] + pub xnft: Pubkey, + #[serde(with = "value::keypair")] + pub target: Keypair, + #[serde(with = "value::pubkey")] + pub install_vault: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + //TODO +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + + let target = &input.target.pubkey(); + let seeds = &["install".as_ref(), target.as_ref(), input.xnft.as_ref()]; + + // Install PDA + let install = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + let accounts = xnft::accounts::CreateInstall { + xnft: input.xnft, + install_vault: input.install_vault, + install, + authority: input.authority.pubkey(), + target: input.target.pubkey(), + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = xnft::instruction::CreateInstall {}.data(); + + let minimum_balance_for_rent_exemption = + ctx.solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + xnft::accounts::CreateInstall, + >()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [ + input.authority.clone_keypair(), + input.target.clone_keypair(), + ] + .into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "install"=>install, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/xnft/create_permissioned_install.rs b/archive/crates/cmds-solana/src/xnft/create_permissioned_install.rs new file mode 100644 index 00000000..7bb43cc3 --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/create_permissioned_install.rs @@ -0,0 +1,105 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const CREATE_PERMISSIONED_INSTALL: &str = "create_permissioned_install"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/create_permissioned_install.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(CREATE_PERMISSIONED_INSTALL)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(CREATE_PERMISSIONED_INSTALL, |_| { + build() +})); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::keypair")] + pub authority: Keypair, + #[serde(with = "value::pubkey")] + pub xnft: Pubkey, + #[serde(with = "value::keypair")] + pub target: Keypair, + #[serde(with = "value::pubkey")] + pub install_vault: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + //TODO +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + + // Install PDA + let authority = &input.authority.pubkey(); + let seeds = &["install".as_ref(), authority.as_ref(), input.xnft.as_ref()]; + let install = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + // Access PDA + let seeds = &["access".as_ref(), authority.as_ref(), input.xnft.as_ref()]; + let access = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + let accounts = xnft::accounts::CreatePermissionedInstall { + xnft: input.xnft, + install_vault: input.install_vault, + install, + authority: input.authority.pubkey(), + system_program: system_program::id(), + access, + } + .to_account_metas(None); + + let data = xnft::instruction::CreatePermissionedInstall {}.data(); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + xnft::accounts::CreatePermissionedInstall, + >()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.payer.clone_keypair(), input.authority.clone_keypair()].into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "install"=>install, + "access"=>access, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/xnft/create_xnft.rs b/archive/crates/cmds-solana/src/xnft/create_xnft.rs new file mode 100644 index 00000000..c0daa823 --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/create_xnft.rs @@ -0,0 +1,134 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; +use spl_associated_token_account::get_associated_token_address; + +use super::CreateXnftParams; + +// Command Name +const CREATE_XNFT: &str = "create_xnft"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/create_xnft.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(CREATE_XNFT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(CREATE_XNFT, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + #[serde(with = "value::keypair")] + pub publisher: Keypair, + pub name: String, + pub parameters: CreateXnftParams, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + let metadata_program = mpl_token_metadata::ID; + + // Master Mint PDA + let seeds = &[ + "mint".as_ref(), + input.authority.as_ref(), + input.name.as_ref(), + ]; + let master_mint = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + // Master Token + let master_token = get_associated_token_address(&input.authority, &master_mint); + + // xNFT PDA + let seeds = &["xnft".as_ref(), master_mint.as_ref()]; + let xnft = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + // Master Metadata + let master_metadata = Pubkey::find_program_address( + &[ + "metadata".as_ref(), + metadata_program.to_bytes().as_ref(), + master_mint.as_ref(), + ], + &metadata_program, + ) + .0; + + let accounts = xnft::accounts::CreateAppXnft { + master_mint, + master_token, + master_metadata, + xnft, + payer: input.payer.pubkey(), + publisher: input.publisher.pubkey(), + system_program: system_program::id(), + token_program: spl_token::id(), + associated_token_program: spl_associated_token_account::id(), + metadata_program, + rent: solana_sdk::sysvar::rent::id(), + } + .to_account_metas(None); + + let params = xnft::state::CreateXnftParams::from(input.parameters); + + let data = xnft::instruction::CreateAppXnft { + name: input.name, + params, + } + .data(); + + let minimum_balance_for_rent_exemption = + ctx.solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + xnft::accounts::CreateAppXnft, + >()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.payer.clone_keypair(), input.publisher.clone_keypair()].into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "master_mint"=>master_mint, + "master_token"=>master_token, + "master_metadata"=>master_metadata, + "xnft"=>xnft, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/xnft/delete_install.rs b/archive/crates/cmds-solana/src/xnft/delete_install.rs new file mode 100644 index 00000000..8a5ac2bc --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/delete_install.rs @@ -0,0 +1,79 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const DELETE_INSTALL: &str = "delete_install"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/delete_install.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(DELETE_INSTALL)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(DELETE_INSTALL, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::keypair")] + pub authority: Keypair, + #[serde(with = "value::pubkey")] + pub receiver: Pubkey, + #[serde(with = "value::pubkey")] + pub install: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + + let accounts = xnft::accounts::DeleteInstall { + install: input.install, + receiver: input.receiver, + authority: input.authority.pubkey(), + } + .to_account_metas(None); + + let data = xnft::instruction::DeleteInstall {}.data(); + + let minimum_balance_for_rent_exemption = + ctx.solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + xnft::accounts::DeleteInstall, + >()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.authority.clone_keypair(), input.payer.clone_keypair()].into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/xnft/grant_access.rs b/archive/crates/cmds-solana/src/xnft/grant_access.rs new file mode 100644 index 00000000..b3b41c8d --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/grant_access.rs @@ -0,0 +1,97 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const GRANT_ACCESS: &str = "grant_access"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/grant_access.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(GRANT_ACCESS)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(GRANT_ACCESS, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::keypair")] + pub authority: Keypair, + #[serde(with = "value::pubkey")] + pub xnft: Pubkey, + #[serde(with = "value::pubkey")] + pub wallet: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + + // Access PDA + let seeds = &[ + "access".as_ref(), + input.wallet.as_ref(), + input.xnft.as_ref(), + ]; + let access = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + let accounts = xnft::accounts::GrantAccess { + xnft: input.xnft, + authority: input.authority.pubkey(), + access, + wallet: input.wallet, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = xnft::instruction::CreatePermissionedInstall {}.data(); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + xnft::accounts::CreatePermissionedInstall, + >()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.payer.clone_keypair(), input.authority.clone_keypair()].into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "access"=>access, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/archive/crates/cmds-solana/src/xnft/mod.rs b/archive/crates/cmds-solana/src/xnft/mod.rs new file mode 100644 index 00000000..bb12dd0a --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/mod.rs @@ -0,0 +1,130 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; + +pub mod create_install; +pub mod create_permissioned_install; +pub mod create_xnft; +pub mod delete_install; +pub mod grant_access; +pub mod revoke_access; + +// #[derive(Deserialize, Serialize, Debug)] +// pub enum Kind { +// App, +// Collectible, +// } + +// #[derive(Deserialize, Serialize, Debug)] +// pub enum L1 { +// Solana, +// Ethereum, +// } + +#[derive(Deserialize, Serialize, Debug)] +pub enum Tag { + None, + Defi, + Game, + Nft, +} + +impl From for xnft::state::Tag { + fn from(tag: Tag) -> Self { + match tag { + Tag::None => Self::None, + Tag::Defi => Self::Defi, + Tag::Game => Self::Game, + Tag::Nft => Self::Nfts, + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CuratorStatus { + /// The pubkey of the `Curator` program account (32). + pub pubkey: Pubkey, + /// Whether the curator's authority has verified the assignment (1). + pub verified: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CreatorsParam { + pub address: String, + pub share: u8, +} + +impl From for xnft::state::CreatorsParam { + fn from(param: CreatorsParam) -> Self { + Self { + address: Pubkey::from_str(¶m.address).unwrap(), + share: param.share, + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct CreateXnftParams { + pub creators: Vec, + pub curator: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub install_authority: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub install_price: u64, + pub install_vault: String, + pub seller_fee_basis_points: u16, + pub supply: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub symbol: String, + pub tag: Tag, + pub uri: String, +} + +impl From for xnft::state::CreateXnftParams { + fn from(params: CreateXnftParams) -> Self { + Self { + creators: params + .creators + .into_iter() + .map(|param| param.into()) + .collect(), + curator: params + .curator + .map(|curator| Pubkey::from_str(&curator).unwrap()), + install_authority: params + .install_authority + .map(|install_authority| Pubkey::from_str(&install_authority).unwrap()), + install_price: params.install_price, + install_vault: Pubkey::from_str(¶ms.install_vault).unwrap(), + seller_fee_basis_points: params.seller_fee_basis_points, + supply: params.supply, + symbol: params.symbol, + tag: params.tag.into(), + uri: params.uri, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct UpdateParams { + pub install_authority: Option, // Some("...") values are only relevant for Kind::App xNFTs + // Will remove any existing install authority is given `None` + pub install_price: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub install_vault: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub name: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub supply: Option, // Some("...") values are only relevant for Kind::App xNFTs + pub tag: Option, + pub uri: Option, +} + +impl From for xnft::state::UpdateParams { + fn from(params: UpdateParams) -> Self { + Self { + install_authority: params.install_authority, + install_price: params.install_price, + install_vault: params.install_vault, + name: params.name, + supply: params.supply, + tag: params.tag.map(|tag| tag.into()), + uri: params.uri, + } + } +} diff --git a/archive/crates/cmds-solana/src/xnft/parameters.json b/archive/crates/cmds-solana/src/xnft/parameters.json new file mode 100644 index 00000000..01a16aab --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/parameters.json @@ -0,0 +1,17 @@ +{ + "creators": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "share": 100 + } + ], + "curator": null, + "install_authority": null, + "install_price": 0, + "install_vault": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "seller_fee_basis_points": 275, + "supply": 10, + "symbol": "xNFT", + "tag": "None", + "uri": "https://www.example.com" + } \ No newline at end of file diff --git a/archive/crates/cmds-solana/src/xnft/revoke_access.rs b/archive/crates/cmds-solana/src/xnft/revoke_access.rs new file mode 100644 index 00000000..b01f9b58 --- /dev/null +++ b/archive/crates/cmds-solana/src/xnft/revoke_access.rs @@ -0,0 +1,86 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const REMOVE_ACCESS: &str = "revoke_access"; + +const DEFINITION: &str = flow_lib::node_definition!("xnft/revoke_access.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(REMOVE_ACCESS)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(REMOVE_ACCESS, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::keypair")] + pub payer: Keypair, + #[serde(with = "value::keypair")] + pub authority: Keypair, + #[serde(with = "value::pubkey")] + pub xnft: Pubkey, + #[serde(with = "value::pubkey")] + pub wallet: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let xnft_program_id = xnft::id(); + + // Access PDA + let seeds = &[ + "access".as_ref(), + input.wallet.as_ref(), + input.xnft.as_ref(), + ]; + let access = Pubkey::find_program_address(seeds, &xnft_program_id).0; + + let accounts = xnft::accounts::RevokeAccess { + xnft: input.xnft, + authority: input.authority.pubkey(), + access, + wallet: input.wallet, + } + .to_account_metas(None); + + let data = xnft::instruction::RevokeAccess {}.data(); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::()) + .await?; + + let ins = Instructions { + fee_payer: input.payer.pubkey(), + signers: [input.payer.clone_keypair(), input.authority.clone_keypair()].into(), + instructions: [Instruction { + program_id: xnft_program_id, + accounts, + data, + }] + .into(), + minimum_balance_for_rent_exemption, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/certs/supabase-prod-ca-2021.crt b/certs/supabase-prod-ca-2021.crt new file mode 100644 index 00000000..3d693669 --- /dev/null +++ b/certs/supabase-prod-ca-2021.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxDCCAqygAwIBAgIUbLxMod62P2ktCiAkxnKJwtE9VPYwDQYJKoZIhvcNAQEL +BQAwazELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5l +dyBDYXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJh +c2UgUm9vdCAyMDIxIENBMB4XDTIxMDQyODEwNTY1M1oXDTMxMDQyNjEwNTY1M1ow +azELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0RlbHdhcmUxEzARBgNVBAcMCk5ldyBD +YXN0bGUxFTATBgNVBAoMDFN1cGFiYXNlIEluYzEeMBwGA1UEAwwVU3VwYWJhc2Ug +Um9vdCAyMDIxIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQXW +QyHOB+qR2GJobCq/CBmQ40G0oDmCC3mzVnn8sv4XNeWtE5XcEL0uVih7Jo4Dkx1Q +DmGHBH1zDfgs2qXiLb6xpw/CKQPypZW1JssOTMIfQppNQ87K75Ya0p25Y3ePS2t2 +GtvHxNjUV6kjOZjEn2yWEcBdpOVCUYBVFBNMB4YBHkNRDa/+S4uywAoaTWnCJLUi +cvTlHmMw6xSQQn1UfRQHk50DMCEJ7Cy1RxrZJrkXXRP3LqQL2ijJ6F4yMfh+Gyb4 +O4XajoVj/+R4GwywKYrrS8PrSNtwxr5StlQO8zIQUSMiq26wM8mgELFlS/32Uclt +NaQ1xBRizkzpZct9DwIDAQABo2AwXjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFKjX +uXY32CztkhImng4yJNUtaUYsMB8GA1UdIwQYMBaAFKjXuXY32CztkhImng4yJNUt +aUYsMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB8spzNn+4VU +tVxbdMaX+39Z50sc7uATmus16jmmHjhIHz+l/9GlJ5KqAMOx26mPZgfzG7oneL2b +VW+WgYUkTT3XEPFWnTp2RJwQao8/tYPXWEJDc0WVQHrpmnWOFKU/d3MqBgBm5y+6 +jB81TU/RG2rVerPDWP+1MMcNNy0491CTL5XQZ7JfDJJ9CCmXSdtTl4uUQnSuv/Qx +Cea13BX2ZgJc7Au30vihLhub52De4P/4gonKsNHYdbWjg7OWKwNv/zitGDVDB9Y2 +CMTyZKG3XEu5Ghl1LEnI3QmEKsqaCLv12BnVjbkSeZsMnevJPs1Ye6TjjJwdik5P +o/bKiIz+Fq8= +-----END CERTIFICATE----- diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..30684752 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +allow-dbg-in-tests = true +allow-print-in-tests = true diff --git a/crates/cmds-deno/@space-operator b/crates/cmds-deno/@space-operator new file mode 120000 index 00000000..93791330 --- /dev/null +++ b/crates/cmds-deno/@space-operator @@ -0,0 +1 @@ +../../@space-operator \ No newline at end of file diff --git a/crates/cmds-deno/Cargo.toml b/crates/cmds-deno/Cargo.toml new file mode 100644 index 00000000..337d290a --- /dev/null +++ b/crates/cmds-deno/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "cmds-deno" +version = "0.0.0" +edition = "2021" + +[features] +default = [] +local-deps = [] + +[dependencies] +command-rpc.workspace = true +flow-lib.workspace = true + +serde_json.workspace = true +serde.workspace = true +anyhow.workspace = true + +tempfile = "3.10.1" +tokio = "1" +url = "2.5.0" +home = "0.5.9" +tracing = "0.1" +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +srpc.workspace = true +value.workspace = true + +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +actix-web = "4.5.1" diff --git a/crates/cmds-deno/deno.json b/crates/cmds-deno/deno.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/crates/cmds-deno/deno.json @@ -0,0 +1 @@ +{} diff --git a/crates/cmds-deno/deno.lock b/crates/cmds-deno/deno.lock new file mode 100644 index 00000000..05b8714c --- /dev/null +++ b/crates/cmds-deno/deno.lock @@ -0,0 +1,322 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@space-operator/flow-lib": "jsr:@space-operator/flow-lib@0.10.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.91.7", + "npm:@types/node": "npm:@types/node@18.16.19" + }, + "jsr": { + "@space-operator/flow-lib@0.10.0": { + "integrity": "d6f74303435982c9b70319bf9c75b419e1c15549c675468a75a1918b19a41fe1", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + } + }, + "npm": { + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.7": { + "integrity": "sha512-HqljZKDwk6Z4TajKRGhGLlRsbGK4S8EY27DA7v1z6yakewiUY3J7ZKDZRxcqz2MYV/ZXRrJ6wnnpiHFkPdv0WA==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.10.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.10.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-cemZ6RiDtYZpPiBzYijdOrkQQzmBCmug0E9SdRH2gIUNT15ql4mwCYWIp0VnSZq6Qrw/JkGUygp4PrK1y9KfwQ==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/crates/cmds-deno/deps.ts b/crates/cmds-deno/deps.ts new file mode 120000 index 00000000..afadfcb6 --- /dev/null +++ b/crates/cmds-deno/deps.ts @@ -0,0 +1 @@ +deps_local.ts \ No newline at end of file diff --git a/crates/cmds-deno/deps_jsr.ts b/crates/cmds-deno/deps_jsr.ts new file mode 100644 index 00000000..d22e1379 --- /dev/null +++ b/crates/cmds-deno/deps_jsr.ts @@ -0,0 +1 @@ +export * as rpc from "jsr:@space-operator/deno-command-rpc@0.10.0"; diff --git a/crates/cmds-deno/deps_local.ts b/crates/cmds-deno/deps_local.ts new file mode 100644 index 00000000..b7622f09 --- /dev/null +++ b/crates/cmds-deno/deps_local.ts @@ -0,0 +1 @@ +export * as rpc from "./@space-operator/deno-command-rpc/src/mod.ts"; diff --git a/crates/cmds-deno/node-definitions/deno_playground.json b/crates/cmds-deno/node-definitions/deno_playground.json new file mode 100644 index 00000000..97e66743 --- /dev/null +++ b/crates/cmds-deno/node-definitions/deno_playground.json @@ -0,0 +1,90 @@ +{ + "type": "native", + "data": { + "node_id": "deno_playground", + "display_name": "Deno Playground", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "input_one", + "type_bounds": ["free"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "input_two", + "type_bounds": ["free"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "input_three", + "type_bounds": ["free"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "output", + "type": "free", + "optional": false, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {}, + "targets_form.extra": { + "source": "import * as lib from \"jsr:@space-operator/flow-lib\";\nimport * as web3 from \"npm:@solana/web3.js\";\nimport { Instructions } from \"jsr:@space-operator/flow-lib/context\";\n\ninterface Inputs {\n from: web3.PublicKey;\n to: web3.PublicKey;\n amount: number;\n}\n\nexport default class Playground extends lib.BaseCommand {\n async run(\n ctx: lib.Context,\n params: Inputs,\n ): Promise> {\n const result = await ctx.execute(\n new Instructions(\n params.from,\n [params.from],\n [\n web3.SystemProgram.transfer({\n fromPubkey: params.from,\n toPubkey: params.to,\n lamports: params.amount,\n }),\n ]\n ),\n {}\n );\n\n return {\n signature: result.signature!,\n };\n }\n}\n" + } +} diff --git a/crates/cmds-deno/run.ts b/crates/cmds-deno/run.ts new file mode 100644 index 00000000..e7cb582a --- /dev/null +++ b/crates/cmds-deno/run.ts @@ -0,0 +1,5 @@ +import { rpc } from "./deps.ts"; +import NODE_DATA from "./node-data.json" with { type: "json" }; +import UserCommand from "./cmd.ts"; + +rpc.start(new UserCommand(NODE_DATA), { hostname: "127.0.0.1", port: 0 }); diff --git a/crates/cmds-deno/src/lib.rs b/crates/cmds-deno/src/lib.rs new file mode 100644 index 00000000..9059db57 --- /dev/null +++ b/crates/cmds-deno/src/lib.rs @@ -0,0 +1,208 @@ +use anyhow::Context as _; +use command_rpc::client::RpcCommandClient; +use flow_lib::{ + command::{CommandError, CommandTrait}, + config::client::NodeData, +}; +use serde::de::value::MapDeserializer; +use serde::Deserialize; +use std::process::Stdio; +use tempfile::tempdir; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, BufReader}, + process::{Child, Command}, +}; +use url::Url; + +#[derive(Deserialize)] +struct Extra { + source: String, +} + +#[cfg(feature = "local-deps")] +fn copy_dir_all( + src: impl AsRef, + dst: impl AsRef, +) -> std::pin::Pin>>> { + Box::pin(async move { + std::fs::create_dir_all(&dst)?; + let mut files = tokio::fs::read_dir(src).await?; + while let Some(entry) = files.next_entry().await? { + let ty = entry.file_type().await?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name())).await?; + } else { + tokio::fs::copy(entry.path(), dst.as_ref().join(entry.file_name())).await?; + } + } + Ok(()) + }) +} + +macro_rules! include { + ($path:expr) => { + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $path)) + }; +} + +pub async fn new(nd: &NodeData) -> Result<(Box, Child), CommandError> { + let extra = &nd.targets_form.extra.rest; + let source = Extra::deserialize(MapDeserializer::new( + extra.iter().map(|(k, v)| (k.as_str(), v)), + ))? + .source; + + let dir = tempdir()?; + + tokio::fs::write(dir.path().join("cmd.ts"), source) + .await + .context("write cmd.ts")?; + + let mut node_data = nd.clone(); + node_data.targets_form.extra.rest.remove("source"); + let node_data_json = serde_json::to_string(&node_data).context("serialize NodeData")?; + tokio::fs::write(dir.path().join("node-data.json"), node_data_json) + .await + .context("write node-data.json")?; + + tokio::fs::write(dir.path().join("run.ts"), include!("/run.ts")) + .await + .context("write run.ts")?; + + tokio::fs::write( + dir.path().join("deps.ts"), + if cfg!(feature = "local-deps") { + include!("/deps_local.ts") + } else { + include!("/deps_jsr.ts") + }, + ) + .await + .context("write deps.ts")?; + + #[cfg(feature = "local-deps")] + { + let libs = concat!(env!("CARGO_MANIFEST_DIR"), "/@space-operator"); + copy_dir_all(libs, dir.path().join("@space-operator")) + .await + .context("copy dirs")?; + } + + let deno_dir = std::env::var("DENO_DIR").unwrap_or_else(|_| { + let mut home = home::home_dir().unwrap(); + home.push(".cache"); + home.push("deno"); + home.display().to_string() + }); + + let mut spawned = Command::new("deno") + .current_dir(dir.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("DENO_DIR", &deno_dir) + .env("NO_COLOR", "1") + .kill_on_drop(true) + .arg("run") + .arg("--allow-net") + .arg("--no-prompt") + .arg("run.ts") + .spawn() + .context("spawn")?; + + let mut stdout = BufReader::new(spawned.stdout.take().unwrap()).lines(); + let port = match stdout.next_line().await? { + Some(line) => line.parse::().context("parse port")?, + None => { + let mut error = String::new(); + let mut stderr = BufReader::new(spawned.stderr.take().unwrap()); + stderr.read_to_string(&mut error).await.map_err(|error| { + tracing::warn!("read error: {}", error); + CommandError::msg("could not start command") + })?; + return Err(CommandError::msg(error)); + } + }; + let base_url = Url::parse(&format!("http://127.0.0.1:{}", port)).unwrap(); + let cmd = RpcCommandClient::new(base_url, String::new(), node_data.clone()); + tokio::spawn(async move { + while let Ok(Some(line)) = stdout.next_line().await { + tracing::debug!("{}", line); + } + }); + + Ok((Box::new(cmd), spawned)) +} + +#[cfg(test)] +mod tests { + use flow_lib::{ + config::{ + client::{Extra, Source, Target, TargetsForm}, + node::Definition, + }, + Context, + }; + use serde_json::Value as JsonValue; + use std::sync::Arc; + use uuid::Uuid; + + use super::*; + + fn node_data(def: &str, source: &str) -> NodeData { + let def = serde_json::from_str::(def).unwrap(); + NodeData { + r#type: def.r#type, + node_id: def.data.node_id, + sources: def + .sources + .into_iter() + .map(|x| Source { + id: Uuid::new_v4(), + name: x.name, + optional: x.optional, + r#type: x.r#type, + }) + .collect(), + targets: def + .targets + .into_iter() + .map(|x| Target { + id: Uuid::new_v4(), + name: x.name, + required: x.required, + passthrough: x.passthrough, + type_bounds: x.type_bounds, + }) + .collect(), + targets_form: TargetsForm { + form_data: JsonValue::Null, + wasm_bytes: None, + extra: Extra { + supabase_id: None, + rest: [("source".to_owned(), source.into())].into(), + }, + }, + instruction_info: None, + } + } + + #[actix_web::test] + async fn test_run() { + tracing_subscriber::fmt::try_init().ok(); + const SOURCE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/add.ts")); + const JSON: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/add.json")); + let nd = node_data(JSON, SOURCE); + let (cmd, child) = new(&nd).await.unwrap(); + let mut ctx = Context::default(); + Arc::get_mut(&mut ctx.extensions) + .unwrap() + .insert(srpc::Server::start_http_server().unwrap()); + let output = cmd + .run(ctx, value::map! { "a" => 12, "b" => 13 }) + .await + .unwrap(); + let c = value::from_value::(output["c"].clone()).unwrap(); + assert_eq!(c, 25.0); + drop(child); + } +} diff --git a/crates/cmds-deno/tests/add.json b/crates/cmds-deno/tests/add.json new file mode 100644 index 00000000..ae00f346 --- /dev/null +++ b/crates/cmds-deno/tests/add.json @@ -0,0 +1,83 @@ +{ + "type": "deno", + "data": { + "node_id": "deno_add", + "display_name": "Deno Add", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "a", + "type_bounds": [ + "f64" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "f64" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "c", + "type": "f64", + "optional": false, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} \ No newline at end of file diff --git a/crates/cmds-deno/tests/add.ts b/crates/cmds-deno/tests/add.ts new file mode 100644 index 00000000..dc1cbd0a --- /dev/null +++ b/crates/cmds-deno/tests/add.ts @@ -0,0 +1,7 @@ +import { Context, BaseCommand } from "jsr:@space-operator/flow-lib"; + +export default class MyCommand extends BaseCommand { + async run(_: Context, inputs: any): Promise { + return { c: inputs.a + inputs.b }; + } +} diff --git a/crates/cmds-deno/tests/deno.json b/crates/cmds-deno/tests/deno.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/crates/cmds-deno/tests/deno.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/crates/cmds-deno/tests/deno.lock b/crates/cmds-deno/tests/deno.lock new file mode 100644 index 00000000..5ae49b7a --- /dev/null +++ b/crates/cmds-deno/tests/deno.lock @@ -0,0 +1,321 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@space-operator/flow-lib": "jsr:@space-operator/flow-lib@0.9.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.91.7" + }, + "jsr": { + "@space-operator/flow-lib@0.9.0": { + "integrity": "e97a2002a192e7dfc79c10eeb7c0fc667be24ec1d77b85ac8288d25eb1c594ad", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + } + }, + "npm": { + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.7": { + "integrity": "sha512-HqljZKDwk6Z4TajKRGhGLlRsbGK4S8EY27DA7v1z6yakewiUY3J7ZKDZRxcqz2MYV/ZXRrJ6wnnpiHFkPdv0WA==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.10.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.10.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-cemZ6RiDtYZpPiBzYijdOrkQQzmBCmug0E9SdRH2gIUNT15ql4mwCYWIp0VnSZq6Qrw/JkGUygp4PrK1y9KfwQ==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/crates/cmds-pdg/Cargo.toml b/crates/cmds-pdg/Cargo.toml new file mode 100644 index 00000000..c2983ec7 --- /dev/null +++ b/crates/cmds-pdg/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cmds-pdg" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pdg-common = { workspace = true } +flow-lib = { workspace = true } + +futures = "0.3.28" +serde = { version = "1.0.159", features = ["derive"] } +serde_json = "1.0.95" +tokio-tungstenite = { version = "0.20.1", features = [ + "rustls-tls-webpki-roots", +] } +thiserror = "1.0.40" +reqwest = { version = "0.12", features = ["rustls-tls", "json"] } +once_cell = "1.17.1" +uuid = { version = "1.3.1", features = ["serde"] } +tracing = "0.1.37" +tokio = "1.33.0" +rand = "0.8" +rand_chacha = "0.3" diff --git a/crates/cmds-pdg/node-definitions/gen_metaplex_attrs.json b/crates/cmds-pdg/node-definitions/gen_metaplex_attrs.json new file mode 100644 index 00000000..60e8f419 --- /dev/null +++ b/crates/cmds-pdg/node-definitions/gen_metaplex_attrs.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "gen_metaplex_attrs", + "version": "0.1", + "display_name": "Generate Metaplex attributes", + "description": "Generate Metaplex attributes from PDG render parameters", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "attributes", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "attributes", + "type_bounds": ["object"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/gen_pdg_attrs.json b/crates/cmds-pdg/node-definitions/gen_pdg_attrs.json new file mode 100644 index 00000000..6ce4d6ef --- /dev/null +++ b/crates/cmds-pdg/node-definitions/gen_pdg_attrs.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "gen_pdg_attrs", + "version": "0.1", + "display_name": "Generate PDG attributes", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "attributes", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "attributes", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "gen_human_readable", + "type_bounds": ["bool"], + "required": false, + "passthrough": false, + "defaultValue": true, + "tooltip": "Generate human-readable attributes." + }, + { + "name": "flag", + "type_bounds": ["string"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "base, pose, effect_lottery" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/generate_base.json b/crates/cmds-pdg/node-definitions/generate_base.json new file mode 100644 index 00000000..229396d1 --- /dev/null +++ b/crates/cmds-pdg/node-definitions/generate_base.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "generate_base", + "version": "0.1", + "display_name": "Generate Base", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "attributes", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "seed", + "type_bounds": ["u64"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Optional random seed to use." + }, + { + "name": "defaults", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Default values to overwrite randomly generated values." + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/get_effect_list.json b/crates/cmds-pdg/node-definitions/get_effect_list.json new file mode 100644 index 00000000..52e219b0 --- /dev/null +++ b/crates/cmds-pdg/node-definitions/get_effect_list.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "get_effect_list", + "version": "0.1", + "display_name": "Get Effects List", + "description": "Get effects list from PDG render parameters", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "effects", + "type": "array", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "attributes", + "type_bounds": ["object"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/parse_pdg_attrs.json b/crates/cmds-pdg/node-definitions/parse_pdg_attrs.json new file mode 100644 index 00000000..6c756dc9 --- /dev/null +++ b/crates/cmds-pdg/node-definitions/parse_pdg_attrs.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "parse_pdg_attrs", + "version": "0.1", + "display_name": "Parse PDG attributes", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "attributes", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "attributes", + "type_bounds": ["object"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "check_human_readable", + "type_bounds": ["bool"], + "required": false, + "passthrough": false, + "defaultValue": true, + "tooltip": "Check human-readable attributes to see if they match." + }, + { + "name": "defaults", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Supply default values for missing attributes." + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/pdg_render.json b/crates/cmds-pdg/node-definitions/pdg_render.json new file mode 100644 index 00000000..48313b1a --- /dev/null +++ b/crates/cmds-pdg/node-definitions/pdg_render.json @@ -0,0 +1,112 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "pdg_render", + "version": "0.1", + "display_name": "PDG Render", + "description": "Render an image with PDG pipeline", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "metadata", + "type": "json", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "metadata_url", + "type": "string", + "defaultValue": "", + "tooltip": "URL to metadata file" + }, + { + "name": "main_image_url", + "type": "string", + "defaultValue": "", + "tooltip": "URL to main image" + }, + { + "name": "sketch_image_url", + "type": "string", + "defaultValue": "", + "tooltip": "URL to sketch image" + } + ], + "targets": [ + { + "name": "url", + "type_bounds": ["string"], + "required": false, + "passthrough": false, + "defaultValue": "wss://api.spaceoperator.com/pdg/render", + "tooltip": "URL to send the request to." + }, + { + "name": "rand_seed", + "type_bounds": ["string"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Random seed" + }, + { + "name": "attributes", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Attributes to override" + }, + { + "name": "headers", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "HTTP Headers" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/push_effect_list.json b/crates/cmds-pdg/node-definitions/push_effect_list.json new file mode 100644 index 00000000..cfd391f1 --- /dev/null +++ b/crates/cmds-pdg/node-definitions/push_effect_list.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "push_effect_list", + "version": "0.1", + "display_name": "Push Effects List", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "effects", + "type": "array", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "effects", + "type_bounds": ["array"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "element", + "type_bounds": ["array"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/node-definitions/update_render_params.json b/crates/cmds-pdg/node-definitions/update_render_params.json new file mode 100644 index 00000000..12255fcc --- /dev/null +++ b/crates/cmds-pdg/node-definitions/update_render_params.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_render_params", + "version": "0.1", + "display_name": "Update Render Parameters", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "params", + "type": "object", + "defaultValue": "", + "tooltip": "RenderParams" + } + ], + "targets": [ + { + "name": "params", + "type_bounds": ["object"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "RenderParams" + }, + { + "name": "effect", + "type_bounds": ["object"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Effect to apply" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-pdg/src/gen_metaplex_attrs.rs b/crates/cmds-pdg/src/gen_metaplex_attrs.rs new file mode 100644 index 00000000..ce9f9e49 --- /dev/null +++ b/crates/cmds-pdg/src/gen_metaplex_attrs.rs @@ -0,0 +1,42 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::{ + generate::{Effect, EffectsList}, + metaplex::{MetaplexAttribute, NftTraits}, + RenderParams, +}; +use serde::{Deserialize, Serialize}; + +const NAME: &str = "gen_metaplex_attrs"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("gen_metaplex_attrs.json"))?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + attributes: RenderParams, + effects: Vec, +} + +#[derive(Serialize, Debug)] +struct Output { + attributes: Vec, +} + +async fn run(_: Context, input: Input) -> Result { + let traits = NftTraits::new(&input.attributes, &EffectsList::from(input.effects)); + Ok(Output { + attributes: traits.gen_metaplex_attrs()?, + }) +} diff --git a/crates/cmds-pdg/src/gen_pdg_attrs.rs b/crates/cmds-pdg/src/gen_pdg_attrs.rs new file mode 100644 index 00000000..b3ca2870 --- /dev/null +++ b/crates/cmds-pdg/src/gen_pdg_attrs.rs @@ -0,0 +1,76 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::RenderParams; +use serde::{Deserialize, Serialize}; +use tracing::info; + +const GEN_PDG_ATTRS: &str = "gen_pdg_attrs"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("gen_pdg_attrs.json"))?.check_name(GEN_PDG_ATTRS) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(GEN_PDG_ATTRS, |_| build())); + +const fn bool_true() -> bool { + true +} + +#[derive(Deserialize, Debug)] +struct Input { + attributes: Option, + #[serde(default = "bool_true")] + gen_human_readable: bool, + flag: Option, +} + +#[derive(Serialize, Debug)] +struct Output { + attributes: serde_json::Value, +} + +async fn run(_: Context, input: Input) -> Result { + let attributes = match input.flag { + Some(flag) => match flag.as_str() { + "base" => RenderParams::generate_base(&mut rand::thread_rng()), + _ => RenderParams::default(), + }, + None => input.attributes.unwrap_or_default(), + } + .to_pdg_metadata(input.gen_human_readable)?; + + info!("{:#?}", attributes); + + Ok(Output { attributes }) +} + +#[cfg(test)] +mod tests { + use super::*; + use flow_lib::value; + + #[tokio::test] + async fn test_generate() { + let output = build() + .unwrap() + .run( + <_>::default(), + value::map! { + "flag" => "base", + }, + ) + .await + .unwrap(); + let attrs = &output["attributes"]; + let pose = value::crud::get(attrs, &["Pose", "value"]).unwrap(); + assert!(matches!(pose, flow_lib::Value::Array(_))); + } +} diff --git a/crates/cmds-pdg/src/generate_base.rs b/crates/cmds-pdg/src/generate_base.rs new file mode 100644 index 00000000..0a0d7ea4 --- /dev/null +++ b/crates/cmds-pdg/src/generate_base.rs @@ -0,0 +1,50 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::RenderParams; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use serde::{Deserialize, Serialize}; + +const NAME: &str = "generate_base"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("generate_base.json"))?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + #[serde(default)] + seed: Option, + #[serde(default)] + defaults: flow_lib::value::Map, +} + +#[derive(Serialize, Debug)] +struct Output { + attributes: RenderParams, +} + +async fn run(_: Context, input: Input) -> Result { + let mut rng = match input.seed { + Some(seed) => ChaCha20Rng::seed_from_u64(seed), + None => ChaCha20Rng::from_entropy(), + }; + + let attributes = RenderParams::generate_base(&mut rng); + + let mut map = flow_lib::value::to_map(&attributes)?; + map.extend(input.defaults.into_iter()); + let attributes = flow_lib::value::from_map(map)?; + + Ok(Output { attributes }) +} diff --git a/crates/cmds-pdg/src/get_effect_list.rs b/crates/cmds-pdg/src/get_effect_list.rs new file mode 100644 index 00000000..a8e4ce07 --- /dev/null +++ b/crates/cmds-pdg/src/get_effect_list.rs @@ -0,0 +1,42 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::{ + generate::{Effect, EffectsList}, + RenderParams, +}; +use serde::{Deserialize, Serialize}; + +const NAME: &str = "get_effect_list"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("get_effect_list.json"))?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + attributes: RenderParams, +} + +#[derive(Serialize, Debug)] +struct Output { + effects: Vec, +} + +async fn run(_: Context, input: Input) -> Result { + Ok(Output { + effects: EffectsList::from(input.attributes) + .effects + .into_iter() + .collect(), + }) +} diff --git a/crates/cmds-pdg/src/lib.rs b/crates/cmds-pdg/src/lib.rs new file mode 100644 index 00000000..6129bdfc --- /dev/null +++ b/crates/cmds-pdg/src/lib.rs @@ -0,0 +1,8 @@ +pub mod gen_metaplex_attrs; +pub mod gen_pdg_attrs; +pub mod generate_base; +pub mod get_effect_list; +pub mod parse_pdg_attrs; +pub mod pdg_render; +pub mod push_effect_list; +pub mod update_render_params; diff --git a/crates/cmds-pdg/src/parse_pdg_attrs.rs b/crates/cmds-pdg/src/parse_pdg_attrs.rs new file mode 100644 index 00000000..0b481a67 --- /dev/null +++ b/crates/cmds-pdg/src/parse_pdg_attrs.rs @@ -0,0 +1,51 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, Value, +}; +use pdg_common::nft_metadata::RenderParams; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::collections::HashMap; + +const PARSE_PDG_ATTRS: &str = "parse_pdg_attrs"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("parse_pdg_attrs.json"))? + .check_name(PARSE_PDG_ATTRS) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(PARSE_PDG_ATTRS, |_| build())); + +const fn bool_true() -> bool { + true +} + +#[derive(Deserialize, Debug)] +struct Input { + attributes: Value, + #[serde(default = "bool_true")] + check_human_readable: bool, + #[serde(default)] + defaults: HashMap, +} + +#[derive(Serialize, Debug)] +struct Output { + attributes: RenderParams, +} + +async fn run(_: Context, input: Input) -> Result { + Ok(Output { + attributes: RenderParams::from_pdg_metadata( + &mut input.attributes.into(), + input.check_human_readable, + &input.defaults, + )?, + }) +} diff --git a/crates/cmds-pdg/src/pdg_render.rs b/crates/cmds-pdg/src/pdg_render.rs new file mode 100644 index 00000000..06e139d6 --- /dev/null +++ b/crates/cmds-pdg/src/pdg_render.rs @@ -0,0 +1,230 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderError, CmdBuilder}, + CommandDescription, CommandError, + }, + context::Context, +}; +use futures::{stream::BoxStream, FutureExt, SinkExt, StreamExt}; +use once_cell::sync::Lazy; +use pdg_common::{PostReply, RenderRequest, RenderSuccess, ResultBool, WaitRequest, WorkItem}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, future::pending, time::Duration}; +use thiserror::Error as ThisError; +use tokio::{net::TcpStream, time::Instant}; +use tokio_tungstenite::{ + tungstenite::{Error as WsError, Message}, + MaybeTlsStream, WebSocketStream, +}; +use tracing::instrument::WithSubscriber; +use uuid::Uuid; + +const PDG_RENDER: &str = "pdg_render"; + +fn build() -> BuildResult { + static CACHE: Lazy> = Lazy::new(|| { + CmdBuilder::new(flow_lib::node_definition!("pdg_render.json"))?.check_name(PDG_RENDER) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(PDG_RENDER, |_| build())); + +fn default_url() -> String { + // "ws://127.0.0.1:8081/render".to_owned() + "wss://dev-api.spaceoperator.com/pdg/render".to_owned() +} + +#[derive(Serialize, Deserialize, Debug)] +struct Input { + #[serde(default = "default_url")] + url: String, + rand_seed: Option, + #[serde(default)] + attributes: HashMap, + #[serde(default)] + headers: HashMap, +} + +#[derive(Serialize, Debug)] +struct Output { + main_image_url: String, + sketch_image_url: String, + metadata_url: String, + metadata: flow_lib::Value, +} + +#[derive(ThisError, Debug)] +#[error("server disconnected")] +struct Disconnected; + +fn run_ws( + ws: WebSocketStream>, +) -> BoxStream<'static, Result> { + // ping every 30 secs + const PING_INTERVAL: Duration = Duration::from_secs(30); + // if server doesn't pong in 8 secs, consider it dead + const PONG_TIMEOUT: Duration = Duration::from_secs(8); + + let (text_tx, text_rx) = futures::channel::mpsc::unbounded(); + let (mut write, mut read) = ws.split(); + tokio::spawn( + async move { + let mut pong_deadline = pending::<()>().boxed(); + let mut ping_interval = + tokio::time::interval_at(Instant::now() + PING_INTERVAL, PING_INTERVAL); + ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + tokio::select!( + _ = ping_interval.tick() => { + if let Err(error) = write.send(Message::Ping(Vec::new())).await { + text_tx.unbounded_send(Err(error)).ok(); + break; + } + pong_deadline = Box::pin(tokio::time::sleep_until(Instant::now() + PONG_TIMEOUT)); + }, + _ = &mut pong_deadline => { + text_tx.unbounded_send(Err(WsError::ConnectionClosed)).ok(); + break; + }, + res = read.next() => { + match res { + Some(Ok(msg)) => { + tracing::trace!("received message: {:?}", msg); + pong_deadline = pending::<()>().boxed(); + if let Message::Text(text) = msg { + if text_tx.unbounded_send(Ok(text)).is_err() { + break; + } + } + } + Some(Err(error)) => { + text_tx.unbounded_send(Err(error)).ok(); + break; + } + None => { + break; + } + } + } + ); + } + } + .with_current_subscriber() + ); + + text_rx.boxed() +} + +async fn ws_wait( + render_url: &str, + request_uuid: Uuid, +) -> Result>, CommandError> { + let url = match render_url.strip_suffix("/render") { + Some(s) => format!("{}/wait", s), + None => return Err(CommandError::msg("could not build URL for waiting")), + }; + let (mut ws, _) = tokio_tungstenite::connect_async(&url).await?; + ws.send(serde_json::to_string(&WaitRequest { request_uuid })?.into()) + .await?; + + Ok(run_ws(ws)) +} + +async fn run(_: Context, input: Input) -> Result { + let (mut ws, _) = tokio_tungstenite::connect_async(&input.url).await?; + + let rand_seed = input.rand_seed.or_else(|| { + Some( + input + .attributes + .get("wedgeindex")? + .pointer("/value/0")? + .as_i64()? + .to_string(), + ) + }); + + // send the request + ws.send({ + tracing::debug!( + "rand_seed={}", + &rand_seed.as_ref().unwrap_or(&"".to_owned()) + ); + let text = serde_json::to_string({ + &RenderRequest { + rand_seed, + version: "6".to_owned(), + workitem: WorkItem { + attributes: input.attributes, + ..<_>::default() + }, + } + })?; + tracing::debug!("{}", text); + text.into() + }) + .await?; + + let mut text_stream = run_ws(ws); + + let id = serde_json::from_str::>( + &text_stream.next().await.ok_or(Disconnected)??, + )? + .into_result()?? + .request_uuid; + tracing::info!("request_uuid={}", id); + + let mut tries = 10; + + let res = loop { + let text = match text_stream.next().await { + Some(Ok(text)) => text, + Some(Err(error)) => { + tracing::warn!("error: {}, reconnecting", error); + tries -= 1; + if tries == 0 { + return Err(CommandError::msg("too many errors")); + } + tokio::time::sleep(Duration::from_millis(200)).await; + text_stream = ws_wait(&input.url, id).await?; + continue; + } + None => { + tracing::warn!("connection closed, reconnecting"); + tries -= 1; + if tries == 0 { + return Err(CommandError::msg("too many errors")); + } + tokio::time::sleep(Duration::from_millis(200)).await; + text_stream = ws_wait(&input.url, id).await?; + continue; + } + }; + let res = serde_json::from_str::>(&text)?.into_result()??; + break res; + }; + + let metadata = reqwest::get(&res.metadata_url) + .await? + .json::() + .await?; + + Ok(Output { + main_image_url: res.main_image_url, + sketch_image_url: res.sketch_image_url, + metadata_url: res.metadata_url, + metadata: metadata.into(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-pdg/src/push_effect_list.rs b/crates/cmds-pdg/src/push_effect_list.rs new file mode 100644 index 00000000..20aae882 --- /dev/null +++ b/crates/cmds-pdg/src/push_effect_list.rs @@ -0,0 +1,39 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::generate::{Effect, EffectsList}; +use serde::{Deserialize, Serialize}; + +const NAME: &str = "push_effect_list"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("push_effect_list.json"))?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + effects: Vec, + element: Effect, +} + +#[derive(Serialize, Debug)] +struct Output { + effects: Vec, +} + +async fn run(_: Context, input: Input) -> Result { + let mut e = EffectsList::from(input.effects); + e.push(input.element); + Ok(Output { + effects: e.effects.into_iter().collect(), + }) +} diff --git a/crates/cmds-pdg/src/update_render_params.rs b/crates/cmds-pdg/src/update_render_params.rs new file mode 100644 index 00000000..d74c2b3b --- /dev/null +++ b/crates/cmds-pdg/src/update_render_params.rs @@ -0,0 +1,48 @@ +use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, + }, + Context, +}; +use pdg_common::nft_metadata::{generate::Effect, RenderParams}; +use serde::{Deserialize, Serialize}; + +const NAME: &str = "update_render_params"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("update_render_params.json"))?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + params: RenderParams, + effect: Effect, +} + +#[derive(Serialize, Debug)] +struct Output { + params: RenderParams, +} + +async fn run(_: Context, mut input: Input) -> Result { + input.params.add_effect(input.effect); + Ok(Output { + params: input.params, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/Cargo.toml b/crates/cmds-solana/Cargo.toml new file mode 100644 index 00000000..f595be4a --- /dev/null +++ b/crates/cmds-solana/Cargo.toml @@ -0,0 +1,128 @@ +[package] +name = "cmds-solana" +version = "0.0.0" +edition = "2021" + +[dependencies] +value = { workspace = true } +flow-lib = { workspace = true } + +async-trait = "0.1" +tiny-bip39 = "0.8" +serde = { version = "1", features = ["derive"] } +rust_decimal = { version = "1", features = ["maths"] } +bincode = "1" +base64 = "0.13" +serde_json = "1" +reqwest = { version = "0.12", features = [ + "json", + "rustls-tls", + "gzip", + "multipart", +] } +tokio = { version = "1", features = ["time", "macros"] } +mime_guess = "2" +borsh = "0.10.4" +bytes = "1" +ed25519-dalek = "1" +futures = "0.3" +thiserror = "1" +anyhow = "1" +serde_with = "3.1.0" +bs58 = "0.4" +tracing = "0.1" +once_cell = "1.17" +rand = "0.8" +hex = "0.4.3" +byteorder = "1.4.3" +primitive-types = { version = "0.9.0", default-features = false } +chrono = "0.4.33" +bytemuck = { version = "1.16.1" } +bytemuck_derive = { version = "1.7.0" } +# branch: dev +struct-convert = { git = "https://github.com/juchiast/struct-convert", rev = "83b132915519515e364ce612ef2ac92441dc7bc9" } + +# solana libs +solana-sdk = "=1.18.26" +solana-client = "=1.18.26" +solana-program = "=1.18.26" +solana-account-decoder = "=1.18.26" +solana-transaction-status = '=1.18.26' +spl-token = { version = "=4.0.0", features = ["no-entrypoint"] } +spl-token-2022 = { version = "=3.0.4", features = ["serde-traits"] } +spl-memo = { version = "=4.0.0", features = ["no-entrypoint"] } +spl-associated-token-account = { version = "=2.3.0", features = ["no-entrypoint"] } +spl-account-compression = "=0.4.1" +spl-noop = "=0.2.0" +spl-pod = "=0.2.5" + +# metaplex +mpl-token-auth-rules = "=1.5.1" +mpl-token-metadata = "=4.1.2" +mpl-candy-machine-core = { version = "=3.0.1", features = ["no-entrypoint"] } +mpl-candy-guard = { version = "=3.0.0", features = ["no-entrypoint"] } +# core, core candy machine and candy guard +mpl-core-candy-machine-core = { version = "=0.2.1", features = ["no-entrypoint"] } +mpl-core-candy-guard = { version = "=0.2.1", features = ["no-entrypoint"] } +mpl-core = { version = "=0.8.0", features = ["serde"] } +mpl-bubblegum = { version = "=1.4.0" } + +# bundlr +bundlr-sdk = { version = "=0.3.0", default-features = false, features = [ + "solana", +] } + +# branch: vendor +pyth-sdk-solana = { git = "https://github.com/space-operator/pyth-sdk-rs", rev = "84cc6fe07acbb8a81b216228be244bf039621560" } +jupiter-swap-api-client = { git = "https://github.com/jup-ag/jupiter-swap-api-client.git", package = "jupiter-swap-api-client", rev = "e3c162b921f46eacc867ca9c55e7d610eefabe25"} + +# anchor +anchor-lang = "=0.30.1" +anchor-spl = "=0.30.1" + +# clockwork-client = "=2.0.1" +# clockwork-utils = "=2.0.1" +# clockwork-thread-program = "=2.0.1" +# clockwork-sdk = "=2.0.1" + +# [dependencies.clockwork-thread-program] +# git = "https://github.com/space-operator/clockwork.git" +# rev = "efd73675f670bfc0aab551d89c741e18c8cfed10" +# [dependencies.clockwork-thread-program] +# git = "https://github.com/space-operator/clockwork.git" +# rev = "efd73675f670bfc0aab551d89c741e18c8cfed10" + +# [dependencies.payments] +# git = "https://github.com/clockwork-xyz/examples.git" +# rev = "1b65e8185796b4eac009750c0a0b8128576201ad" + +# [dependencies.metaboss_utils] +# git = "https://github.com/space-operator/metaboss_utils.git" +# rev = "5e0a4c9bcd16d2f062549e190e928bebb135537f" + +# [dependencies.space-wrapper] +# git = "https://github.com/space-operator/space-wrapper" +# rev = "eb257a070de0545d9051d9b9bb530df3e8736570" +# features = ["cpi"] + +# [dependencies.xnft] +# git = "https://github.com/coral-xyz/xnft" +# rev = "6ff7a8e7b1bfa0024cdea8269df92bca681d68ec" +# features = ["no-entrypoint"] + +[dependencies.wormhole-sdk] +git = "https://github.com/space-operator/wormhole" +rev = "b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1" + +[dependencies.serde_wormhole] +git = "https://github.com/space-operator/wormhole" +rev = "b209022b85d8e6cbf4e37b059bfe3ce7fa11c6e1" + +# [dependencies.wormhole-anchor-sdk] +# git = "https://github.com/wormhole-foundation/wormhole-scaffolding.git" +# rev = "7756f517fd63abae4be9b41ed82723def22bad09" + +[dev-dependencies] +rust_decimal_macros = "1.26" +inventory = "0.3" +tracing-subscriber = "0.3" diff --git a/crates/cmds-solana/node-definitions/associated_token_account.json b/crates/cmds-solana/node-definitions/associated_token_account.json new file mode 100644 index 00000000..36ef78bf --- /dev/null +++ b/crates/cmds-solana/node-definitions/associated_token_account.json @@ -0,0 +1,100 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "associated_token_account", + "version": "0.1", + "display_name": "Associated Token Account", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/associated_token_account.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "associated_token_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/clockwork/payments/create_payment.json b/crates/cmds-solana/node-definitions/clockwork/payments/create_payment.json new file mode 100644 index 00000000..5a6febd6 --- /dev/null +++ b/crates/cmds-solana/node-definitions/clockwork/payments/create_payment.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_payment", + "version": "0.1", + "display_name": "Create Payment", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "thread", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "trigger", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/clockwork/payments/disburse_payment_ix.json b/crates/cmds-solana/node-definitions/clockwork/payments/disburse_payment_ix.json new file mode 100644 index 00000000..76e29db8 --- /dev/null +++ b/crates/cmds-solana/node-definitions/clockwork/payments/disburse_payment_ix.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "disburse_payment_ix", + "version": "0.1", + "display_name": "Disburse Payment Instruction", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "instruction", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority_token_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "payment", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "thread", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "recipient_ata", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/clockwork/payments/update_payment.json b/crates/cmds-solana/node-definitions/clockwork/payments/update_payment.json new file mode 100644 index 00000000..ba97b38f --- /dev/null +++ b/crates/cmds-solana/node-definitions/clockwork/payments/update_payment.json @@ -0,0 +1,94 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_payment", + "version": "0.1", + "display_name": "Update Payment", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/clockwork/threads/thread_create.json b/crates/cmds-solana/node-definitions/clockwork/threads/thread_create.json new file mode 100644 index 00000000..d6be6f12 --- /dev/null +++ b/crates/cmds-solana/node-definitions/clockwork/threads/thread_create.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "thread_create", + "version": "0.1", + "display_name": "Thread Create", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "thread", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "id", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "thread_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "instructions", + "type_bounds": ["array"], + "required": true, + "defaultValue": null, + "tooltip": "instruction array", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "amount to fund the thread account with", + "passthrough": false + }, + { + "name": "trigger", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "trigger type, https://docs.clockwork.xyz/developers/threads/triggers", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/clockwork/threads/thread_delete.json b/crates/cmds-solana/node-definitions/clockwork/threads/thread_delete.json new file mode 100644 index 00000000..5781a473 --- /dev/null +++ b/crates/cmds-solana/node-definitions/clockwork/threads/thread_delete.json @@ -0,0 +1,94 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "thread_delete", + "version": "0.1", + "display_name": "Thread Delete", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "thread_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "thread", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "close_to", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/burn_cNFT.json b/crates/cmds-solana/node-definitions/compression/burn_cNFT.json new file mode 100644 index 00000000..d0ff6ba6 --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/burn_cNFT.json @@ -0,0 +1,142 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "burn_cNFT", + "version": "0.1", + "display_name": "Burn cNFT", + "description": "", + "tags": ["compression", "NFT"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/compression/burn.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "leaf_owner", + "type_bounds": ["keypair", "pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_delegate", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "root", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "data_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "creator_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_id", + "type_bounds": ["u64"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "index", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/create_tree.json b/crates/cmds-solana/node-definitions/compression/create_tree.json new file mode 100644 index 00000000..7af8483e --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/create_tree.json @@ -0,0 +1,132 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_tree", + "version": "0.1", + "display_name": "Create Tree", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "tree_config", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "creator", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "max_depth", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "max_buffer", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "canopy_levels", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "is_public", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "If public, anyone will be able to mint Compressed NFTs from it. Otherwise, only the Tree Creator or the Tree Delegate can.", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/mint_compressed_NFT.json b/crates/cmds-solana/node-definitions/compression/mint_compressed_NFT.json new file mode 100644 index 00000000..dd93a9c7 --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/mint_compressed_NFT.json @@ -0,0 +1,156 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint_compressed_NFT", + "version": "0.1", + "display_name": "Mint Compressed NFT", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "id", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "nonce", + "type": "u64", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "creator_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "data_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "leaf_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "creator_or_delegate", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "tree_config", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_delegate", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "metadata", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/mint_to_collection_v1.json b/crates/cmds-solana/node-definitions/compression/mint_to_collection_v1.json new file mode 100644 index 00000000..b7821294 --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/mint_to_collection_v1.json @@ -0,0 +1,180 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint_cNFT_to_collection", + "version": "0.1", + "display_name": "Mint cNFT to Collection", + "description": "", + "tags": ["compression", "NFT"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "id", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "nonce", + "type": "u64", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "creator_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "data_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "leaf_hash", + "type": "bytes", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "creator_or_delegate", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "is_delegate_authority", + "type_bounds": ["bool"], + "required": false, + "defaultValue": false, + "tooltip": "", + "passthrough": false + }, + { + "name": "tree_config", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_delegate", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "metadata", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/transfer.json b/crates/cmds-solana/node-definitions/compression/transfer.json new file mode 100644 index 00000000..6b172eaa --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/transfer.json @@ -0,0 +1,166 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_cNFT", + "version": "0.1", + "display_name": "Transfer cNFT", + "description": "", + "tags": ["compression", "NFT"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "leaf_owner", + "type_bounds": ["keypair", "pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "new_leaf_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_delegate", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "tree_config", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "root", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "data_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "creator_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_id", + "type_bounds": ["u64"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "index", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "proof", + "type_bounds": ["json"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/compression/update_cNFT.json b/crates/cmds-solana/node-definitions/compression/update_cNFT.json new file mode 100644 index 00000000..1dbab368 --- /dev/null +++ b/crates/cmds-solana/node-definitions/compression/update_cNFT.json @@ -0,0 +1,166 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_cNFT", + "version": "0.1", + "display_name": "Update cNFT", + "description": "", + "tags": ["compression", "NFT"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "leaf_owner", + "type_bounds": ["keypair", "pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "new_leaf_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_delegate", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "tree_config", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "merkle_tree", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "root", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "data_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "creator_hash", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "leaf_id", + "type_bounds": ["u64"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "index", + "type_bounds": ["u32"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "proof", + "type_bounds": ["json"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/create_mint_account.json b/crates/cmds-solana/node-definitions/create_mint_account.json new file mode 100644 index 00000000..ce2960d2 --- /dev/null +++ b/crates/cmds-solana/node-definitions/create_mint_account.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_mint_account", + "version": "0.1", + "display_name": "Create Mint Account", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/create_mint_account.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Who pays for account rent and transaction fees", + "passthrough": true + }, + { + "name": "decimals", + "type_bounds": ["u8"], + "required": true, + "defaultValue": null, + "tooltip": "NFTs should have decimal = 0\nUS dollars have 2 decimals\nFrom Metaplex documentation:\n'If the token has a master edition it is a NonFungible. If the token has no master edition(ensuring its supply can be > 1) and decimals of 0 it is a FungibleAsset. If the token has no master edition(ensuring its supply can be > 1) and decimals of > 0 it is a Fungible. If the token is a limited edition of a MasterEditon it is a NonFungibleEdition.'", + "passthrough": false + }, + { + "name": "mint_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Mint authority - who can mint more tokens", + "passthrough": true + }, + { + "name": "freeze_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "memo", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "Additional notes", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/create_proxy_authority.json b/crates/cmds-solana/node-definitions/create_proxy_authority.json new file mode 100644 index 00000000..447ff8e4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/create_proxy_authority.json @@ -0,0 +1,76 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_proxy_authority", + "version": "0.1", + "display_name": "Create Proxy Authority", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proxy_authority", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/create_token_account.json b/crates/cmds-solana/node-definitions/create_token_account.json new file mode 100644 index 00000000..c662e2cd --- /dev/null +++ b/crates/cmds-solana/node-definitions/create_token_account.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_token_account", + "version": "0.1", + "display_name": "Create Token Account", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/create_token_account.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_account", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/das_api.json b/crates/cmds-solana/node-definitions/das_api.json new file mode 100644 index 00000000..dc96cde7 --- /dev/null +++ b/crates/cmds-solana/node-definitions/das_api.json @@ -0,0 +1,94 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "das_api", + "version": "0.1", + "display_name": "DAS API", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/das.rs.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "response", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "url", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "id", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "method", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "GetAsset, GetAssetProof, GetAssetsByOwner, GetAssetsByCreator, GetAssetsByAuthority, GetAssetsbyCreator, GetAssetsByGroup, SearchAssets, GetSignaturesForAsset, GetTokenAccounts", + "passthrough": true + }, + { + "name": "params", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/find_pda.json b/crates/cmds-solana/node-definitions/find_pda.json new file mode 100644 index 00000000..4202d904 --- /dev/null +++ b/crates/cmds-solana/node-definitions/find_pda.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "find_pda", + "version": "0.1", + "display_name": "Find PDA", + "description": "Find the PDA address for a given program ID and seeds", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "pda", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "program_id", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed_1", + "type_bounds": ["string", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed_2", + "type_bounds": ["string", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed_3", + "type_bounds": ["string", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed_4", + "type_bounds": ["string", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed_5", + "type_bounds": ["string", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/generate_keypair.json b/crates/cmds-solana/node-definitions/generate_keypair.json new file mode 100644 index 00000000..b6a225b3 --- /dev/null +++ b/crates/cmds-solana/node-definitions/generate_keypair.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "generate_keypair", + "version": "0.1", + "display_name": "Generate Keypair", + "description": "Generate or load a keypair and it's pubkey.\n\nWill generate a random keypair every run if no inputs are provided. This is useful for testing purpose.", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "pubkey", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "seed", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "12 word BIP39 mnemonic seed phrase", + "passthrough": false + }, + { + "name": "private_key", + "type_bounds": ["keypair", "pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "Load using a base 58 string, ignores seed/passphrase", + "passthrough": false + }, + { + "name": "passphrase", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/get_balance.json b/crates/cmds-solana/node-definitions/get_balance.json new file mode 100644 index 00000000..04e03ec2 --- /dev/null +++ b/crates/cmds-solana/node-definitions/get_balance.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "get_balance", + "version": "0.1", + "display_name": "Get Balance", + "description": "Get the balance of the account", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "balance", + "type": "u64", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/add_required_signatory.json b/crates/cmds-solana/node-definitions/governance/add_required_signatory.json new file mode 100644 index 00000000..2c50d5db --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/add_required_signatory.json @@ -0,0 +1,100 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "add_required_signatory", + "version": "0.1", + "display_name": "Add Required Signatory", + "description": "Adds a required signatory to the Governance, which will be applied all proposals created with it", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/add_required_signatory.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "required_signatory_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signatory", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/add_signatory.json b/crates/cmds-solana/node-definitions/governance/add_signatory.json new file mode 100644 index 00000000..b2c1bc68 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/add_signatory.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "add_signatory", + "version": "0.1", + "display_name": "Add Signatory", + "description": "Add a Signatory to a proposal. Cannot leave draft until Signatory signs", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/add_signatory.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signatory_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "Proposal account associated with the governance", + "passthrough": false + }, + { + "name": "signatory", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "Signatory record account", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "Governance Authority (Token Owner or Governance Delegate)", + "passthrough": false + }, + { + "name": "add_signatory_authority", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/cancel_proposal.json b/crates/cmds-solana/node-definitions/governance/cancel_proposal.json new file mode 100644 index 00000000..9fbbf07a --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/cancel_proposal.json @@ -0,0 +1,119 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "cancel_proposal", + "version": "0.1", + "display_name": "Cancel Proposal", + "description": "Change proposal state to Cancelled", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/cancel_proposal.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_owner_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "TokenOwnerRecord account of the Proposal owner", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Governance Authority (Token Owner or Governance Delegate)", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/cast_vote.json b/crates/cmds-solana/node-definitions/governance/cast_vote.json new file mode 100644 index 00000000..6d346e50 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/cast_vote.json @@ -0,0 +1,164 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "cast_vote", + "version": "0.1", + "display_name": "Cast Vote", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/cast_vote.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vote_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "TokenOwnerRecord of the Proposal owner", + "passthrough": false + }, + { + "name": "voter_token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "TokenOwnerRecord of the voter", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Token Owner or Governance Delegate", + "passthrough": false + }, + { + "name": "vote_governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "max_voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vote", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/complete_proposal.json b/crates/cmds-solana/node-definitions/governance/complete_proposal.json new file mode 100644 index 00000000..361a0833 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/complete_proposal.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "complete_proposal", + "version": "0.1", + "display_name": "Complete Proposal", + "description": "Transitions an off-chain or manually executable Proposal from Succeeded into Completed state", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/complete_proposal.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "TokenOwnerRecord of the Proposal Owner", + "passthrough": true + }, + { + "name": "complete_proposal_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Token Owner or Delegate", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/create_governance.json b/crates/cmds-solana/node-definitions/governance/create_governance.json new file mode 100644 index 00000000..c862caca --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/create_governance.json @@ -0,0 +1,132 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_governance", + "version": "0.1", + "display_name": "Create Governance", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_governance.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "governance_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance_seed", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "create_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "config", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/create_native_treasury.json b/crates/cmds-solana/node-definitions/governance/create_native_treasury.json new file mode 100644 index 00000000..7c3f8d2b --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/create_native_treasury.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_native_treasury", + "version": "0.1", + "display_name": "Create Native Treasury", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_native_treasury.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "native_treasury_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/create_proposal.json b/crates/cmds-solana/node-definitions/governance/create_proposal.json new file mode 100644 index 00000000..efb283b4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/create_proposal.json @@ -0,0 +1,186 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_proposal", + "version": "0.1", + "display_name": "Create Proposal", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_proposal.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_deposit_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "description_link", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vote_type", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "use_deny_option", + "type_bounds": ["bool"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "options", + "type_bounds": ["array of strings"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_seed", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/create_realm.json b/crates/cmds-solana/node-definitions/governance/create_realm.json new file mode 100644 index 00000000..954664d3 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/create_realm.json @@ -0,0 +1,154 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_realm", + "version": "0.1", + "display_name": "Create Realm", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_realm.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "community_token_holding", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "community_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "council_token_mint", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "community_token_config_args", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "council_token_config_args", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "min_weight", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "max_weight_source", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/create_token_owner_record.json b/crates/cmds-solana/node-definitions/governance/create_token_owner_record.json new file mode 100644 index 00000000..f7ba500e --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/create_token_owner_record.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_token_owner_record", + "version": "0.1", + "display_name": "Create Token Owner Record", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_token_owner_record.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_owner_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/deposit_governing_tokens.json b/crates/cmds-solana/node-definitions/governance/deposit_governing_tokens.json new file mode 100644 index 00000000..5a6559ac --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/deposit_governing_tokens.json @@ -0,0 +1,136 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "deposit_governing_tokens", + "version": "0.1", + "display_name": "Deposit Governing Tokens", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/deposit_governing_tokens.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm_config_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "governing_token_holding_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_owner_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_source_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/execute_transaction.json b/crates/cmds-solana/node-definitions/governance/execute_transaction.json new file mode 100644 index 00000000..01044306 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/execute_transaction.json @@ -0,0 +1,156 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "execute_transaction", + "version": "0.1", + "display_name": "Execute Transaction", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/execute_transaction.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_transaction_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal_transaction", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "instruction_program_id", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "instruction_accounts", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "additional_signers", + "type_bounds": ["array of strings"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signer_1", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signer_2", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signer_3", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/finalize_vote.json b/crates/cmds-solana/node-definitions/governance/finalize_vote.json new file mode 100644 index 00000000..eef2d6b9 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/finalize_vote.json @@ -0,0 +1,132 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "finalize_vote", + "version": "0.1", + "display_name": "Finalize Vote", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/finalize_vote.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vote_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "TokenOwnerRecord of the Proposal owner", + "passthrough": false + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "max_voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/insert_transaction.json b/crates/cmds-solana/node-definitions/governance/insert_transaction.json new file mode 100644 index 00000000..73018046 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/insert_transaction.json @@ -0,0 +1,140 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "insert_transaction", + "version": "0.1", + "display_name": "Insert Transaction", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/insert_transaction.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_transaction_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "option_index", + "type_bounds": ["u8"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "index", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "instructions", + "type_bounds": ["instructions"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/post_message.json b/crates/cmds-solana/node-definitions/governance/post_message.json new file mode 100644 index 00000000..d48c5125 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/post_message.json @@ -0,0 +1,150 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "governance_post_message", + "version": "0.1", + "display_name": "Post Message", + "description": "", + "tags": ["governance", "solana", "chat"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/post_message.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "chat_message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "body", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "reply_to", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "voter_weight_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/refund_proposal_deposit.json b/crates/cmds-solana/node-definitions/governance/refund_proposal_deposit.json new file mode 100644 index 00000000..c25f3388 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/refund_proposal_deposit.json @@ -0,0 +1,100 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "refund_proposal_deposit", + "version": "0.1", + "display_name": "Refund Proposal Deposit", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/refund_proposal_deposit.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_deposit_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal_deposit_payer", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/relinquish_token_owner_record_locks.json b/crates/cmds-solana/node-definitions/governance/relinquish_token_owner_record_locks.json new file mode 100644 index 00000000..7f312792 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/relinquish_token_owner_record_locks.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "relinquish_token_owner_record_locks", + "version": "0.1", + "display_name": "Relinquish Token Owner Locks", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/relinquish_token_owner_record_locks.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm_config_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record_lock_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "lock_ids", + "type_bounds": ["array of u8"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/relinquish_vote.json b/crates/cmds-solana/node-definitions/governance/relinquish_vote.json new file mode 100644 index 00000000..3172037f --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/relinquish_vote.json @@ -0,0 +1,140 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "relinquish_vote", + "version": "0.1", + "display_name": "Relinquish Vote", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/relinquish_vote.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vote_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "TokenOwnerRecord of the Proposal owner", + "passthrough": false + }, + { + "name": "vote_governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "Token Owner or Governance Delegate", + "passthrough": false + }, + { + "name": "beneficiary", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/remove_required_signatory.json b/crates/cmds-solana/node-definitions/governance/remove_required_signatory.json new file mode 100644 index 00000000..60122436 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/remove_required_signatory.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "remove_required_signatory", + "version": "0.1", + "display_name": "Remove Required Signatory", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/add_required_signatory.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "required_signatory_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signatory", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "beneficiary", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/remove_transaction.json b/crates/cmds-solana/node-definitions/governance/remove_transaction.json new file mode 100644 index 00000000..b02c436b --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/remove_transaction.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "remove_transaction", + "version": "0.1", + "display_name": "Remove Transaction", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/remove_transaction.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "proposal_transaction_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_transaction", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "beneficiary", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/revoke_governing_tokens.json b/crates/cmds-solana/node-definitions/governance/revoke_governing_tokens.json new file mode 100644 index 00000000..37217141 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/revoke_governing_tokens.json @@ -0,0 +1,136 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "revoke_governing_tokens", + "version": "0.1", + "display_name": "Revoke Governing Tokens", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/revoke_governing_tokens.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm_config_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "governing_token_holding_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_owner_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "revoke_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/set_governance_config.json b/crates/cmds-solana/node-definitions/governance/set_governance_config.json new file mode 100644 index 00000000..145c09b8 --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/set_governance_config.json @@ -0,0 +1,94 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_governance_config", + "version": "0.1", + "display_name": "Set Governance Config", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/set_governance_config.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "config", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/set_governance_delegate.json b/crates/cmds-solana/node-definitions/governance/set_governance_delegate.json new file mode 100644 index 00000000..0441c65e --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/set_governance_delegate.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_governance_delegate", + "version": "0.1", + "display_name": "Set Governance Delegate", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/set_governance_delegate.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vote_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "new_governance_delegate", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/set_realm_authority.json b/crates/cmds-solana/node-definitions/governance/set_realm_authority.json new file mode 100644 index 00000000..484c6f6f --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/set_realm_authority.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_realm_authority", + "version": "0.1", + "display_name": "Set Realm Authority", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/create_realm.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "new_realm_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "action", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/set_realm_config.json b/crates/cmds-solana/node-definitions/governance/set_realm_config.json new file mode 100644 index 00000000..8b28eafa --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/set_realm_config.json @@ -0,0 +1,142 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_realm_config", + "version": "0.1", + "display_name": "Set Realm Config", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/set_realm_config.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "community_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "council_token_mint", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "community_token_config_args", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "council_token_config_args", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "min_weight", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "max_weight_source", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/set_token_owner_record_locks.json b/crates/cmds-solana/node-definitions/governance/set_token_owner_record_locks.json new file mode 100644 index 00000000..fa36b78d --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/set_token_owner_record_locks.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_token_owner_record_locks", + "version": "0.1", + "display_name": "Set Token Owner Record Locks", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/set_token_owner_record_locks.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm_config_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_owner_record_lock_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "lock_ids", + "type_bounds": ["array of u8"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "expiry", + "type_bounds": ["i64"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/sign_off_proposal.json b/crates/cmds-solana/node-definitions/governance/sign_off_proposal.json new file mode 100644 index 00000000..94c5eb3d --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/sign_off_proposal.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "sign_off_proposal", + "version": "0.1", + "display_name": "Sign Off Proposal", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/sign_off_proposal.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governance", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "proposal", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signatory", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "proposal_owner_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/governance/withdraw_governing_tokens.json b/crates/cmds-solana/node-definitions/governance/withdraw_governing_tokens.json new file mode 100644 index 00000000..efbb17ca --- /dev/null +++ b/crates/cmds-solana/node-definitions/governance/withdraw_governing_tokens.json @@ -0,0 +1,128 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "withdraw_governing_tokens", + "version": "0.1", + "display_name": "Withdraw Governing Tokens", + "description": "", + "tags": ["governance", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/governance/withdraw_governing_tokens.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#1976d2", + "backgroundColor": "#1976d2" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "realm_config_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "governing_token_holding_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_owner_record_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "realm", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_destination", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "governing_token_owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "governing_token_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/jupiter/swap.json b/crates/cmds-solana/node-definitions/jupiter/swap.json new file mode 100644 index 00000000..09d0dc82 --- /dev/null +++ b/crates/cmds-solana/node-definitions/jupiter/swap.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "jupiter_swap", + "version": "0.1", + "display_name": "Jupiter Swap", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": null, + "options": null, + "instruction_info": { + "before": [], + "signature": "signature", + "after": [] + } + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "optional": true + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "input_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "output_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "auto_slippage", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "slippage_percent", + "type_bounds": ["u16"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.ui_schema": {}, + "targets_form.json_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/memo.json b/crates/cmds-solana/node-definitions/memo.json new file mode 100644 index 00000000..f56ef96a --- /dev/null +++ b/crates/cmds-solana/node-definitions/memo.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "memo", + "version": "0.1", + "display_name": "Memo", + "description": "Adds a memo to a transaction", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "memo", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/metaboss/update_authority.ignore b/crates/cmds-solana/node-definitions/metaboss/update_authority.ignore new file mode 100644 index 00000000..6f4531ec --- /dev/null +++ b/crates/cmds-solana/node-definitions/metaboss/update_authority.ignore @@ -0,0 +1,87 @@ +// ignore +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "upgrade_authority", + "version": "0.1", + "display_name": "Upgrade Authority", + "description": "", + "width": 200, + "height": 350, + "backgroundColorDark":"#000000" ,"backgroundColor": "#FFFF99" + }, + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": [ + "keypair" + ], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": [ + "keypair" + ], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} \ No newline at end of file diff --git a/crates/cmds-solana/node-definitions/mint_token.json b/crates/cmds-solana/node-definitions/mint_token.json new file mode 100644 index 00000000..e9efe873 --- /dev/null +++ b/crates/cmds-solana/node-definitions/mint_token.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint_token", + "version": "0.1", + "display_name": "Mint Token", + "description": "Identifies the token, determines who can mint, and how many", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "amount", + "type_bounds": ["f64"], + "required": true, + "defaultValue": null, + "tooltip": "NFTs should have amount = 1", + "passthrough": false + }, + { + "name": "decimals", + "type_bounds": ["u8"], + "required": false, + "defaultValue": null, + "tooltip": "NFTs should have decimals = 0", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/approve_collection_authority.json b/crates/cmds-solana/node-definitions/nft/approve_collection_authority.json new file mode 100644 index 00000000..a7bf6893 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/approve_collection_authority.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "approve_collection_authority", + "version": "0.1", + "display_name": "Approve Collection Authority", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "new_collection_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/approve_use_authority.json b/crates/cmds-solana/node-definitions/nft/approve_use_authority.json new file mode 100644 index 00000000..34c34526 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/approve_use_authority.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "approve_use_authority", + "version": "0.1", + "display_name": "Approve Use Authority", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "use_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "token_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "burner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "number_of_uses", + "type_bounds": ["u64"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/arweave_file_upload.json b/crates/cmds-solana/node-definitions/nft/arweave_file_upload.json new file mode 100644 index 00000000..9fa15d0c --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/arweave_file_upload.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "arweave_file_upload", + "version": "0.1", + "display_name": "Arweave File Upload", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "file_url", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "file_path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "fund_bundlr", + "type_bounds": ["bool"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/arweave_nft_upload.json b/crates/cmds-solana/node-definitions/nft/arweave_nft_upload.json new file mode 100644 index 00000000..293b1ff4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/arweave_nft_upload.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "arweave_nft_upload", + "version": "0.1", + "display_name": "Arweave NFT Upload", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "metadata_url", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "updated_metadata", + "type": "json", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "metadata", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "fund_bundlr", + "type_bounds": ["bool"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/auction_house_sell.json b/crates/cmds-solana/node-definitions/nft/auction_house_sell.json new file mode 100644 index 00000000..c649dd56 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/auction_house_sell.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "auction_house_sell", + "version": "0.1", + "display_name": "Auction House Sell", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "treasury_mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "auction_house_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": true + }, + { + "name": "seller", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "seller_token_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "seller_token_mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "sale_price", + "type_bounds": ["u64"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine/add_config_lines.json b/crates/cmds-solana/node-definitions/nft/candy_machine/add_config_lines.json new file mode 100644 index 00000000..5cbfd931 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine/add_config_lines.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "add_config_lines", + "version": "0.1", + "display_name": "Add Config Lines", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "index", + "type_bounds": ["u32"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "config_lines", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine/initialize.json b/crates/cmds-solana/node-definitions/nft/candy_machine/initialize.json new file mode 100644 index 00000000..35aecf2d --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine/initialize.json @@ -0,0 +1,150 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_candy_machine", + "version": "0.1", + "display_name": "Initialize Candy Machine", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair", "string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "rule_set", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "eBJLFYPxJmMGKuFwpDWkzxZeUrad92kZRC5BJLpzyT9", + "tooltip": "Default is Metaplex Rule Set", + "passthrough": false + }, + { + "name": "collection_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_machine_data", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authorization_rules_program", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "authorization_rules", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "token_standard", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine/initialize_candy_guard.json b/crates/cmds-solana/node-definitions/nft/candy_machine/initialize_candy_guard.json new file mode 100644 index 00000000..d7d2f710 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine/initialize_candy_guard.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_candy_guard", + "version": "0.1", + "display_name": "Initialize Candy Guard", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "candy_guard", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "base", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_guards", + "type_bounds": ["json"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine/mint.json b/crates/cmds-solana/node-definitions/nft/candy_machine/mint.json new file mode 100644 index 00000000..18b198d1 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine/mint.json @@ -0,0 +1,175 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint", + "version": "0.1", + "display_name": "Mint from Candy Machine", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "minter", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_guards", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "group_label", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "rule_set", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "eBJLFYPxJmMGKuFwpDWkzxZeUrad92kZRC5BJLpzyT9", + "tooltip": "Default is Metaplex Rule Set", + "passthrough": false + }, + { + "name": "authorization_rules_program", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "authorization_rules", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine/wrap.json b/crates/cmds-solana/node-definitions/nft/candy_machine/wrap.json new file mode 100644 index 00000000..d25c4a49 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine/wrap.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "wrap", + "version": "0.1", + "display_name": "Wrap Candy Machine", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_machine_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "candy_guard", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "candy_guard_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine_core/add_config_lines_core.json b/crates/cmds-solana/node-definitions/nft/candy_machine_core/add_config_lines_core.json new file mode 100644 index 00000000..8ac1c482 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine_core/add_config_lines_core.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "add_config_lines_core", + "version": "0.1", + "display_name": "Add Config Lines Core", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "index", + "type_bounds": ["u32"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "config_lines", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_candy_machine_core.json b/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_candy_machine_core.json new file mode 100644 index 00000000..a9cbada4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_candy_machine_core.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_candy_machine_core", + "version": "0.1", + "display_name": "Initialize Candy Machine Core", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair", "string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "rule_set", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "eBJLFYPxJmMGKuFwpDWkzxZeUrad92kZRC5BJLpzyT9", + "tooltip": "Default is Metaplex Rule Set", + "passthrough": false + }, + { + "name": "collection_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_machine_data", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_core_candy_guards.json b/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_core_candy_guards.json new file mode 100644 index 00000000..ee12b229 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine_core/initialize_core_candy_guards.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_core_candy_guards", + "version": "0.1", + "display_name": "Initialize Candy Guard Core", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "candy_guard", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "base", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_guards", + "type_bounds": ["json"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine_core/mint_core.json b/crates/cmds-solana/node-definitions/nft/candy_machine_core/mint_core.json new file mode 100644 index 00000000..7581aa73 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine_core/mint_core.json @@ -0,0 +1,150 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint_candy_machine_core", + "version": "0.1", + "display_name": "Mint from Candy Machine Core", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "minter", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "owner", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_update_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_guards", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "group_label", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/candy_machine_core/wrap_core.json b/crates/cmds-solana/node-definitions/nft/candy_machine_core/wrap_core.json new file mode 100644 index 00000000..f233ab90 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/candy_machine_core/wrap_core.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "wrap_core", + "version": "0.1", + "display_name": "Wrap Candy Machine Core", + "description": "", + "tags": ["NFT", "Solana", "Candy Machine Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "candy_machine", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "candy_machine_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "candy_guard", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "candy_guard_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/core/collection_sample copy.json b/crates/cmds-solana/node-definitions/nft/core/collection_sample copy.json new file mode 100644 index 00000000..67c1f467 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/collection_sample copy.json @@ -0,0 +1,43 @@ +[ + { + "plugin": { + "VerifiedCreators": { + "signatures": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "verified": true + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Attributes": { + "attribute_list": [ + { + "key": "blinks_gg", + "value": "test_value" + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Royalties": { + "basis_points": 750, + "creators": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "percentage": 100 + } + ], + "rule_set": "None" + } + }, + "authority": "UpdateAuthority" + } +] \ No newline at end of file diff --git a/crates/cmds-solana/node-definitions/nft/core/collection_sample.json b/crates/cmds-solana/node-definitions/nft/core/collection_sample.json new file mode 100644 index 00000000..67c1f467 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/collection_sample.json @@ -0,0 +1,43 @@ +[ + { + "plugin": { + "VerifiedCreators": { + "signatures": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "verified": true + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Attributes": { + "attribute_list": [ + { + "key": "blinks_gg", + "value": "test_value" + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Royalties": { + "basis_points": 750, + "creators": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "percentage": 100 + } + ], + "rule_set": "None" + } + }, + "authority": "UpdateAuthority" + } +] \ No newline at end of file diff --git a/crates/cmds-solana/node-definitions/nft/core/fetch_assets.json b/crates/cmds-solana/node-definitions/nft/core/fetch_assets.json new file mode 100644 index 00000000..95057225 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/fetch_assets.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "fetch_assets", + "version": "0.1", + "display_name": "Fetch Assets", + "description": "", + "tags": ["NFT", "Solana", "Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "assets", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "collection", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_asset.json b/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_asset.json new file mode 100644 index 00000000..0b8ef4b5 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_asset.json @@ -0,0 +1,134 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_core_v2", + "version": "0.1", + "display_name": "Create Core Asset", + "description": "", + "tags": ["NFT", "Solana", "Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "asset", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "uri", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "collection", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "verified_creator", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "plugins", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_collection.json b/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_collection.json new file mode 100644 index 00000000..bd4ae9fb --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/mpl_core_create_collection.json @@ -0,0 +1,133 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_core_collection_v2", + "version": "0.1", + "display_name": "Create Core Collection", + "description": "", + "tags": ["NFT", "Solana", "Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "collection", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "collection", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "verified_creator", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "uri", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + + { + "name": "plugins", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_asset.json b/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_asset.json new file mode 100644 index 00000000..ea63d20e --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_asset.json @@ -0,0 +1,137 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_core_v1", + "version": "0.1", + "display_name": "Update Core Asset", + "description": "", + "tags": ["NFT", "Solana", "Core"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_edition_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "asset", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "collection", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "new_name", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "new_uri", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "new_update_authority", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} + } + \ No newline at end of file diff --git a/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_plugin.json b/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_plugin.json new file mode 100644 index 00000000..5b875b11 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/mpl_core_update_plugin.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mpl_core_update_plugin", + "version": "0.1", + "display_name": "MPL Core Update Plugin", + "description": "", + "tags": ["NFT", "Solana", "Core", "Plugins"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "asset", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "collection", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "plugin", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/core/sample.json b/crates/cmds-solana/node-definitions/nft/core/sample.json new file mode 100644 index 00000000..fc280869 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/core/sample.json @@ -0,0 +1,43 @@ +[ + { + "plugin": { + "VerifiedCreators": { + "signatures": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "verified": true + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Attributes": { + "attribute_list": [ + { + "key": "test_key", + "value": "test_value" + } + ] + } + }, + "authority": "UpdateAuthority" + }, + { + "plugin": { + "Royalties": { + "basis_points": 750, + "creators": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "percentage": 100 + } + ], + "rule_set": "None" + } + }, + "authority": "UpdateAuthority" + } +] diff --git a/crates/cmds-solana/node-definitions/nft/create_master_edition.json b/crates/cmds-solana/node-definitions/nft/create_master_edition.json new file mode 100644 index 00000000..5fc8b4f7 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/create_master_edition.json @@ -0,0 +1,130 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_master_edition", + "version": "0.1", + "display_name": "Create Master Edition", + "description": "", + "tags": ["NFT", "Solana"], + "related_to": [ + { + "id": "create_metadata_account", + "type": "node", + "relationship": "group" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_edition_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "proxy_as_update_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "Whether a Proxy Authority is signing the Update Authority", + "passthrough": true + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "max_supply", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "How many copies you can print. Leave empty for unlimited\n1/1 NFTs should have supply 0", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/create_metadata_account.json b/crates/cmds-solana/node-definitions/nft/create_metadata_account.json new file mode 100644 index 00000000..d5931544 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/create_metadata_account.json @@ -0,0 +1,133 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_metadata_account", + "version": "0.1", + "display_name": "Create Metadata Account", + "description": "", + "tags": ["NFT", "Solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "Who can update the on-chain metadata", + "passthrough": true + }, + { + "name": "is_mutable", + "type_bounds": ["bool"], + "required": true, + "defaultValue": null, + "tooltip": "Whether Metadata Account can be updated", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "Token Mint Account", + "passthrough": true + }, + { + "name": "mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "metadata", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_details", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "Only applies to Collection NFTs and is automatically set. To facility migration, set the collection size manually.", + "passthrough": false + }, + + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/get_left_uses.json b/crates/cmds-solana/node-definitions/nft/get_left_uses.json new file mode 100644 index 00000000..8ebdd614 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/get_left_uses.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "get_left_uses", + "version": "0.1", + "display_name": "Get Left Uses", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "left_uses", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/set_token_standard.json b/crates/cmds-solana/node-definitions/nft/set_token_standard.json new file mode 100644 index 00000000..b5f73fe0 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/set_token_standard.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_token_standard", + "version": "0.1", + "display_name": "Set Token Standard", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "edition_account", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/sign_metadata.json b/crates/cmds-solana/node-definitions/nft/sign_metadata.json new file mode 100644 index 00000000..e62d1379 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/sign_metadata.json @@ -0,0 +1,94 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "sign_metadata", + "version": "0.1", + "display_name": "Sign Metadata", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "creator", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/update_metadata_account.json b/crates/cmds-solana/node-definitions/nft/update_metadata_account.json new file mode 100644 index 00000000..1c3eb0e6 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/update_metadata_account.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_metadata_account", + "version": "0.1", + "display_name": "Update Metadata Account", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "new_update_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["object"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "primary_sale_happen", + "type_bounds": ["bool"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "is_mutable", + "type_bounds": ["bool"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/utilize.json b/crates/cmds-solana/node-definitions/nft/utilize.json new file mode 100644 index 00000000..4e2d4444 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/utilize.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "utilize", + "version": "0.1", + "display_name": "Utilize", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "use_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "burner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "number_of_uses", + "type_bounds": ["u64"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/burn_v1.json b/crates/cmds-solana/node-definitions/nft/v1/burn_v1.json new file mode 100644 index 00000000..96158651 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/burn_v1.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "burn_v1", + "version": "0.1", + "display_name": "Burn V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": false, + "defaultValue": "1", + "tooltip": "1 by default", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/create_v1.json b/crates/cmds-solana/node-definitions/nft/v1/create_v1.json new file mode 100644 index 00000000..da4789d4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/create_v1.json @@ -0,0 +1,200 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_v1", + "version": "0.1", + "display_name": "Create V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_edition_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "DataV2", + "passthrough": false + }, + { + "name": "print_supply", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "0, u64, or none", + "passthrough": false + }, + { + "name": "collection_mint_account", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_details", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "Only applies to Collection NFTs and is automatically set. To facility migration, set the collection size manually.", + "passthrough": false + }, + { + "name": "is_mutable", + "type_bounds": ["bool"], + "required": true, + "defaultValue": null, + "tooltip": "Whether Metadata Account can be updated", + "passthrough": false + }, + { + "name": "token_standard", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "non_fungible, fungible_asset, fungible, non_fungible_edition, programmable_non_fungible, programmable_non_fungible_edition,", + "passthrough": false + }, + { + "name": "decimals", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "creators", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "Creators and their share of royalties. Limited to 5 creators", + "passthrough": false + }, + { + "name": "uses", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "How many and which type of uses each NFT will have.\nUses:\nBurn is a single-time use and is burned after use.\nSingle is a single-time use and does not burn the token.\nMultiple allows up to the specified number of uses", + "passthrough": false + }, + { + "name": "rule_set", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "Programmable rule set configuration (only applicable to `Programmable` asset types)", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/delegate_v1.json b/crates/cmds-solana/node-definitions/nft/v1/delegate_v1.json new file mode 100644 index 00000000..57bac459 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/delegate_v1.json @@ -0,0 +1,122 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "delegate_v1", + "version": "0.1", + "display_name": "Delegate V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "delegate_record", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_record", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "delegate", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "delegate_args", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/mint_v1.json b/crates/cmds-solana/node-definitions/nft/v1/mint_v1.json new file mode 100644 index 00000000..19712da0 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/mint_v1.json @@ -0,0 +1,149 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "mint_v1", + "version": "0.1", + "display_name": "Mint V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "create_v1", + "type": "node", + "relationship": "group" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + + { + "name": "token", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "Recipient of the mint", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "delegate_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authorization_rules_program", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authorization_rules", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authorization_data", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/update_v1.json b/crates/cmds-solana/node-definitions/nft/v1/update_v1.json new file mode 100644 index 00000000..fa6d6918 --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/update_v1.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "update_v1", + "version": "0.1", + "display_name": "Update V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "authority or delegate", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "delegate", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "delegate_record", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "update_args", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/verify_collection_v1.json b/crates/cmds-solana/node-definitions/nft/v1/verify_collection_v1.json new file mode 100644 index 00000000..be5876bd --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/verify_collection_v1.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "verify_collection_v1", + "version": "0.1", + "display_name": "Verify Collection V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "delegate_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/v1/verify_creator_v1.json b/crates/cmds-solana/node-definitions/nft/v1/verify_creator_v1.json new file mode 100644 index 00000000..3573d6ba --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/v1/verify_creator_v1.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "verify_creator_v1", + "version": "0.1", + "display_name": "Verify Creator V1", + "description": "", + "tags": ["NFT", "Solana", "V1"], + "related_to": [ + { + "id": "create_v1", + "type": "node", + "relationship": "group" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "Creator to verify, collection update authority or delegate", + "passthrough": false + }, + { + "name": "delegate_record", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "metadata", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_mint_account", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/nft/verify_collection.json b/crates/cmds-solana/node-definitions/nft/verify_collection.json new file mode 100644 index 00000000..5813531f --- /dev/null +++ b/crates/cmds-solana/node-definitions/nft/verify_collection.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "verify_collection", + "version": "0.1", + "display_name": "Verify Collection", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "collection_mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "collection_authority_is_delegated", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/pyth_price.json b/crates/cmds-solana/node-definitions/pyth_price.json new file mode 100644 index 00000000..0040eae4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/pyth_price.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "pyth_price", + "version": "0.1", + "display_name": "Pyth Price", + "description": "Get Pyth price", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "price_feed_id", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "price", + "type": "i64", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/record/initialize_record_with_seed.json b/crates/cmds-solana/node-definitions/record/initialize_record_with_seed.json new file mode 100644 index 00000000..1cedb1d4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/record/initialize_record_with_seed.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_record_with_seed", + "version": "0.1", + "display_name": "Initialize Record With Seed", + "description": "Adds a memo to a transaction", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/record/read_record.json b/crates/cmds-solana/node-definitions/record/read_record.json new file mode 100644 index 00000000..8d051dbe --- /dev/null +++ b/crates/cmds-solana/node-definitions/record/read_record.json @@ -0,0 +1,90 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "read_record", + "version": "0.1", + "display_name": "Read Record", + "description": "Reads a record", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "authority", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "version", + "type": "number", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "data", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/record/write_to_record.json b/crates/cmds-solana/node-definitions/record/write_to_record.json new file mode 100644 index 00000000..a1a51268 --- /dev/null +++ b/crates/cmds-solana/node-definitions/record/write_to_record.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "write_to_record", + "version": "0.1", + "display_name": "Write To Record", + "description": "Writes to a record", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "seed", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "offset", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/request_airdrop.json b/crates/cmds-solana/node-definitions/request_airdrop.json new file mode 100644 index 00000000..7ff08a50 --- /dev/null +++ b/crates/cmds-solana/node-definitions/request_airdrop.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "request_airdrop", + "version": "0.1", + "display_name": "Request Airdrop", + "description": "Airdrop SOL for testing purposes\n\nCurrently takes 30s to complete, pass the signature to a wait command", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": false, + "defaultValue": 1000000000, + "tooltip": "in lamports, 1 SOL = 1,000,000,000 lamports", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/spl_token/set_authority.json b/crates/cmds-solana/node-definitions/spl_token/set_authority.json new file mode 100644 index 00000000..91d1ce24 --- /dev/null +++ b/crates/cmds-solana/node-definitions/spl_token/set_authority.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_authority", + "version": "0.1", + "display_name": "Set Authority", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/spl_token/set_authority.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "owned_pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "new_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority_type", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "owner_pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signer_pubkeys", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/spl_token_2022/set_authority.json b/crates/cmds-solana/node-definitions/spl_token_2022/set_authority.json new file mode 100644 index 00000000..14fe5a14 --- /dev/null +++ b/crates/cmds-solana/node-definitions/spl_token_2022/set_authority.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "set_authority_2022", + "version": "0.1", + "display_name": "Set Authority 2022", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/spl_token_2022/set_authority.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "owned_pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "new_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority_type", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "owner_pubkey", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signer_pubkeys", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/streamflow/create.json b/crates/cmds-solana/node-definitions/streamflow/create.json new file mode 100644 index 00000000..f9f1a96e --- /dev/null +++ b/crates/cmds-solana/node-definitions/streamflow/create.json @@ -0,0 +1,150 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_streamflow_timelock", + "version": "0.1", + "display_name": "Create Streamflow Timelock", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "escrow_tokens", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sender_tokens", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "recipient_tokens", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "sender", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "metadata", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "partner", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/streamflow/withdraw.json b/crates/cmds-solana/node-definitions/streamflow/withdraw.json new file mode 100644 index 00000000..a10ed2a9 --- /dev/null +++ b/crates/cmds-solana/node-definitions/streamflow/withdraw.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "withdraw_streamflow_timelock", + "version": "0.1", + "display_name": "Withdraw Streamflow Timelock", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#2e003f", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "metadata", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "data", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "partner", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/transfer_sol.json b/crates/cmds-solana/node-definitions/transfer_sol.json new file mode 100644 index 00000000..79bae845 --- /dev/null +++ b/crates/cmds-solana/node-definitions/transfer_sol.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_sol", + "version": "0.1", + "display_name": "Transfer SOL", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "sender", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["decimal"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/transfer_token.json b/crates/cmds-solana/node-definitions/transfer_token.json new file mode 100644 index 00000000..48a1f7ff --- /dev/null +++ b/crates/cmds-solana/node-definitions/transfer_token.json @@ -0,0 +1,156 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_token", + "version": "0.1", + "display_name": "Transfer Token", + "description": "Transfer a custom token", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "recipient_token_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["f64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "decimals", + "type_bounds": ["u8"], + "required": false, + "defaultValue": null, + "tooltip": "NFTs should have decimals = 0", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "sender_token_account", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "sender token account\n if empty, will be derived from sender owner", + "passthrough": true + }, + { + "name": "sender_owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "allow_unfunded", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "fund_recipient", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "memo", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "Additional notes", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wallet.json b/crates/cmds-solana/node-definitions/wallet.json new file mode 100644 index 00000000..dd1ca3ea --- /dev/null +++ b/crates/cmds-solana/node-definitions/wallet.json @@ -0,0 +1,76 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "wallet", + "version": "0.1", + "display_name": "Wallet", + "description": "", + "tags": ["std", "wallet", "solana"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wallet.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 300, + "height": 155, + "icon_url": "", + "backgroundColorDark": "#149b0f", + "backgroundColor": "#baf2b1" + }, + "options": {} + }, + "sources": [ + { + "name": "pubkey", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "name", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/get_vaa.json b/crates/cmds-solana/node-definitions/wormhole/get_vaa.json new file mode 100644 index 00000000..4676e1f5 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/get_vaa.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "get_vaa", + "version": "0.1", + "display_name": "Get VAA", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "crates/cmds-solana/src/wormhole/get_vaa.rs", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "response", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vaa", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "emitter", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "sequence", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "chain_id", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/redeem_nft_on_eth.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/redeem_nft_on_eth.json new file mode 100644 index 00000000..6bb6ca2f --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/redeem_nft_on_eth.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "redeem_nft_on_eth", + "version": "0.1", + "display_name": "Redeem NFT On ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/eth/redeem_nft_on_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "receipt", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signed_vaa", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/transfer_nft_from_eth.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/transfer_nft_from_eth.json new file mode 100644 index 00000000..dd72e866 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/eth/transfer_nft_from_eth.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_nft_from_eth", + "version": "0.1", + "display_name": "Transfer NFT From ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/eth/transfer_nft_from_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "response", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "address", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "u32", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "recipient_ata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token_id", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_native.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_native.json new file mode 100644 index 00000000..006cb7a0 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_native.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "nft_complete_native", + "version": "0.1", + "display_name": "NFT Complete Native", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/complete_native.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "token", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "to_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped.json new file mode 100644 index 00000000..52f8c5e7 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped.json @@ -0,0 +1,128 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "nft_complete_wrapped", + "version": "0.1", + "display_name": "NFT Complete Wrapped", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "mint_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "token_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "to_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped_meta.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped_meta.json new file mode 100644 index 00000000..dae34fb4 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_complete_wrapped_meta.json @@ -0,0 +1,120 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "nft_complete_wrapped_meta", + "version": "0.1", + "display_name": "NFT Complete Wrapped Meta", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped_meta.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "spl_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_native.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_native.json new file mode 100644 index 00000000..7280d5f9 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_native.json @@ -0,0 +1,144 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "nft_transfer_native", + "version": "0.1", + "display_name": "NFT Transfer Native", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/transfer_native.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "token_chain", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_address", + "type_bounds": ["address"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_chain", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "from", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_wrapped.json b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_wrapped.json new file mode 100644 index 00000000..077b67bc --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/nft_bridge/nft_transfer_wrapped.json @@ -0,0 +1,136 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "nft_transfer_wrapped", + "version": "0.1", + "display_name": "NFT Transfer Wrapped", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/nft_bridge/transfer_wrapped.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "wrapped_meta_key", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_address", + "type_bounds": ["address"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_chain", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "from_owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/parse_vaa.json b/crates/cmds-solana/node-definitions/wormhole/parse_vaa.json new file mode 100644 index 00000000..f3b6fdc6 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/parse_vaa.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "parse_vaa", + "version": "0.1", + "display_name": "Parse VAA", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "crates/cmds-solana/src/wormhole/parse_vaa.rs", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "parsed_vaa", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vaa_bytes", + "type": "bytes", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signatures", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "guardian_set_index", + "type": "u32", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "body", + "type": "bytes", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vaa_hash", + "type": "bytes", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vaa_secp256k_hash", + "type": "bytes", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "payload", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "nft_token_id", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "vaa", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/post_message.json b/crates/cmds-solana/node-definitions/wormhole/post_message.json new file mode 100644 index 00000000..07fe2f9f --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/post_message.json @@ -0,0 +1,106 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "post_message", + "version": "0.1", + "display_name": "Post Message", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/post_message.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "emitter", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/post_vaa.json b/crates/cmds-solana/node-definitions/wormhole/post_vaa.json new file mode 100644 index 00000000..e85edd4f --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/post_vaa.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "post_vaa", + "version": "0.1", + "display_name": "Post VAA", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/post_vaa.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "vaa_address", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signature_set", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "guardian_set_index", + "type_bounds": ["u32"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/attest.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/attest.json new file mode 100644 index 00000000..4513199b --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/attest.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "attest_token", + "version": "0.1", + "display_name": "Attest Token", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/attest.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "spl_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_native.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_native.json new file mode 100644 index 00000000..4262dabd --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_native.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "complete_native", + "version": "0.1", + "display_name": "Complete Native", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/complete_native.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "fee_recipient", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_transfer_wrapped.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_transfer_wrapped.json new file mode 100644 index 00000000..f39bdc46 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/complete_transfer_wrapped.json @@ -0,0 +1,122 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "complete_transfer_wrapped", + "version": "0.1", + "display_name": "Complete Transfer Wrapped", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/complete_transfer_wrapped.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "mint_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "fee_recipient", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/create_wrapped.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/create_wrapped.json new file mode 100644 index 00000000..eeab9bcf --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/create_wrapped.json @@ -0,0 +1,120 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_wrapped", + "version": "0.1", + "display_name": "Create Wrapped", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/create_wrapped.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "spl_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "payload", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/attest_from_eth.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/attest_from_eth.json new file mode 100644 index 00000000..213e39c9 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/attest_from_eth.json @@ -0,0 +1,98 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "attest_from_eth", + "version": "0.1", + "display_name": "Attest From ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/eth/attest_from_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "response", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "address", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "u32", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token", + "type_bounds": ["address"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/create_wrapped_on_eth.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/create_wrapped_on_eth.json new file mode 100644 index 00000000..f22b8470 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/create_wrapped_on_eth.json @@ -0,0 +1,100 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_wrapped_on_eth", + "version": "0.1", + "display_name": "Create Wrapped On ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/eth/create_wrapped_on_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "receipt", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "address", + "type": "address", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signed_vaa", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "token", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/redeem_on_eth.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/redeem_on_eth.json new file mode 100644 index 00000000..e5ab3c90 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/redeem_on_eth.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "redeem_on_eth", + "version": "0.1", + "display_name": "Redeem On ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/eth/redeem_on_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "receipt", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "signed_vaa", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/transfer_from_eth.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/transfer_from_eth.json new file mode 100644 index 00000000..7892d852 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/eth/transfer_from_eth.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_from_eth", + "version": "0.1", + "display_name": "Transfer From ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/eth/transfer_from_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "response", + "type": "json", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "address", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "u32", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "recipient_ata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "keypair", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "ETH Wallet Private Key", + "passthrough": true + }, + { + "name": "network_name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "Wormhole Network Name", + "passthrough": false + }, + { + "name": "token", + "type_bounds": ["address"], + "required": true, + "defaultValue": null, + "tooltip": "ETH token address", + "passthrough": false + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "SOL recipient associated token address", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["f64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/initialize.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/initialize.json new file mode 100644 index 00000000..5bb3774c --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/initialize.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "initialize_token_bridge", + "version": "0.1", + "display_name": "Initialize Token Bridge", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/initialize.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_native.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_native.json new file mode 100644 index 00000000..770b708c --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_native.json @@ -0,0 +1,158 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_native", + "version": "0.1", + "display_name": "Transfer Native", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/transfer_native.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "custody", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "custody_signer", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "from", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "fee", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_address", + "type_bounds": ["address"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_chain", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_wrapped.json b/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_wrapped.json new file mode 100644 index 00000000..96abb002 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/token_bridge/transfer_wrapped.json @@ -0,0 +1,152 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "transfer_wrapped", + "version": "0.1", + "display_name": "Transfer Wrapped", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/token_bridge/transfer_wrapped.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "wrapped_meta_key", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "emitter", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "sequence", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "mint", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "fee", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_address", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "target_chain", + "type_bounds": ["u16"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "message", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "from_owner", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/utils/get_foreign_asset_eth.json b/crates/cmds-solana/node-definitions/wormhole/utils/get_foreign_asset_eth.json new file mode 100644 index 00000000..3ffd1b06 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/utils/get_foreign_asset_eth.json @@ -0,0 +1,95 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "get_foreign_asset_eth", + "version": "0.1", + "display_name": "Get Foreign Asset on ETH", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/utils/get_foreign_asset_eth.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "address", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "token", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "is_nft", + "type_bounds": ["bool"], + "required": false, + "defaultValue": false, + "tooltip": "", + "passthrough": false + }, + { + "name": "chain_id", + "type_bounds": ["number"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + + { + "name": "network", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/wormhole/verify_signatures.json b/crates/cmds-solana/node-definitions/wormhole/verify_signatures.json new file mode 100644 index 00000000..95b8ac45 --- /dev/null +++ b/crates/cmds-solana/node-definitions/wormhole/verify_signatures.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "verify_signatures", + "version": "0.1", + "display_name": "Verify Signatures", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/wormhole/verify_signatures.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#0d2e51", + "backgroundColor": "#8DB7FB" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signature_set", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "guardian_set_index", + "type_bounds": ["u32"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "signatures", + "type_bounds": ["array"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + }, + { + "name": "vaa_body", + "type_bounds": ["bytes"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": true + }, + { + "name": "vaa_hash", + "type_bounds": ["bytes"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/create_install.json b/crates/cmds-solana/node-definitions/xnft/create_install.json new file mode 100644 index 00000000..4961f6ee --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/create_install.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_install", + "version": "0.1", + "display_name": "xNFT Create Install", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "install", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "xnft", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "target", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "install_vault", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/create_permissioned_install.json b/crates/cmds-solana/node-definitions/xnft/create_permissioned_install.json new file mode 100644 index 00000000..f64fba3f --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/create_permissioned_install.json @@ -0,0 +1,122 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_permissioned_install", + "version": "0.1", + "display_name": "xNFT Create Private Install", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "install", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "access", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "xnft", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "target", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "install_vault", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/create_xnft.json b/crates/cmds-solana/node-definitions/xnft/create_xnft.json new file mode 100644 index 00000000..2bafe5d2 --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/create_xnft.json @@ -0,0 +1,134 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "create_xnft", + "version": "0.1", + "display_name": "xNFT Create", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_mint", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_token", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "master_metadata", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "xnft", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "publisher", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "name", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "parameters", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/delete_install.json b/crates/cmds-solana/node-definitions/xnft/delete_install.json new file mode 100644 index 00000000..2f600613 --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/delete_install.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "delete_install", + "version": "0.1", + "display_name": "xNFT Delete Install", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "receiver", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "install", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/grant_access.json b/crates/cmds-solana/node-definitions/xnft/grant_access.json new file mode 100644 index 00000000..d6187937 --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/grant_access.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "grant_access", + "version": "0.1", + "display_name": "xNFT Grant Access", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "access", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "xnft", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "wallet", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/node-definitions/xnft/revoke_access.json b/crates/cmds-solana/node-definitions/xnft/revoke_access.json new file mode 100644 index 00000000..babf2856 --- /dev/null +++ b/crates/cmds-solana/node-definitions/xnft/revoke_access.json @@ -0,0 +1,108 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "revoke_access", + "version": "0.1", + "display_name": "xNFT Revoke Access", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "access", + "type": "pubkey", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "xnft", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "wallet", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-solana/src/associated_token_account.rs b/crates/cmds-solana/src/associated_token_account.rs new file mode 100644 index 00000000..715210f7 --- /dev/null +++ b/crates/cmds-solana/src/associated_token_account.rs @@ -0,0 +1,83 @@ +use crate::prelude::*; +use spl_associated_token_account::instruction::create_associated_token_account; + +const SOLANA_ASSOCIATED_TOKEN_ACCOUNT: &str = "associated_token_account"; + +const DEFINITION: &str = flow_lib::node_definition!("associated_token_account.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(SOLANA_ASSOCIATED_TOKEN_ACCOUNT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new( + SOLANA_ASSOCIATED_TOKEN_ACCOUNT, + |_| { build() } +)); + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde_as(as = "AsPubkey")] + owner: Pubkey, + fee_payer: Wallet, + #[serde_as(as = "AsPubkey")] + mint_account: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde_as(as = "Option")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let instruction = create_associated_token_account( + &input.fee_payer.pubkey(), + &input.owner, + &input.mint_account, + &spl_token::id(), + ); + + let associated_token_account = instruction.accounts[1].pubkey; + + let instructions = if input.submit { + Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [instruction].into(), + } + } else { + <_>::default() + }; + + let signature = ctx + .execute( + instructions, + value::map! { + "associated_token_account" => associated_token_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/compression/burn.rs b/crates/cmds-solana/src/compression/burn.rs new file mode 100644 index 00000000..1e04128b --- /dev/null +++ b/crates/cmds-solana/src/compression/burn.rs @@ -0,0 +1,192 @@ +use crate::prelude::*; +use mpl_bubblegum::instructions::BurnBuilder; +use solana_sdk::pubkey::Pubkey; +use std::str::FromStr; + +use super::{ + types::asset::{Asset, AssetProof}, + GetAssetResponse, WalletOrPubkey, +}; + +// Command Name +const NAME: &str = "burn_cNFT"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/burn_cNFT.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + #[serde(default)] + pub leaf_owner: Option, + // + pub das_get_asset_proof: Option>, + pub das_get_asset: Option>, + // + pub leaf_delegate: Option, + #[serde(default, with = "value::pubkey::opt")] + pub collection_mint: Option, + // + #[serde(default, with = "value::pubkey::opt")] + pub merkle_tree: Option, + pub root: Option, + pub data_hash: Option, + pub creator_hash: Option, + pub leaf_id: Option, + pub index: Option, + // + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // get from asset proof: merkle tree, root, index, proof + // get from asset: data hash, creator hash, leaf id or nonce, metadata + + // get root + let root = match &input.root { + Some(root) => root, + None => match &input.das_get_asset_proof { + Some(proof) => &proof.result.root, + None => return Err(CommandError::msg("root is required")), + }, + }; + + let root = Pubkey::from_str(root) + .map_err(|_| CommandError::msg("Invalid root string"))? + .to_bytes(); + + // get data hash + let data_hash = match input.data_hash { + Some(data_hash) => data_hash, + None => match input.das_get_asset.clone() { + Some(asset) => asset.result.compression.unwrap().data_hash, + None => return Err(CommandError::msg("data_hash is required")), + }, + }; + + let data_hash = Pubkey::from_str(&data_hash) + .map_err(|_| CommandError::msg("Invalid data_hash string"))? + .to_bytes(); + + // get creator hash + let creator_hash = match input.creator_hash { + Some(creator_hash) => creator_hash, + None => match input.das_get_asset.clone() { + Some(asset) => asset.result.compression.unwrap().creator_hash, + None => return Err(CommandError::msg("creator_hash is required")), + }, + }; + + let creator_hash = Pubkey::from_str(&creator_hash) + .map_err(|_| CommandError::msg("Invalid creator_hash string"))? + .to_bytes(); + + // leaf_id aka nonce + let nonce = match input.leaf_id { + Some(leaf_id) => leaf_id, + None => match input.das_get_asset { + Some(asset) => asset + .result + .compression + .unwrap() + .leaf_id + .try_into() + .unwrap(), + None => return Err(CommandError::msg("leaf_id is required")), + }, + }; + + // get index + + let index = match input.index { + Some(index) => index, + None => match input.das_get_asset_proof.clone() { + Some(asset) => (asset.result.node_index - 2 * asset.result.proof.len() as i64) + .try_into() + .unwrap(), + None => return Err(CommandError::msg("index is required")), + }, + }; + + let merkle_tree = match input.merkle_tree { + Some(merkle_tree) => merkle_tree, + None => match input.das_get_asset_proof { + Some(asset) => Pubkey::from_str(&asset.result.tree_id).unwrap(), + None => return Err(CommandError::msg("merkle_tree is required")), + }, + }; + + let tree_config = mpl_bubblegum::accounts::TreeConfig::find_pda(&merkle_tree).0; + + // who is signing? + let delegate_is_signing = input.leaf_delegate.is_some(); + + let signer = if delegate_is_signing { + input.leaf_delegate.as_ref().unwrap().clone() + } else { + match input.leaf_owner.as_ref().unwrap() { + WalletOrPubkey::Wallet(k) => k.clone(), + WalletOrPubkey::Pubkey(_) => { + return Err(CommandError::msg("leaf delegate keypair required")); + } + } + }; + + let leaf_owner = match input.leaf_owner { + Some(WalletOrPubkey::Wallet(k)) => k.pubkey(), + Some(WalletOrPubkey::Pubkey(p)) => p, + None => return Err(CommandError::msg("leaf_owner is required".to_string())), + }; + + // if delegate is signing, leaf delegate otherwise leaf owner + let leaf_delegate = if delegate_is_signing { + match input.leaf_delegate { + Some(keypair) => keypair.pubkey(), + None => return Err(CommandError::msg("leaf delegate keypair required")), + } + } else { + leaf_owner + }; + + let ix = BurnBuilder::new() + .tree_config(tree_config) + .leaf_owner(leaf_owner, !delegate_is_signing) + .leaf_delegate(leaf_delegate, delegate_is_signing) + .merkle_tree(merkle_tree) + .root(root) + .data_hash(data_hash) + .creator_hash(creator_hash) + .nonce(nonce) + .index(index) + .instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, signer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/compression/create_tree.rs b/crates/cmds-solana/src/compression/create_tree.rs new file mode 100644 index 00000000..2be253eb --- /dev/null +++ b/crates/cmds-solana/src/compression/create_tree.rs @@ -0,0 +1,312 @@ +use crate::prelude::*; +use solana_program::system_instruction; +use solana_sdk::pubkey::Pubkey; +use spl_account_compression::{state::CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1, ConcurrentMerkleTree}; +use std::mem::size_of; + +// Command Name +const NAME: &str = "create_tree"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/create_tree.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub creator: Wallet, + pub merkle_tree: Wallet, + pub max_depth: u32, + pub max_buffer: u32, + pub canopy_levels: Option, + is_public: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde_as(as = "Option")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let bubble_gum_program_id = mpl_bubblegum::ID; + + // Allocate tree's account + + // Only the following permutations are valid: + let merkle_tree_account_size: usize = match input.max_depth { + 3 => match input.max_buffer { + 8 => { + const MAX_DEPTH: usize = 3; + const MAX_BUFFER_SIZE: usize = 8; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 5 => match input.max_buffer { + 8 => { + const MAX_DEPTH: usize = 5; + const MAX_BUFFER_SIZE: usize = 8; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 14 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 14; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + 256 => { + const MAX_DEPTH: usize = 14; + const MAX_BUFFER_SIZE: usize = 256; + size_of::>() + } + 1024 => { + const MAX_DEPTH: usize = 14; + const MAX_BUFFER_SIZE: usize = 1024; + size_of::>() + } + 2048 => { + const MAX_DEPTH: usize = 14; + const MAX_BUFFER_SIZE: usize = 2048; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 15 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 15; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 16 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 16; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 17 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 17; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 18 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 18; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 19 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 19; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 20 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 20; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + 256 => { + const MAX_DEPTH: usize = 20; + const MAX_BUFFER_SIZE: usize = 256; + size_of::>() + } + 1024 => { + const MAX_DEPTH: usize = 20; + const MAX_BUFFER_SIZE: usize = 1024; + size_of::>() + } + 2048 => { + const MAX_DEPTH: usize = 20; + const MAX_BUFFER_SIZE: usize = 2048; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 24 => match input.max_buffer { + 64 => { + const MAX_DEPTH: usize = 24; + const MAX_BUFFER_SIZE: usize = 64; + size_of::>() + } + 256 => { + const MAX_DEPTH: usize = 24; + const MAX_BUFFER_SIZE: usize = 256; + size_of::>() + } + 512 => { + const MAX_DEPTH: usize = 24; + const MAX_BUFFER_SIZE: usize = 512; + size_of::>() + } + 1024 => { + const MAX_DEPTH: usize = 24; + const MAX_BUFFER_SIZE: usize = 1024; + size_of::>() + } + 2048 => { + const MAX_DEPTH: usize = 24; + const MAX_BUFFER_SIZE: usize = 2048; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 26 => match input.max_buffer { + 512 => { + const MAX_DEPTH: usize = 26; + const MAX_BUFFER_SIZE: usize = 512; + size_of::>() + } + 1024 => { + const MAX_DEPTH: usize = 26; + const MAX_BUFFER_SIZE: usize = 1024; + size_of::>() + } + 2048 => { + const MAX_DEPTH: usize = 26; + const MAX_BUFFER_SIZE: usize = 2048; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + 30 => match input.max_buffer { + 512 => { + const MAX_DEPTH: usize = 30; + const MAX_BUFFER_SIZE: usize = 512; + size_of::>() + } + 1024 => { + const MAX_DEPTH: usize = 30; + const MAX_BUFFER_SIZE: usize = 1024; + size_of::>() + } + 2048 => { + const MAX_DEPTH: usize = 30; + const MAX_BUFFER_SIZE: usize = 2048; + size_of::>() + } + _ => { + return Err(anyhow::anyhow!("invalid max_buffer_size")); + } + }, + + _ => { + return Err(anyhow::anyhow!("invalid max_depth_size")); + } + }; + + // To initialize a canopy on a ConcurrentMerkleTree account, you must initialize + // the ConcurrentMerkleTree account with additional bytes. The number of additional bytes + // needed is `(pow(2, N+1)-1) * 32`, where `N` is the number of levels of the merkle tree + // you want the canopy to cache. + // + //https://github.com/solana-labs/solana-program-library/blob/9610bed5349f7a198903140cf2b74a727477b818/account-compression/programs/account-compression/src/canopy.rs + //https://github.com/solana-labs/solana-program-library/blob/9610bed5349f7a198903140cf2b74a727477b818/account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts#L209 + + let canopy_size = if let Some(canopy_levels) = input.canopy_levels { + canopy_levels * 32 + } else { + 0 + }; + + let merkle_tree_account_size: usize = + CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1 + merkle_tree_account_size + canopy_size as usize; + + let rent = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(merkle_tree_account_size) + .await?; + + let create_merkle_account_ix = system_instruction::create_account( + &input.payer.pubkey(), + &input.merkle_tree.pubkey(), + rent, + u64::try_from(merkle_tree_account_size).unwrap(), + &spl_account_compression::ID, + ); + + // Create Tree + + let pubkey = &input.merkle_tree.pubkey(); + let seeds = &[pubkey.as_ref()]; + let tree_config = Pubkey::find_program_address(seeds, &bubble_gum_program_id).0; + + let create_tree_config_ix = mpl_bubblegum::instructions::CreateTreeConfigBuilder::new() + .tree_config(tree_config) + .merkle_tree(input.merkle_tree.pubkey()) + .payer(input.payer.pubkey()) + .tree_creator(input.creator.pubkey()) + .max_depth(input.max_depth) + .max_buffer_size(input.max_buffer) + .public(input.is_public.is_some()) + .instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.creator, input.merkle_tree].into(), + instructions: [create_merkle_account_ix, create_tree_config_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "tree_config" => tree_config, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/compression/metadata.json b/crates/cmds-solana/src/compression/metadata.json new file mode 100644 index 00000000..c3c89306 --- /dev/null +++ b/crates/cmds-solana/src/compression/metadata.json @@ -0,0 +1,27 @@ +{ + "name": "Bubble Gum", + "symbol": "BGUM", + "uri": "https://bubblegum.com", + "seller_fee_basis_points": 500, + "primary_sale_happened": false, + "is_mutable": false, + "edition_nonce": 0, + "token_standard": "NonFungible", + "collection": { + "verified": false, + "key": "CqnbStqccanSFCs7cexXySzTTyFsJG8wKrikR4LbxCai" + }, + "uses": { + "use_method": "Single", + "remaining": 1, + "total": 1 + }, + "token_program_version": "Original", + "creators": [ + { + "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", + "verified": true, + "share": 100 + } + ] +} diff --git a/crates/cmds-solana/src/compression/mint_to_collection_v1.rs b/crates/cmds-solana/src/compression/mint_to_collection_v1.rs new file mode 100644 index 00000000..140c8666 --- /dev/null +++ b/crates/cmds-solana/src/compression/mint_to_collection_v1.rs @@ -0,0 +1,192 @@ +use super::MetadataBubblegum; +use crate::compression::get_leaf_schema_event; +use crate::prelude::*; + +use bytes::Bytes; +use mpl_bubblegum::instructions::MintToCollectionV1Builder; +use solana_sdk::pubkey::Pubkey; +use tracing::info; + +// Command Name +const MINT_COMPRESSED_NFT: &str = "mint_cNFT_to_collection"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/mint_to_collection_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(MINT_COMPRESSED_NFT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(MINT_COMPRESSED_NFT, |_| { + build() +})); + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + #[serde_as(as = "AsPubkey")] + pub collection_mint: Pubkey, + pub collection_authority: Wallet, + pub creator_or_delegate: Wallet, + #[serde(default = "value::default::bool_false")] + pub is_delegate_authority: bool, + #[serde_as(as = "AsPubkey")] + pub tree_config: Pubkey, + #[serde_as(as = "AsPubkey")] + pub merkle_tree: Pubkey, + #[serde_as(as = "AsPubkey")] + pub leaf_owner: Pubkey, + #[serde_as(as = "Option")] + pub leaf_delegate: Option, + pub metadata: MetadataBubblegum, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde_as(as = "Option")] + signature: Option, + #[serde_as(as = "Option")] + id: Option, + nonce: Option, + creator_hash: Option, + data_hash: Option, + leaf_hash: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // Bubblegum address if none is provided + // TODO update to MetadataDelegateRecord::find_pda + let collection_authority_record_pda = input.is_delegate_authority.then_some( + mpl_token_metadata::accounts::CollectionAuthorityRecord::find_pda( + &input.collection_mint, + &input.collection_authority.pubkey(), + ) + .0, + ); + + let collection_metadata = + mpl_token_metadata::accounts::Metadata::find_pda(&input.collection_mint).0; + + let collection_edition = + mpl_token_metadata::accounts::MasterEdition::find_pda(&input.collection_mint).0; + + let mut metadata = input.metadata; + metadata.collection = Some(super::Collection { + verified: false, + key: input.collection_mint.to_string(), + }); + info!("metadata: {:?}", metadata); + info!( + "collection authority {}", + input.collection_authority.pubkey() + ); + let mint_ix = MintToCollectionV1Builder::new() + .tree_config(input.tree_config) + .leaf_owner(input.leaf_owner) + .leaf_delegate(input.leaf_delegate.unwrap_or(input.leaf_owner)) + .merkle_tree(input.merkle_tree) + .payer(input.payer.pubkey()) + .tree_creator_or_delegate(input.creator_or_delegate.pubkey()) + .collection_authority(input.collection_authority.pubkey()) + .collection_authority_record_pda(collection_authority_record_pda) + .collection_mint(input.collection_mint) + .collection_metadata(collection_metadata) + .collection_edition(collection_edition) + // Optional with defaults + // .bubblegum_signer(bubblegum_signer) + // .log_wrapper(log_wrapper) + // .compression_program(compression_program) + // .token_metadata_program(token_metadata_program) + // .system_program(system_program) + .metadata(metadata.into()) + .instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.creator_or_delegate, + input.collection_authority, + ] + .into(), + instructions: [mint_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + let mut leaf_schema = None; + if let Some(signature) = signature { + leaf_schema = Some(get_leaf_schema_event(ctx, signature, true).await?.1); + info!("{:?}", leaf_schema); + } + + let id = leaf_schema.as_ref().map(|schema| schema.id()); + let nonce = leaf_schema.as_ref().map(|schema| schema.nonce()); + let data_hash = leaf_schema + .as_ref() + .map(|schema| bytes::Bytes::copy_from_slice(&schema.data_hash())); + let leaf_hash = leaf_schema + .as_ref() + .map(|schema| bytes::Bytes::copy_from_slice(&schema.hash())); + let creator_hash = + leaf_schema.map(|schema| bytes::Bytes::copy_from_slice(&schema.creator_hash())); + + Ok(Output { + signature, + id, + nonce, + creator_hash, + data_hash, + leaf_hash, + }) +} + +#[cfg(test)] +mod tests { + use anchor_lang::AnchorDeserialize; + use mpl_bubblegum::LeafSchemaEvent; + use spl_account_compression::{ + events::{ApplicationDataEvent, ApplicationDataEventV1}, + AccountCompressionEvent, + }; + + // use crate::compression::get_leaf_schema_event; + + /* + * devnet resetted + #[tokio::test] + async fn test_get_leaf_schema() { + tracing_subscriber::fmt::try_init().ok(); + const SIGNATURE: &str = "3a4asE3CbWjmpEBpxLwctqgF2BfwzUhsaDdrQS9ZnanNrWJYfxc8hWfow7gCF9MVjdB2SQ1svg8QujDMjNknufCU"; + let result = get_leaf_schema_event(<_>::default(), SIGNATURE.parse().unwrap(), true).await; + dbg!(&result); + result.unwrap(); + } + */ + + #[test] + fn test_parse_instruction_data() { + const DATA: &str = "2GJh7oUmkZKnjHLqLHwKU8DSRK2PJ6gqTyCkQzE4TvouB75xxWG7AbvGgMBvuw5QTbGAFKcUJGy9ftDfxdkk55MRYXruCpNqFcHp5GijZRzf3SCuHveuURcjqJ6owS9T9DBxxij7cQgfwfZzuR7LavH7MsiDatmpEj3NnmQdJRxDGm3S3JcsVqxy6Zd9zieqHDKR899HohKdxhJ7rKkZfbubHLxmH9vGvChktsHX5DywH1CxHnoiG6918Yjx1xPdLduc71Wx97C3xs7cw9pd9etUtYRCE"; + let bytes = bs58::decode(DATA).into_vec().unwrap(); + let event = AccountCompressionEvent::try_from_slice(&bytes).unwrap(); + let AccountCompressionEvent::ApplicationData(ApplicationDataEvent::V1( + ApplicationDataEventV1 { application_data }, + )) = event + else { + panic!("wrong variant"); + }; + let leaf_schema = LeafSchemaEvent::try_from_slice(&application_data).unwrap(); + dbg!(leaf_schema); + } +} diff --git a/crates/cmds-solana/src/compression/mint_v1.rs b/crates/cmds-solana/src/compression/mint_v1.rs new file mode 100644 index 00000000..d5f73485 --- /dev/null +++ b/crates/cmds-solana/src/compression/mint_v1.rs @@ -0,0 +1,101 @@ +use crate::{compression::get_leaf_schema_event, prelude::*}; +use bytes::Bytes; +use mpl_bubblegum::instructions::MintV1Builder; +use solana_sdk::pubkey::Pubkey; +use tracing::info; + +use super::MetadataBubblegum; + +// Command Name +const NAME: &str = "mint_compressed_NFT"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/mint_compressed_NFT.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub creator_or_delegate: Wallet, + #[serde(with = "value::pubkey")] + pub tree_config: Pubkey, + #[serde(with = "value::pubkey")] + pub merkle_tree: Pubkey, + #[serde(with = "value::pubkey")] + pub leaf_owner: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub leaf_delegate: Option, + pub metadata: MetadataBubblegum, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + #[serde(with = "value::pubkey::opt")] + id: Option, + nonce: Option, + creator_hash: Option, + data_hash: Option, + leaf_hash: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mint_ix = MintV1Builder::new() + .leaf_delegate(input.leaf_delegate.unwrap_or(input.leaf_owner)) + .leaf_owner(input.leaf_owner) + .merkle_tree(input.merkle_tree) + .payer(input.payer.pubkey()) + .tree_config(input.tree_config) + .tree_creator_or_delegate(input.creator_or_delegate.pubkey()) + .metadata(input.metadata.into()) + .instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.creator_or_delegate].into(), + instructions: [mint_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + let mut leaf_schema = None; + if let Some(signature) = signature { + leaf_schema = Some(get_leaf_schema_event(ctx, signature, false).await?.1); + info!("{:?}", leaf_schema); + } + + let id = leaf_schema.as_ref().map(|schema| schema.id()); + let nonce = leaf_schema.as_ref().map(|schema| schema.nonce()); + let data_hash = leaf_schema + .as_ref() + .map(|schema| bytes::Bytes::copy_from_slice(&schema.data_hash())); + let leaf_hash = leaf_schema + .as_ref() + .map(|schema| bytes::Bytes::copy_from_slice(&schema.hash())); + let creator_hash = + leaf_schema.map(|schema| bytes::Bytes::copy_from_slice(&schema.creator_hash())); + + Ok(Output { + signature, + id, + nonce, + creator_hash, + data_hash, + leaf_hash, + }) +} diff --git a/crates/cmds-solana/src/compression/mod.rs b/crates/cmds-solana/src/compression/mod.rs new file mode 100644 index 00000000..bd52b59c --- /dev/null +++ b/crates/cmds-solana/src/compression/mod.rs @@ -0,0 +1,262 @@ +use crate::prelude::*; +use anchor_lang::AnchorDeserialize; +use anyhow::{anyhow, Context as _}; +use flow_lib::command::CommandError; +use mpl_bubblegum::types::LeafSchema; +use mpl_bubblegum::types::{MetadataArgs, UpdateArgs}; +use mpl_bubblegum::LeafSchemaEvent; +use serde::{Deserialize, Serialize}; +use solana_client::rpc_config::RpcTransactionConfig; +use solana_program::pubkey::Pubkey; +use solana_sdk::commitment_config::CommitmentConfig; +use solana_transaction_status::UiParsedInstruction; +use solana_transaction_status::{ + option_serializer::OptionSerializer, UiInstruction, UiTransactionEncoding, +}; +use spl_account_compression::{ + events::{ApplicationDataEvent, ApplicationDataEventV1}, + AccountCompressionEvent, +}; +use std::str::FromStr; +use tracing::info; + +pub mod types; + +pub mod burn; +pub mod create_tree; +pub mod mint_to_collection_v1; +pub mod mint_v1; +pub mod transfer; +pub mod update; + +pub use super::WalletOrPubkey; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum TokenProgramVersion { + Original, + Token2022, +} + +impl From for mpl_bubblegum::types::TokenProgramVersion { + fn from(v: TokenProgramVersion) -> Self { + match v { + TokenProgramVersion::Original => mpl_bubblegum::types::TokenProgramVersion::Original, + TokenProgramVersion::Token2022 => mpl_bubblegum::types::TokenProgramVersion::Token2022, + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Creator { + pub address: String, + pub verified: bool, + // In percentages, NOT basis points ;) Watch out! + pub share: u8, +} + +impl From for mpl_bubblegum::types::Creator { + fn from(v: Creator) -> Self { + mpl_bubblegum::types::Creator { + address: Pubkey::from_str(&v.address).unwrap(), + verified: v.verified, + share: v.share, + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum TokenStandard { + NonFungible, // This is a master edition + FungibleAsset, // A token with metadata that can also have attrributes + Fungible, // A token with simple metadata + NonFungibleEdition, // This is a limited edition +} + +impl From for mpl_bubblegum::types::TokenStandard { + fn from(v: TokenStandard) -> Self { + match v { + TokenStandard::NonFungible => mpl_bubblegum::types::TokenStandard::NonFungible, + TokenStandard::FungibleAsset => mpl_bubblegum::types::TokenStandard::FungibleAsset, + TokenStandard::Fungible => mpl_bubblegum::types::TokenStandard::Fungible, + TokenStandard::NonFungibleEdition => { + mpl_bubblegum::types::TokenStandard::NonFungibleEdition + } + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum UseMethod { + Burn, + Multiple, + Single, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Uses { + // 17 bytes + Option byte + pub use_method: UseMethod, //1 + pub remaining: u64, //8 + pub total: u64, //8 +} + +impl From for mpl_bubblegum::types::Uses { + fn from(v: Uses) -> Self { + mpl_bubblegum::types::Uses { + use_method: match v.use_method { + UseMethod::Burn => mpl_bubblegum::types::UseMethod::Burn, + UseMethod::Multiple => mpl_bubblegum::types::UseMethod::Multiple, + UseMethod::Single => mpl_bubblegum::types::UseMethod::Single, + }, + remaining: v.remaining, + total: v.total, + } + } +} + +#[repr(C)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Collection { + pub verified: bool, + pub key: String, +} + +impl From for mpl_bubblegum::types::Collection { + fn from(v: Collection) -> Self { + mpl_bubblegum::types::Collection { + verified: v.verified, + key: Pubkey::from_str(&v.key).unwrap(), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct MetadataBubblegum { + /// The name of the asset + pub name: String, + /// The symbol for the asset + pub symbol: String, + /// URI pointing to JSON representing the asset + pub uri: String, + /// Royalty basis points that goes to creators in secondary sales (0-10000) + pub seller_fee_basis_points: u16, + // Immutable, once flipped, all sales of this metadata are considered secondary. + pub primary_sale_happened: bool, + // Whether or not the data struct is mutable, default is not + pub is_mutable: bool, + /// nonce for easy calculation of editions, if present + pub edition_nonce: Option, + /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. + pub token_standard: Option, + /// Collection + pub collection: Option, + /// Uses + pub uses: Option, + pub token_program_version: TokenProgramVersion, + pub creators: Vec, +} + +// implement From MetadataBubblegum to MetadataArgs +impl From for MetadataArgs { + fn from(v: MetadataBubblegum) -> Self { + Self { + name: v.name, + symbol: v.symbol, + uri: v.uri, + seller_fee_basis_points: v.seller_fee_basis_points, + primary_sale_happened: v.primary_sale_happened, + is_mutable: v.is_mutable, + edition_nonce: v.edition_nonce, + token_standard: v.token_standard.map(Into::into), + collection: v.collection.map(Into::into), + uses: v.uses.map(Into::into), + token_program_version: v.token_program_version.into(), + creators: v.creators.into_iter().map(Into::into).collect(), + } + } +} + +impl From for UpdateArgs { + fn from(v: MetadataBubblegum) -> Self { + Self { + name: Some(v.name), + symbol: Some(v.symbol), + uri: Some(v.uri), + creators: Some(v.creators.into_iter().map(Into::into).collect()), + seller_fee_basis_points: Some(v.seller_fee_basis_points), + primary_sale_happened: Some(v.primary_sale_happened), + is_mutable: Some(v.is_mutable), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct GetAssetResponse { + pub id: String, + pub result: T, + pub jsonrpc: String, +} + +pub async fn get_leaf_schema_event( + ctx: Context, + signature: Signature, + is_mint_to_collection: bool, +) -> Result<(LeafSchemaEvent, LeafSchema), anyhow::Error> { + let index = if is_mint_to_collection { 1 } else { 0 }; + + let config = RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::JsonParsed), + commitment: Some(CommitmentConfig::confirmed()), + // we only send "legacy" tx at the moment + max_supported_transaction_version: None, + }; + let tx_meta = ctx + .solana_client + .get_transaction_with_config(&signature, config) + .await? + .transaction + .meta + .map(|meta| meta.inner_instructions); + + let tx_meta = match tx_meta { + Some(OptionSerializer::Some(m)) => Some(m), + Some(OptionSerializer::None) | Some(OptionSerializer::Skip) | None => None, + }; + + info!("tx_meta: {:?}", tx_meta); + + let inner_instruction = tx_meta + .as_ref() + .ok_or_else(|| CommandError::msg("tx_meta is None"))? + .last() // Inserted 2 priority fee instructions at the beginning + .ok_or_else(|| CommandError::msg("No inner instruction"))? + .instructions + .get(index) + .ok_or_else(|| CommandError::msg("No instruction at index 1"))? + .clone(); + + let data_bs58 = match inner_instruction { + UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded(i)) => i.data, + _ => { + return Err(anyhow!( + "expected UiInstruction::Parsed(PartiallyDecoded(_)), got {:?}", + inner_instruction + )); + } + }; + let bytes = bs58::decode(data_bs58).into_vec().context("bs58::decode")?; + let event = + AccountCompressionEvent::try_from_slice(&bytes).context("parse AccountCompressionEvent")?; + let AccountCompressionEvent::ApplicationData(ApplicationDataEvent::V1( + ApplicationDataEventV1 { application_data }, + )) = event + else { + return Err(anyhow!("wrong AccountCompressionEvent variant")); + }; + let leaf_schema_event = + LeafSchemaEvent::try_from_slice(&application_data).context("parse LeafSchemaEvent")?; + + let leaf_schema = leaf_schema_event.schema.clone(); + + Ok((leaf_schema_event, leaf_schema)) +} diff --git a/crates/cmds-solana/src/compression/transfer.rs b/crates/cmds-solana/src/compression/transfer.rs new file mode 100644 index 00000000..f2cf9a9c --- /dev/null +++ b/crates/cmds-solana/src/compression/transfer.rs @@ -0,0 +1,226 @@ +use super::{ + types::asset::{Asset, AssetProof}, + GetAssetResponse, WalletOrPubkey, +}; +use crate::prelude::*; +use mpl_bubblegum::instructions::TransferBuilder; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; +use std::str::FromStr; + +// Command Name +const NAME: &str = "transfer_cNFT"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/transfer.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + // #[serde(with = "value::pubkey")] + // pub asset_id: Pubkey, + #[serde(default)] + pub leaf_owner: Option, + #[serde(with = "value::pubkey")] + pub new_leaf_owner: Pubkey, + // + pub das_get_asset_proof: Option>, + pub das_get_asset: Option>, + // + pub leaf_delegate: Option, + #[serde(default, with = "value::pubkey::opt")] + pub tree_config: Option, + #[serde(default, with = "value::pubkey::opt")] + pub merkle_tree: Option, + pub root: Option, + pub data_hash: Option, + pub creator_hash: Option, + pub leaf_id: Option, + pub index: Option, + pub proof: Option>, + // + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // get from asset proof: merkle tree, root, index, proof + // get from asset: data hash, creator hash, leaf id or nonce, metadata + + // Get proof + let proof = match input.proof { + Some(proof) => proof.to_owned(), + None => match &input.das_get_asset_proof { + Some(proof) => proof.result.proof.to_owned(), + None => return Err(CommandError::msg("proof is required")), + }, + }; + + let proof: Result, CommandError> = proof + .iter() + .map(|node| { + let pubkey = + Pubkey::from_str(node).map_err(|_| CommandError::msg("Invalid pubkey string"))?; + Ok(AccountMeta { + pubkey, + is_signer: false, + is_writable: false, + }) + }) + .collect(); + + let proof = proof?; + + // get root + let root = match &input.root { + Some(root) => root, + None => match &input.das_get_asset_proof { + Some(proof) => &proof.result.root, + None => return Err(CommandError::msg("root is required")), + }, + }; + + let root = Pubkey::from_str(root) + .map_err(|_| CommandError::msg("Invalid root string"))? + .to_bytes(); + + // get data hash + let data_hash = match input.data_hash { + Some(data_hash) => data_hash, + None => match input.das_get_asset.clone() { + Some(asset) => asset.result.compression.unwrap().data_hash, + None => return Err(CommandError::msg("data_hash is required")), + }, + }; + + let data_hash = Pubkey::from_str(&data_hash) + .map_err(|_| CommandError::msg("Invalid data_hash string"))? + .to_bytes(); + + // get creator hash + let creator_hash = match input.creator_hash { + Some(creator_hash) => creator_hash, + None => match input.das_get_asset.clone() { + Some(asset) => asset.result.compression.unwrap().creator_hash, + None => return Err(CommandError::msg("creator_hash is required")), + }, + }; + + let creator_hash = Pubkey::from_str(&creator_hash) + .map_err(|_| CommandError::msg("Invalid creator_hash string"))? + .to_bytes(); + + // who is signing? + let delegate_is_signing = input.leaf_delegate.is_some(); + + let signer = match delegate_is_signing { + true => input.leaf_delegate.as_ref().unwrap().clone(), + false => match input.leaf_owner.as_ref().unwrap() { + WalletOrPubkey::Wallet(k) => k.clone(), + WalletOrPubkey::Pubkey(_) => { + return Err(CommandError::msg("leaf delegate keypair required")); + } + }, + }; + + let leaf_owner = match input.leaf_owner { + Some(WalletOrPubkey::Wallet(k)) => k.pubkey(), + Some(WalletOrPubkey::Pubkey(p)) => p, + None => return Err(CommandError::msg("leaf_owner is required".to_string())), + }; + + // if delegate is signing, leaf delegate otherwise leaf owner + let leaf_delegate = match delegate_is_signing { + true => match input.leaf_delegate { + Some(keypair) => keypair.pubkey(), + None => return Err(CommandError::msg("leaf delegate keypair required")), + }, + false => leaf_owner, + }; + + // leaf_id aka nonce + let nonce = match input.leaf_id { + Some(leaf_id) => leaf_id, + None => match input.das_get_asset { + Some(asset) => asset + .result + .compression + .unwrap() + .leaf_id + .try_into() + .unwrap(), + None => return Err(CommandError::msg("leaf_id is required")), + }, + }; + + // get index + + let index = match input.index { + Some(index) => index, + None => match input.das_get_asset_proof.clone() { + Some(asset) => (asset.result.node_index - 2 * asset.result.proof.len() as i64) + .try_into() + .unwrap(), + None => return Err(CommandError::msg("index is required")), + }, + }; + + let merkle_tree = match input.merkle_tree { + Some(merkle_tree) => merkle_tree, + None => match input.das_get_asset_proof { + Some(asset) => Pubkey::from_str(&asset.result.tree_id).unwrap(), + None => return Err(CommandError::msg("merkle_tree is required")), + }, + }; + + let tree_config = input + .tree_config + .unwrap_or(mpl_bubblegum::accounts::TreeConfig::find_pda(&merkle_tree).0); + + let mint_ix = TransferBuilder::new() + .new_leaf_owner(input.new_leaf_owner) + .tree_config(tree_config) + .leaf_owner(leaf_owner, !delegate_is_signing) + .leaf_delegate(leaf_delegate, delegate_is_signing) + .merkle_tree(merkle_tree) + //Optional with defaults + // .log_wrapper(log_wrapper) + // .compression_program(compression_program) + // .system_program(system_program) + .root(root) + .data_hash(data_hash) + .creator_hash(creator_hash) + .nonce(nonce) + .index(index) + .add_remaining_accounts(&proof) + .instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, signer].into(), + instructions: [mint_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/compression/types/asset.rs b/crates/cmds-solana/src/compression/types/asset.rs new file mode 100644 index 00000000..251714e3 --- /dev/null +++ b/crates/cmds-solana/src/compression/types/asset.rs @@ -0,0 +1,334 @@ +use std::collections::BTreeMap; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ChainMutability { + Immutable, + Mutable, + Unknown, +} +use { + serde::{Deserialize, Serialize}, + std::collections::HashMap, +}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct AssetProof { + pub root: String, + pub proof: Vec, + pub node_index: i64, + pub leaf: String, + pub tree_id: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Interface { + #[serde(rename = "V1_NFT")] + V1NFT, + #[serde(rename = "V1_PRINT")] + V1PRINT, + #[serde(rename = "LEGACY_NFT")] + // TODO: change on version bump + #[allow(non_camel_case_types)] + LEGACY_NFT, + #[serde(rename = "V2_NFT")] + Nft, + #[serde(rename = "FungibleAsset")] + FungibleAsset, + #[serde(rename = "Custom")] + Custom, + #[serde(rename = "Identity")] + Identity, + #[serde(rename = "Executable")] + Executable, + #[serde(rename = "ProgrammableNFT")] + ProgrammableNFT, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Quality { + #[serde(rename = "$$schema")] + pub schema: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Context { + #[serde(rename = "wallet-default")] + WalletDefault, + #[serde(rename = "web-desktop")] + WebDesktop, + #[serde(rename = "web-mobile")] + WebMobile, + #[serde(rename = "app-mobile")] + AppMobile, + #[serde(rename = "app-desktop")] + AppDesktop, + #[serde(rename = "app")] + App, + #[serde(rename = "vr")] + Vr, +} + +pub type Contexts = Vec; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct File { + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mime: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub contexts: Option, +} + +pub type Files = Vec; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct MetadataMap(BTreeMap); + +impl MetadataMap { + pub const fn new() -> Self { + Self(BTreeMap::new()) + } + + pub const fn inner(&self) -> &BTreeMap { + &self.0 + } + + pub fn set_item(&mut self, key: &str, value: serde_json::Value) -> &mut Self { + self.0.insert(key.to_string(), value); + self + } + + pub fn get_item(&self, key: &str) -> Option<&serde_json::Value> { + self.0.get(key) + } +} + +// TODO sub schema support +pub type Links = HashMap; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Content { + #[serde(rename = "$schema")] + pub schema: String, + pub json_uri: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub files: Option, + pub metadata: MetadataMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub links: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum Scope { + #[serde(rename = "full")] + Full, + #[serde(rename = "royalty")] + Royalty, + #[serde(rename = "metadata")] + Metadata, + #[serde(rename = "extension")] + Extension, +} + +impl From for Scope { + fn from(s: String) -> Self { + match &*s { + "royalty" => Scope::Royalty, + "metadata" => Scope::Metadata, + "extension" => Scope::Extension, + _ => Scope::Full, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Authority { + pub address: String, + pub scopes: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Compression { + pub eligible: bool, + pub compressed: bool, + pub data_hash: String, + pub creator_hash: String, + pub asset_hash: String, + pub tree: String, + pub seq: i64, + pub leaf_id: i64, +} + +pub type GroupKey = String; +pub type GroupValue = String; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Group { + pub group_key: String, + pub group_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub verified: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum RoyaltyModel { + #[serde(rename = "creators")] + Creators, + #[serde(rename = "fanout")] + Fanout, + #[serde(rename = "single")] + Single, +} + +impl From for RoyaltyModel { + fn from(s: String) -> Self { + match &*s { + "creators" => RoyaltyModel::Creators, + "fanout" => RoyaltyModel::Fanout, + "single" => RoyaltyModel::Single, + _ => RoyaltyModel::Creators, + } + } +} + +// #[cfg(feature = "sql_types")] +// impl From for RoyaltyModel { +// fn from(s: RoyaltyTargetType) -> Self { +// match s { +// RoyaltyTargetType::Creators => RoyaltyModel::Creators, +// RoyaltyTargetType::Fanout => RoyaltyModel::Fanout, +// RoyaltyTargetType::Single => RoyaltyModel::Single, +// _ => RoyaltyModel::Creators, +// } +// } +// } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Royalty { + pub royalty_model: RoyaltyModel, + pub target: Option, + pub percent: f64, + pub basis_points: u32, + pub primary_sale_happened: bool, + pub locked: bool, +} + +pub type Address = String; +pub type Share = String; +pub type Verified = bool; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Creator { + pub address: String, + pub share: i32, + pub verified: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum OwnershipModel { + #[serde(rename = "single")] + Single, + #[serde(rename = "token")] + Token, +} + +impl From for OwnershipModel { + fn from(s: String) -> Self { + match &*s { + "single" => OwnershipModel::Single, + "token" => OwnershipModel::Token, + _ => OwnershipModel::Single, + } + } +} + +// #[cfg(feature = "sql_types")] +// impl From for OwnershipModel { +// fn from(s: OwnerType) -> Self { +// match s { +// OwnerType::Token => OwnershipModel::Token, +// OwnerType::Single => OwnershipModel::Single, +// _ => OwnershipModel::Single, +// } +// } +// } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Ownership { + pub frozen: bool, + pub delegated: bool, + pub delegate: Option, + pub ownership_model: OwnershipModel, + pub owner: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum UseMethod { + Burn, + Multiple, + Single, +} + +impl From for UseMethod { + fn from(s: String) -> Self { + match &*s { + "Burn" => UseMethod::Burn, + "Single" => UseMethod::Single, + "Multiple" => UseMethod::Multiple, + _ => UseMethod::Single, + } + } +} + +pub type Mutability = bool; + +impl From for Mutability { + fn from(s: ChainMutability) -> Self { + match s { + ChainMutability::Mutable => true, + ChainMutability::Immutable => false, + _ => true, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Uses { + pub use_method: UseMethod, + pub remaining: u64, + pub total: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Supply { + pub print_max_supply: u64, + pub print_current_supply: u64, + pub edition_nonce: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Asset { + pub interface: Interface, + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub authorities: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub compression: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub grouping: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub royalty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub creators: Option>, + pub ownership: Ownership, + #[serde(skip_serializing_if = "Option::is_none")] + pub uses: Option, + pub supply: Option, + pub mutable: bool, + pub burnt: bool, +} diff --git a/crates/cmds-solana/src/compression/types/mod.rs b/crates/cmds-solana/src/compression/types/mod.rs new file mode 100644 index 00000000..64cdb62c --- /dev/null +++ b/crates/cmds-solana/src/compression/types/mod.rs @@ -0,0 +1 @@ +pub mod asset; diff --git a/crates/cmds-solana/src/compression/update.rs b/crates/cmds-solana/src/compression/update.rs new file mode 100644 index 00000000..b3638636 --- /dev/null +++ b/crates/cmds-solana/src/compression/update.rs @@ -0,0 +1,193 @@ +use super::{ + types::asset::{Asset, AssetProof}, + GetAssetResponse, MetadataBubblegum, +}; +use crate::prelude::*; +use mpl_bubblegum::instructions::UpdateMetadataBuilder; +use solana_program::instruction::AccountMeta; +use std::str::FromStr; + +// Command Name +const NAME: &str = "update_cNFT"; + +const DEFINITION: &str = flow_lib::node_definition!("compression/update_cNFT.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub authority: Wallet, + #[serde(default)] + pub leaf_owner: Pubkey, + // + pub das_get_asset_proof: Option>, + pub das_get_asset: Option>, + // + #[serde(with = "value::pubkey")] + pub leaf_delegate: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub collection_mint: Option, + // + #[serde(default, with = "value::pubkey::opt")] + pub merkle_tree: Option, + pub root: Option, + pub leaf_id: Option, + pub index: Option, + pub proof: Option>, + // + pub current_metadata: MetadataBubblegum, + pub updated_metadata: MetadataBubblegum, + // + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // get from asset proof: merkle tree, root, index, proof + // get from asset: data hash, creator hash, leaf id or nonce, metadata + + // Get proof + let proof = match input.proof { + Some(proof) => proof.to_owned(), + None => match &input.das_get_asset_proof { + Some(proof) => proof.result.proof.to_owned(), + None => return Err(CommandError::msg("proof is required")), + }, + }; + + let proof: Result, CommandError> = proof + .iter() + .map(|node| { + let pubkey = + Pubkey::from_str(node).map_err(|_| CommandError::msg("Invalid pubkey string"))?; + Ok(AccountMeta { + pubkey, + is_signer: false, + is_writable: false, + }) + }) + .collect(); + + let _proof = proof?; + + // get root + let root = match &input.root { + Some(root) => root, + None => match &input.das_get_asset_proof { + Some(proof) => &proof.result.root, + None => return Err(CommandError::msg("root is required")), + }, + }; + + let root = Pubkey::from_str(root) + .map_err(|_| CommandError::msg("Invalid root string"))? + .to_bytes(); + + // leaf_id aka nonce + let nonce = match input.leaf_id { + Some(leaf_id) => leaf_id, + None => match input.das_get_asset { + Some(asset) => asset + .result + .compression + .unwrap() + .leaf_id + .try_into() + .unwrap(), + None => return Err(CommandError::msg("leaf_id is required")), + }, + }; + + // get index + + let index = match input.index { + Some(index) => index, + None => match input.das_get_asset_proof.clone() { + Some(asset) => (asset.result.node_index - 2 * asset.result.proof.len() as i64) + .try_into() + .unwrap(), + None => return Err(CommandError::msg("index is required")), + }, + }; + + let merkle_tree = match input.merkle_tree { + Some(merkle_tree) => merkle_tree, + None => match input.das_get_asset_proof { + Some(asset) => Pubkey::from_str(&asset.result.tree_id).unwrap(), + None => return Err(CommandError::msg("merkle_tree is required")), + }, + }; + + let tree_config = mpl_bubblegum::accounts::TreeConfig::find_pda(&merkle_tree).0; + + let ix = match input.collection_mint { + Some(collection_mint) => { + let collection_authority_record_pda = + mpl_token_metadata::accounts::CollectionAuthorityRecord::find_pda( + &collection_mint, + &input.authority.pubkey(), + ) + .0; + + let collection_metadata = + mpl_token_metadata::accounts::Metadata::find_pda(&collection_mint).0; + + UpdateMetadataBuilder::new() + .payer(input.payer.pubkey()) + .tree_config(tree_config) + .authority(input.authority.pubkey()) + .collection_mint(Some(collection_mint)) + .collection_metadata(Some(collection_metadata)) + .collection_authority_record_pda(Some(collection_authority_record_pda)) + .leaf_owner(input.leaf_owner) + .leaf_delegate(input.leaf_delegate) + .merkle_tree(input.merkle_tree.unwrap()) + .root(root) + .nonce(nonce) + .index(index) + .update_args(input.updated_metadata.into()) + .instruction() + } + None => UpdateMetadataBuilder::new() + .payer(input.payer.pubkey()) + .tree_config(tree_config) + .authority(input.authority.pubkey()) + .leaf_owner(input.leaf_owner) + .leaf_delegate(input.leaf_delegate) + .merkle_tree(input.merkle_tree.unwrap()) + .root(root) + .nonce(nonce) + .index(index) + .update_args(input.updated_metadata.into()) + .instruction(), + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.authority].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/create_mint_account.rs b/crates/cmds-solana/src/create_mint_account.rs new file mode 100644 index 00000000..566e6fdc --- /dev/null +++ b/crates/cmds-solana/src/create_mint_account.rs @@ -0,0 +1,88 @@ +use crate::prelude::*; +use solana_sdk::program_pack::Pack; +use solana_sdk::system_instruction; +use spl_token::state::Mint; + +const NAME: &str = "create_mint_account"; + +const DEFINITION: &str = flow_lib::node_definition!("create_mint_account.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + decimals: u8, + mint_authority: Wallet, + #[serde(default, with = "value::pubkey::opt")] + freeze_authority: Option, + mint_account: Wallet, + #[serde(default)] + memo: String, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let lamports = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(Mint::LEN) + .await?; + + let instructions = [ + system_instruction::create_account( + &input.fee_payer.pubkey(), + &input.mint_account.pubkey(), + lamports, + Mint::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_mint2( + &spl_token::id(), + &input.mint_account.pubkey(), + &input.mint_authority.pubkey(), + input.freeze_authority.as_ref(), + input.decimals, + )?, + spl_memo::build_memo(input.memo.as_bytes(), &[&input.fee_payer.pubkey()]), + ] + .into(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.mint_authority, input.mint_account].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/create_token_account.rs b/crates/cmds-solana/src/create_token_account.rs new file mode 100644 index 00000000..0189a911 --- /dev/null +++ b/crates/cmds-solana/src/create_token_account.rs @@ -0,0 +1,106 @@ +use crate::prelude::*; +use solana_program::program_pack::Pack; +use solana_program::{system_instruction, system_program}; + +const SOLANA_CREATE_TOKEN_ACCOUNT: &str = "create_token_account"; + +const DEFINITION: &str = flow_lib::node_definition!("create_token_account.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(SOLANA_CREATE_TOKEN_ACCOUNT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(SOLANA_CREATE_TOKEN_ACCOUNT, |_| { + build() +})); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + owner: Pubkey, + fee_payer: Wallet, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + token_account: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await?; + + let account = input.token_account.pubkey(); + let system_account_ok = false; + let instructions = [ + system_instruction::create_account( + &input.fee_payer.pubkey(), + &account, + minimum_balance_for_rent_exemption, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_account( + &spl_token::id(), + &account, + &input.mint_account, + &input.owner, + )?, + ] + .into(); + + // TODO: with bundling, this data might be outdated when tx is submitted + if let Some(account_data) = ctx + .solana_client + .get_account_with_commitment(&account, ctx.solana_client.commitment()) + .await? + .value + { + if !(account_data.owner == system_program::id() && system_account_ok) { + return Err(crate::Error::custom(anyhow::anyhow!( + "Error: Account already exists: {}", + account + )) + .into()); + } + } + + let instructions = if input.submit { + Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.token_account].into(), + + instructions, + } + } else { + <_>::default() + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/das.rs b/crates/cmds-solana/src/das.rs new file mode 100644 index 00000000..80ab9e80 --- /dev/null +++ b/crates/cmds-solana/src/das.rs @@ -0,0 +1,98 @@ +use chrono::{DateTime, Utc}; +use flow_lib::command::prelude::*; +use reqwest::{header::CONTENT_TYPE, StatusCode}; +use serde_json::json; + +pub const NAME: &str = "das_api"; + +const DEFINITION: &str = flow_lib::node_definition!("das_api.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +enum DasMethod { + GetAsset, + GetAssetProof, + GetAssetsByOwner, + GetAssetsByCreator, + GetAssetsByAuthority, + GetAssetsbyCreator, + GetAssetsByGroup, + SearchAssets, + GetSignaturesForAsset, + GetTokenAccounts, +} + +#[derive(Serialize, Deserialize)] +struct Input { + url: String, + params: JsonValue, + method: DasMethod, + id: Option, +} + +#[derive(Serialize)] +struct Output { + response: JsonValue, +} + +async fn run(ctx: Context, input: Input) -> Result { + let content_type = "application/json"; + + // get time + let now: DateTime = Utc::now(); + let formatted_now = now.format("%m-%d-%y - %r").to_string(); + + let body = json!( + { + "jsonrpc": "2.0", + "method": match input.method { + DasMethod::GetAsset => "getAsset", + DasMethod::GetAssetProof => "getAssetProof", + DasMethod::GetAssetsByOwner => "getAssetsByOwner", + DasMethod::GetAssetsByCreator => "getAssetsByCreator", + DasMethod::GetAssetsByAuthority => "getAssetsByAuthority", + DasMethod::GetAssetsbyCreator => "getAssetsbyCreator", + DasMethod::GetAssetsByGroup => "getAssetsByGroup", + DasMethod::SearchAssets => "searchAssets", + DasMethod::GetSignaturesForAsset => "getSignaturesForAsset", + DasMethod::GetTokenAccounts => "getTokenAccounts", + }, + "params": input.params, + "id": input.id.unwrap_or(formatted_now), + } + ); + + let req = ctx + .http + .post(input.url) + .header(CONTENT_TYPE, content_type) + .json(&body); + + let resp = req.send().await?; + + match resp.status() { + StatusCode::OK => { + let response = resp.json().await?; + Ok(Output { response }) + } + code => Err(CommandError::msg(format!("{} {:?}", code, resp))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/error.rs b/crates/cmds-solana/src/error.rs new file mode 100644 index 00000000..c26897bc --- /dev/null +++ b/crates/cmds-solana/src/error.rs @@ -0,0 +1,71 @@ +use std::error::Error as StdError; +use std::result::Result as StdResult; +use thiserror::Error as ThisError; + +pub type BoxedError = Box; + +pub type Result = StdResult; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error(transparent)] + Any(#[from] anyhow::Error), + #[error("{}", flow_lib::solana::verbose_solana_error(.0))] + SolanaClient(#[from] solana_client::client_error::ClientError), + #[error(transparent)] + SolanaProgram(#[from] solana_sdk::program_error::ProgramError), + #[error(transparent)] + Signer(#[from] solana_sdk::signer::SignerError), + #[error(transparent)] + Value(#[from] value::Error), + #[error(transparent)] + Http(#[from] reqwest::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Bundlr(#[from] bundlr_sdk::error::BundlrError), + #[error("solana error: associated token account doesn't exist")] + AssociatedTokenAccountDoesntExist, + #[error("account {0} not found")] + AccountNotFound(solana_sdk::pubkey::Pubkey), + #[error("Invalid account data for {0}")] + InvalidAccountData(solana_sdk::pubkey::Pubkey), + #[error("bundlr isn't available on solana testnet")] + BundlrNotAvailableOnTestnet, + #[error("bundlr api returned an invalid response: {0}")] + BundlrApiInvalidResponse(String), + #[error("failed to register funding tx to bundlr. tx_id={0};")] + BundlrTxRegisterFailed(String), + #[error("can't get mnemonic from phrase")] + CantGetMnemonicFromPhrase, + #[error("mime type not found")] + MimeTypeNotFound, + #[error("failed to get keypair from seed: {0}")] + KeypairFromSeed(String), + #[error("solana error: unsupported recipient address: {0}")] + UnsupportedRecipientAddress(String), + #[error("solana error: recipient address not funded")] + RecipientAddressNotFunded, + #[error("specified account: {0} isn't a token account")] + NotTokenAccount(solana_sdk::pubkey::Pubkey), + #[error("insufficient solana balance, needed={needed}; have={balance};")] + InsufficientSolanaBalance { needed: u64, balance: u64 }, + #[error("failed to snapshot mints: {0}")] + ErrorSnapshottingMints(String), + #[error("failed to fetch mint snapshot")] + FailedToFetchMintSnapshot, + #[error("worker stopped")] + WorkerStopped, + #[error("time-out waiting for signature")] + SignatureTimeout, + #[error("an error occured while running rhai expression: {0}")] + RhaiExecutionError(String), + #[error("value not found in field \"{0}\"")] + ValueNotFound(String), +} + +impl Error { + pub fn custom>(e: E) -> Self { + Error::Any(e.into()) + } +} diff --git a/crates/cmds-solana/src/find_pda.rs b/crates/cmds-solana/src/find_pda.rs new file mode 100644 index 00000000..0043ce63 --- /dev/null +++ b/crates/cmds-solana/src/find_pda.rs @@ -0,0 +1,137 @@ +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct FindPDA; + +const FIND_PDA: &str = "find_pda"; +#[derive(Serialize, Deserialize, Debug)] + +pub struct Input { + #[serde(with = "value::pubkey")] + pub program_id: Pubkey, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::pubkey")] + pub pda: Pubkey, +} + +const PROGRAM_ID: &str = "program_id"; +const SEED_1: &str = "seed_1"; +const SEED_2: &str = "seed_2"; +const SEED_3: &str = "seed_3"; +const SEED_4: &str = "seed_4"; +const SEED_5: &str = "seed_5"; + +const PDA: &str = "pda"; + +#[async_trait] +impl CommandTrait for FindPDA { + fn name(&self) -> Name { + FIND_PDA.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: PROGRAM_ID.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: SEED_1.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: SEED_2.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: SEED_3.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: SEED_4.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }, + CmdInput { + name: SEED_5.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: PDA.into(), + r#type: ValueType::Pubkey, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _: Context, mut inputs: ValueSet) -> Result { + let Input { program_id } = value::from_map(inputs.clone())?; + + let seed_1: Option = inputs.swap_remove(SEED_1); + let seed_1 = match seed_1 { + Some(Value::B32(v)) => v.to_vec(), + Some(Value::String(v)) => v.as_bytes().to_vec(), + _ => vec![], + }; + + let seed_2: Option = inputs.swap_remove(SEED_2); + let seed_2 = match seed_2 { + Some(Value::B32(v)) => v.to_vec(), + Some(Value::String(v)) => v.as_bytes().to_vec(), + _ => vec![], + }; + let seed_3: Option = inputs.swap_remove(SEED_3); + let seed_3 = match seed_3 { + Some(Value::B32(v)) => v.to_vec(), + Some(Value::String(v)) => v.as_bytes().to_vec(), + _ => vec![], + }; + let seed_4: Option = inputs.swap_remove(SEED_4); + let seed_4 = match seed_4 { + Some(Value::B32(v)) => v.to_vec(), + Some(Value::String(v)) => v.as_bytes().to_vec(), + _ => vec![], + }; + let seed_5: Option = inputs.swap_remove(SEED_5); + let seed_5 = match seed_5 { + Some(Value::B32(v)) => v.to_vec(), + Some(Value::String(v)) => v.as_bytes().to_vec(), + _ => vec![], + }; + + let seeds = vec![seed_1, seed_2, seed_3, seed_4, seed_5]; + + let seeds = seeds + .into_iter() + .filter(|s| !s.is_empty()) + .collect::>>(); + + let seeds = seeds.iter().map(|s| &s[..]).collect::>(); + + let seeds = &seeds[..]; + + let pda = Pubkey::find_program_address(seeds, &program_id).0; + + Ok(value::to_map(&Output { pda })?) + } +} + +flow_lib::submit!(CommandDescription::new(FIND_PDA, |_| Ok(Box::new(FindPDA)))); diff --git a/crates/cmds-solana/src/generate_keypair.rs b/crates/cmds-solana/src/generate_keypair.rs new file mode 100644 index 00000000..3727fe90 --- /dev/null +++ b/crates/cmds-solana/src/generate_keypair.rs @@ -0,0 +1,200 @@ +use crate::prelude::*; +use crate::WalletOrPubkey; +use bip39::{Language, Mnemonic, MnemonicType, Seed}; +use solana_sdk::signature::{keypair_from_seed, Keypair}; + +const GENERATE_KEYPAIR: &str = "generate_keypair"; + +const DEFINITION: &str = flow_lib::node_definition!("generate_keypair.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(GENERATE_KEYPAIR)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(GENERATE_KEYPAIR, |_| build())); + +fn random_seed() -> String { + Mnemonic::new(MnemonicType::Words12, Language::English).into_phrase() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(default)] + private_key: Option, + #[serde(default = "random_seed")] + seed: String, + #[serde(default)] + passphrase: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::pubkey")] + pub pubkey: Pubkey, + pub keypair: Wallet, +} + +fn generate_keypair(passphrase: &str, seed: &str) -> crate::Result { + let sanitized = seed.split_whitespace().collect::>().join(" "); + let parse_language_fn = || { + for language in &[ + Language::English, + Language::ChineseSimplified, + Language::ChineseTraditional, + Language::Japanese, + Language::Spanish, + Language::Korean, + Language::French, + Language::Italian, + ] { + if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) { + return Ok(mnemonic); + } + } + Err(crate::Error::CantGetMnemonicFromPhrase) + }; + let mnemonic = parse_language_fn()?; + let seed = Seed::new(&mnemonic, passphrase); + keypair_from_seed(seed.as_bytes()).map_err(|e| crate::Error::KeypairFromSeed(e.to_string())) +} + +async fn run(_: Context, input: Input) -> Result { + let keypair = input + .private_key + .map(|either| match either { + WalletOrPubkey::Wallet(keypair) => Ok(keypair), + WalletOrPubkey::Pubkey(public_key) => Ok(Wallet::Adapter { public_key }), + }) + .unwrap_or_else(|| { + generate_keypair(&input.passphrase, &input.seed) + .map_err(CommandError::from) + .map(Into::into) + })?; + Ok(Output { + pubkey: keypair.pubkey(), + keypair, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + async fn test_no_input() { + let ctx = Context::default(); + build().unwrap().run(ctx, ValueSet::new()).await.unwrap(); + } + + #[tokio::test] + async fn test_no_password() { + let seed_phrase = + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; + let ctx = Context::default(); + build() + .unwrap() + .run( + ctx, + value::map! { + "seed" => Value::String(seed_phrase.to_owned()), + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_private_key_keypair() { + let private_key = + "56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu"; + let input = value::map! { + "private_key" => private_key, + }; + let output = build() + .unwrap() + .run(Context::default(), input) + .await + .unwrap(); + let output = value::from_map::(output).unwrap(); + assert_eq!( + output.keypair.keypair().unwrap().to_base58_string(), + private_key + ); + assert_eq!( + output.pubkey.to_string(), + "GQZRKDqVzM4DXGGMEUNdnBD3CC4TTywh3PwgjYPBm8W9" + ); + } + + #[tokio::test] + async fn test_private_key_pubkey() { + let input = value::map! { + "private_key" => "GQZRKDqVzM4DXGGMEUNdnBD3CC4TTywh3PwgjYPBm8W9", + }; + let output = build() + .unwrap() + .run(Context::default(), input) + .await + .unwrap(); + let output = value::from_map::(output).unwrap(); + assert_eq!( + output.pubkey.to_string(), + "GQZRKDqVzM4DXGGMEUNdnBD3CC4TTywh3PwgjYPBm8W9" + ); + assert!(output.keypair.is_adapter_wallet()); + assert_eq!(output.keypair.pubkey(), output.pubkey); + } + + #[tokio::test] + async fn test_seed_and_pass() { + let seed_phrase = + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; + let passphrase = "Hunter1!"; + + let keypair = generate_keypair(passphrase, seed_phrase).unwrap(); + + let input = value::map! { + "seed" => Value::String(seed_phrase.to_owned()), + "passphrase" => Value::String(passphrase.to_owned()), + }; + let output = build() + .unwrap() + .run(Context::default(), input) + .await + .unwrap(); + let output = value::from_map::(output).unwrap(); + assert_eq!( + output.pubkey.to_string(), + "ESxeViFP4r7THzVx9hJDkhj4HrNGSjJSFRPbGaAb97hN" + ); + assert_eq!( + output.keypair.keypair().unwrap().to_base58_string(), + "3LUpzbebV5SCftt8CPmicbKxNtQhtJegEz4n8s6LBf3b1s4yfjLapgJhbMERhP73xLmWEP2XJ2Rz7Y3TFiYgTpXv" + ); + assert_eq!(output.pubkey, keypair.pubkey()); + assert_eq!(output.keypair, Wallet::Keypair(keypair)); + } + + #[tokio::test] + async fn test_invalid() { + let seed_phrase = + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; + let passphrase = "Hunter1!"; + let private_key = + "4rQanLxTFvdgtLsGirizXejgY5ShoZgvz4wwXi4jnii7XHSyUFJbvAk4ojRiEAHvzK6Qnjq7UyJFNbydeQ"; + let input = value::map! { + "seed" => Value::String(seed_phrase.to_owned()), + "passphrase" => Value::String(passphrase.to_owned()), + "private_key" => Value::String(private_key.to_string()), + }; + let result = build().unwrap().run(Context::default(), input).await; + assert!(result.is_err()); + } +} diff --git a/crates/cmds-solana/src/get_balance.rs b/crates/cmds-solana/src/get_balance.rs new file mode 100644 index 00000000..b4cc92f2 --- /dev/null +++ b/crates/cmds-solana/src/get_balance.rs @@ -0,0 +1,48 @@ +use crate::prelude::*; + +const NAME: &str = "get_balance"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("get_balance.json"); + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pubkey: Pubkey, +} + +#[derive(Serialize, Debug)] +pub struct Output { + balance: u64, +} + +async fn run(ctx: Context, input: Input) -> Result { + let balance = ctx.solana_client.get_balance(&input.pubkey).await?; + Ok(Output { balance }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_valid() { + let cmd = build().unwrap(); + let ctx = Context::default(); + dbg!(ctx.solana_client.url()); + let output = cmd + .run( + ctx, + value::map! { "pubkey" => Pubkey::new_from_array([1;32]) }, + ) + .await + .unwrap(); + dbg!(output); + } +} diff --git a/crates/cmds-solana/src/governance/add_required_signatory.rs b/crates/cmds-solana/src/governance/add_required_signatory.rs new file mode 100644 index 00000000..3fdd8152 --- /dev/null +++ b/crates/cmds-solana/src/governance/add_required_signatory.rs @@ -0,0 +1,103 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "add_required_signatory"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/add_required_signatory.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub governance: Wallet, + #[serde(with = "value::pubkey")] + pub signatory: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn add_required_signatory( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + payer: &Pubkey, + // Args + signatory: &Pubkey, +) -> (Instruction, Pubkey) { + let seeds = [ + b"required-signatory".as_ref(), + governance.as_ref(), + signatory.as_ref(), + ]; + let required_signatory_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("required_signatory_address: {}", required_signatory_address); + + let accounts = vec![ + AccountMeta::new(*governance, true), + AccountMeta::new(required_signatory_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let data = GovernanceInstruction::AddRequiredSignatory { + signatory: *signatory, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, required_signatory_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, required_signatory_address) = add_required_signatory( + &program_id, + &input.governance.pubkey(), + &input.fee_payer.pubkey(), + &input.signatory, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "required_signatory_address" => required_signatory_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/add_signatory.rs b/crates/cmds-solana/src/governance/add_signatory.rs new file mode 100644 index 00000000..15889ff9 --- /dev/null +++ b/crates/cmds-solana/src/governance/add_signatory.rs @@ -0,0 +1,131 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{AddSignatoryAuthority, GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "add_signatory"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/add_signatory.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub signatory: Pubkey, + pub governance_authority: Option, + pub add_signatory_authority: AddSignatoryAuthority, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn add_signatory( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + proposal: &Pubkey, + add_signatory_authority: &AddSignatoryAuthority, + payer: &Pubkey, + // Args + signatory: &Pubkey, +) -> (Instruction, Pubkey) { + let seeds: [&[u8]; 3] = [b"governance", proposal.as_ref(), signatory.as_ref()]; + let signatory_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + let mut accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(signatory_record_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + match add_signatory_authority { + AddSignatoryAuthority::ProposalOwner { + governance_authority, + token_owner_record, + } => { + accounts.push(AccountMeta::new_readonly(*token_owner_record, false)); + //TODO add as signer + accounts.push(AccountMeta::new_readonly(*governance_authority, true)); + } + AddSignatoryAuthority::None => { + let seeds = [ + b"required-signatory".as_ref(), + governance.as_ref(), + signatory.as_ref(), + ]; + let required_signatory = Pubkey::find_program_address(&seeds, program_id).0; + info!("required_signatory: {:?}", required_signatory); + accounts.push(AccountMeta::new_readonly(required_signatory, false)); + } + }; + + let data = GovernanceInstruction::AddSignatory { + signatory: *signatory, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, signatory_record_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, signatory_record_address) = add_signatory( + &program_id, + &input.governance, + &input.proposal, + &input.add_signatory_authority, + &input.fee_payer.pubkey(), + &input.signatory, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: std::iter::once(input.fee_payer) + .chain(input.governance_authority) + .collect(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "signatory_record_address" => signatory_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/cancel_proposal.rs b/crates/cmds-solana/src/governance/cancel_proposal.rs new file mode 100644 index 00000000..58f87f4c --- /dev/null +++ b/crates/cmds-solana/src/governance/cancel_proposal.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "cancel_proposal"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/cancel_proposal.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_owner_record: Pubkey, + pub governance_authority: Wallet, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn cancel_proposal( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + proposal_owner_record: &Pubkey, + governance_authority: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + ]; + + let instruction = GovernanceInstruction::CancelProposal {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = cancel_proposal( + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.proposal_owner_record, + &input.governance_authority.pubkey(), + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/cast_vote.rs b/crates/cmds-solana/src/governance/cast_vote.rs new file mode 100644 index 00000000..d9ff3c34 --- /dev/null +++ b/crates/cmds-solana/src/governance/cast_vote.rs @@ -0,0 +1,147 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{with_realm_config_accounts, GovernanceInstruction, Vote, SPL_GOVERNANCE_ID}; + +const NAME: &str = "cast_vote"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/cast_vote.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_owner_record: Pubkey, + #[serde(with = "value::pubkey")] + pub voter_token_owner_record: Pubkey, + + pub governance_authority: Wallet, + #[serde(with = "value::pubkey")] + pub vote_governing_token_mint: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub voter_weight_record: Option, + #[serde(default, with = "value::pubkey::opt")] + pub max_voter_weight_record: Option, + pub vote: Vote, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn cast_vote( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + proposal_owner_record: &Pubkey, + voter_token_owner_record: &Pubkey, + governance_authority: &Pubkey, + vote_governing_token_mint: &Pubkey, + payer: &Pubkey, + voter_weight_record: Option, + max_voter_weight_record: Option, + // Args + vote: Vote, +) -> (Instruction, Pubkey) { + let seeds = [ + b"governance", + proposal.as_ref(), + voter_token_owner_record.as_ref(), + ]; + let vote_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_owner_record, false), + AccountMeta::new(*voter_token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(vote_record_address, false), + AccountMeta::new_readonly(*vote_governing_token_mint, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + with_realm_config_accounts( + program_id, + &mut accounts, + realm, + voter_weight_record, + max_voter_weight_record, + ); + info!("accounts: {:?}", accounts); + + let data = GovernanceInstruction::CastVote { vote }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, vote_record_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, vote_record_address) = cast_vote( + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.proposal_owner_record, + &input.voter_token_owner_record, + &input.governance_authority.pubkey(), + &input.vote_governing_token_mint, + &input.fee_payer.pubkey(), + input.voter_weight_record, + input.max_voter_weight_record, + input.vote, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "vote_record_address" => vote_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/complete_proposal.rs b/crates/cmds-solana/src/governance/complete_proposal.rs new file mode 100644 index 00000000..b68041c3 --- /dev/null +++ b/crates/cmds-solana/src/governance/complete_proposal.rs @@ -0,0 +1,84 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "complete_proposal"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/complete_proposal.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub complete_proposal_authority: Wallet, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn complete_proposal( + program_id: &Pubkey, + // Accounts + proposal: &Pubkey, + token_owner_record: &Pubkey, + complete_proposal_authority: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*complete_proposal_authority, true), + ]; + + let instruction = GovernanceInstruction::CompleteProposal {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = complete_proposal( + &program_id, + &input.proposal, + &input.token_owner_record, + &input.complete_proposal_authority.pubkey(), + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.complete_proposal_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/create_governance.rs b/crates/cmds-solana/src/governance/create_governance.rs new file mode 100644 index 00000000..6611c1e9 --- /dev/null +++ b/crates/cmds-solana/src/governance/create_governance.rs @@ -0,0 +1,122 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; + +use crate::prelude::*; + +use super::{ + with_realm_config_accounts, GovernanceConfig, GovernanceInstruction, SPL_GOVERNANCE_ID, +}; + +const NAME: &str = "create_governance"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/create_governance.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance_seed: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub create_authority: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub voter_weight_record: Option, + pub config: GovernanceConfig, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn create_governance( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance_seed: &Pubkey, + token_owner_record: &Pubkey, + payer: &Pubkey, + create_authority: &Pubkey, + voter_weight_record: Option, + // Args + config: GovernanceConfig, +) -> (Instruction, Pubkey) { + let seeds = [ + b"account-governance", + realm.as_ref(), + governance_seed.as_ref(), + ]; + let governance_address = Pubkey::find_program_address(&seeds, program_id).0; + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governance_address, false), + AccountMeta::new_readonly(*governance_seed, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(*create_authority, true), + ]; + + with_realm_config_accounts(program_id, &mut accounts, realm, voter_weight_record, None); + + let data = GovernanceInstruction::CreateGovernance { config }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, governance_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, governance_address) = create_governance( + &program_id, + &input.realm, + &input.governance_seed, + &input.token_owner_record, + &input.fee_payer.pubkey(), + &input.create_authority.pubkey(), + input.voter_weight_record, + input.config, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.create_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "governance_address" => governance_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/create_native_treasury.rs b/crates/cmds-solana/src/governance/create_native_treasury.rs new file mode 100644 index 00000000..64b6fc0b --- /dev/null +++ b/crates/cmds-solana/src/governance/create_native_treasury.rs @@ -0,0 +1,88 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "create_native_treasury"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/create_native_treasury.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn create_native_treasury( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + payer: &Pubkey, +) -> (Instruction, Pubkey) { + let seeds = [b"native-treasury", governance.as_ref()]; + let native_treasury_address = Pubkey::find_program_address(&seeds, program_id).0; + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(native_treasury_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let data = GovernanceInstruction::CreateNativeTreasury {}; + + let instructions = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instructions, native_treasury_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, native_treasury_address) = + create_native_treasury(&program_id, &input.governance, &input.fee_payer.pubkey()); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "native_treasury_address" => native_treasury_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/create_proposal.rs b/crates/cmds-solana/src/governance/create_proposal.rs new file mode 100644 index 00000000..643c4fb5 --- /dev/null +++ b/crates/cmds-solana/src/governance/create_proposal.rs @@ -0,0 +1,163 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; + +use crate::prelude::*; + +use super::{with_realm_config_accounts, GovernanceInstruction, VoteType, SPL_GOVERNANCE_ID}; + +const NAME: &str = "create_proposal"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/create_proposal.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_owner_record: Pubkey, + + pub governance_authority: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub voter_weight_record: Option, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + pub name: String, + pub description_link: String, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + pub vote_type: VoteType, + pub use_deny_option: bool, + pub options: Vec, + #[serde(with = "value::pubkey")] + pub proposal_seed: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn create_proposal( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + proposal_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + voter_weight_record: Option, + // Args + realm: &Pubkey, + name: String, + description_link: String, + governing_token_mint: &Pubkey, + vote_type: VoteType, + options: Vec, + use_deny_option: bool, + proposal_seed: &Pubkey, +) -> (Instruction, Pubkey, Pubkey) { + let seeds = [ + b"governance", + governance.as_ref(), + governing_token_mint.as_ref(), + proposal_seed.as_ref(), + ]; + let proposal_address = Pubkey::find_program_address(&seeds, program_id).0; + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(proposal_address, false), + AccountMeta::new(*governance, false), + AccountMeta::new(*proposal_owner_record, false), + AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + with_realm_config_accounts(program_id, &mut accounts, realm, voter_weight_record, None); + + // Deposit is only required when there are more active proposal then the + // configured exempt amount Note: We always pass the account because the + // actual value is not known here without passing Governance account data + let seeds = [ + b"proposal-deposit", + proposal_address.as_ref(), + payer.as_ref(), + ]; + let proposal_deposit_address = Pubkey::find_program_address(&seeds, program_id).0; + accounts.push(AccountMeta::new(proposal_deposit_address, false)); + + let instruction = GovernanceInstruction::CreateProposal { + name, + description_link, + vote_type, + options, + use_deny_option, + proposal_seed: *proposal_seed, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + }; + + (instruction, proposal_address, proposal_deposit_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, proposal_address, proposal_deposit_address) = create_proposal( + &program_id, + &input.governance, + &input.proposal_owner_record, + &input.governance_authority.pubkey(), + &input.fee_payer.pubkey(), + input.voter_weight_record, + &input.realm, + input.name, + input.description_link, + &input.governing_token_mint, + input.vote_type, + input.options, + input.use_deny_option, + &input.proposal_seed, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "proposal_address" => proposal_address, + "proposal_deposit_address" => proposal_deposit_address + + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/create_realm.rs b/crates/cmds-solana/src/governance/create_realm.rs new file mode 100644 index 00000000..f74f7d1b --- /dev/null +++ b/crates/cmds-solana/src/governance/create_realm.rs @@ -0,0 +1,213 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program, sysvar}; +use tracing::info; + +use crate::prelude::*; + +use super::{ + GovernanceInstruction, GoverningTokenConfigAccountArgs, GoverningTokenConfigArgs, + MintMaxVoterWeightSource, RealmConfigArgs, SPL_GOVERNANCE_ID, +}; + +// TEST program id +// https://github.com/solana-labs/solana-program-library/tree/master/governance#2-shared-instance +// const SPL_GOVERNANCE_ID: &str = "GTesTBiEWE32WHXXE2S4XbZvA5CrEc4xs6ZgRe895dP"; + +const NAME: &str = "create_realm"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/create_realm.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm_authority: Pubkey, + #[serde(with = "value::pubkey")] + pub community_token_mint: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub council_token_mint: Option, + pub community_token_config_args: Option, + pub council_token_config_args: Option, + pub name: String, + pub min_weight: u64, + pub max_weight_source: MintMaxVoterWeightSource, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +/// Adds accounts specified by GoverningTokenConfigAccountArgs +/// and returns GoverningTokenConfigArgs +pub fn with_governing_token_config_args( + accounts: &mut Vec, + governing_token_config_args: Option, +) -> GoverningTokenConfigArgs { + let governing_token_config_args = governing_token_config_args.unwrap_or_default(); + + let use_voter_weight_addin = + if let Some(voter_weight_addin) = governing_token_config_args.voter_weight_addin { + accounts.push(AccountMeta::new_readonly(voter_weight_addin, false)); + true + } else { + false + }; + + let use_max_voter_weight_addin = + if let Some(max_voter_weight_addin) = governing_token_config_args.max_voter_weight_addin { + accounts.push(AccountMeta::new_readonly(max_voter_weight_addin, false)); + true + } else { + false + }; + + GoverningTokenConfigArgs { + use_voter_weight_addin, + use_max_voter_weight_addin, + token_type: governing_token_config_args.token_type, + } +} + +pub fn create_realm( + program_id: &Pubkey, + // Accounts + realm_authority: &Pubkey, + community_token_mint: &Pubkey, + payer: &Pubkey, + council_token_mint: Option, + // Accounts Args + community_token_config_args: Option, + council_token_config_args: Option, + // Args + name: String, + min_community_weight_to_create_governance: u64, + community_mint_max_voter_weight_source: MintMaxVoterWeightSource, +) -> (Instruction, Pubkey, Pubkey) { + let seeds = [b"governance", name.as_bytes()]; + let realm_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("realm_address: {:?}", realm_address); + + let seeds = [ + b"governance", + realm_address.as_ref(), + community_token_mint.as_ref(), + ]; + let community_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + info!( + "community_token_holding_address: {:?}", + community_token_holding_address + ); + + let mut accounts = vec![ + AccountMeta::new(realm_address, false), + AccountMeta::new_readonly(*realm_authority, false), + AccountMeta::new_readonly(*community_token_mint, false), + AccountMeta::new(community_token_holding_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ]; + + let use_council_mint = if let Some(council_token_mint) = council_token_mint { + let seeds = [ + b"governance", + realm_address.as_ref(), + council_token_mint.as_ref(), + ]; + let council_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + info!( + "council_token_holding_address: {:?}", + council_token_holding_address + ); + + accounts.push(AccountMeta::new_readonly(council_token_mint, false)); + accounts.push(AccountMeta::new(council_token_holding_address, false)); + true + } else { + false + }; + + let seeds = [b"realm-config", realm_address.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("realm_config_address: {:?}", realm_config_address); + accounts.push(AccountMeta::new(realm_config_address, false)); + + let community_token_config_args = + with_governing_token_config_args(&mut accounts, community_token_config_args); + + let council_token_config_args = + with_governing_token_config_args(&mut accounts, council_token_config_args); + + let instruction = GovernanceInstruction::CreateRealm { + config_args: RealmConfigArgs { + use_council_mint, + min_community_weight_to_create_governance, + community_mint_max_voter_weight_source, + community_token_config_args, + council_token_config_args, + }, + name, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + }; + + (instruction, realm_address, community_token_holding_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, realm, community_token) = create_realm( + &program_id, + &input.realm_authority, + &input.community_token_mint, + &input.fee_payer.pubkey(), + input.council_token_mint, + input.community_token_config_args, + input.council_token_config_args, + input.name, + input.min_weight, + input.max_weight_source, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm" => realm, + "community_token_holding" => community_token + + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/create_token_owner_record.rs b/crates/cmds-solana/src/governance/create_token_owner_record.rs new file mode 100644 index 00000000..69bda9ae --- /dev/null +++ b/crates/cmds-solana/src/governance/create_token_owner_record.rs @@ -0,0 +1,112 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "create_token_owner_record"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/create_token_owner_record.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_owner: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn create_token_owner_record( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_owner: &Pubkey, + governing_token_mint: &Pubkey, + payer: &Pubkey, +) -> (Instruction, Pubkey) { + let seeds = [ + b"governance", + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ]; + let token_owner_record_address = Pubkey::find_program_address(&seeds, program_id).0; + info!( + "token_owner_record_address: {:?}", + token_owner_record_address + ); + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governing_token_owner, false), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let data = GovernanceInstruction::CreateTokenOwnerRecord {}; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + + (instruction, token_owner_record_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, token_owner_record_address) = create_token_owner_record( + &program_id, + &input.realm, + &input.governing_token_owner, + &input.governing_token_mint, + &input.fee_payer.pubkey(), + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "token_owner_record_address" => token_owner_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/deposit_governing_tokens.rs b/crates/cmds-solana/src/governance/deposit_governing_tokens.rs new file mode 100644 index 00000000..15e3aadf --- /dev/null +++ b/crates/cmds-solana/src/governance/deposit_governing_tokens.rs @@ -0,0 +1,145 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "deposit_governing_tokens"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/deposit_governing_tokens.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + pub governing_token_owner: Wallet, + pub governing_token_source_authority: Wallet, + pub amount: u64, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn deposit_governing_tokens( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_source: &Pubkey, + governing_token_owner: &Pubkey, + governing_token_source_authority: &Pubkey, + payer: &Pubkey, + // Args + amount: u64, + governing_token_mint: &Pubkey, +) -> (Instruction, Pubkey, Pubkey, Pubkey) { + let seeds = [ + b"governance", + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ]; + let token_owner_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"governance", realm.as_ref(), governing_token_mint.as_ref()]; + let governing_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governing_token_holding_address, false), + AccountMeta::new(*governing_token_source, false), + AccountMeta::new_readonly(*governing_token_owner, true), + AccountMeta::new_readonly(*governing_token_source_authority, true), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(realm_config_address, false), + ]; + + let data = GovernanceInstruction::DepositGoverningTokens { amount }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + ( + instruction, + realm_config_address, + governing_token_holding_address, + token_owner_record_address, + ) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let governing_token_source = spl_associated_token_account::get_associated_token_address( + &input.governing_token_owner.pubkey(), + &input.governing_token_mint, + ); + info!("governing_token_source: {governing_token_source}"); + + let (ix, realm_config_address, governing_token_holding_address, token_owner_record_address) = + deposit_governing_tokens( + &program_id, + &input.realm, + &governing_token_source, + &input.governing_token_owner.pubkey(), + &input.governing_token_source_authority.pubkey(), + &input.fee_payer.pubkey(), + input.amount, + &input.governing_token_mint, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [ + input.fee_payer, + input.governing_token_owner, + input.governing_token_source_authority, + ] + .into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm_config_address" => realm_config_address, + "governing_token_holding_address" => governing_token_holding_address, + "token_owner_record_address" => token_owner_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/execute_transaction.rs b/crates/cmds-solana/src/governance/execute_transaction.rs new file mode 100644 index 00000000..857c58b9 --- /dev/null +++ b/crates/cmds-solana/src/governance/execute_transaction.rs @@ -0,0 +1,109 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "execute_transaction"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/execute_transaction.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_transaction: Pubkey, + #[serde(with = "value::pubkey")] + pub instruction_program_id: Pubkey, + pub instruction_accounts: Vec, + // TODO workaround for testing + pub additional_signers: Option>, + pub signer_1: Option, + pub signer_2: Option, + pub signer_3: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn execute_transaction( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + proposal: &Pubkey, + proposal_transaction: &Pubkey, + instruction_program_id: &Pubkey, + instruction_accounts: &[AccountMeta], +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_transaction, false), + AccountMeta::new_readonly(*instruction_program_id, false), + ]; + + accounts.extend_from_slice(instruction_accounts); + + let instruction = GovernanceInstruction::ExecuteTransaction {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = execute_transaction( + &program_id, + &input.governance, + &input.proposal, + &input.proposal_transaction, + &input.instruction_program_id, + &input.instruction_accounts, + ); + + // NOTE: this part used either additional_signers or signer_{1,2,3}, + // I changed it to use both + let signers = input + .additional_signers + .into_iter() + .flatten() + .chain(input.signer_1) + .chain(input.signer_2) + .chain(input.signer_3) + .collect(); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers, + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/finalize_vote.rs b/crates/cmds-solana/src/governance/finalize_vote.rs new file mode 100644 index 00000000..baee65e6 --- /dev/null +++ b/crates/cmds-solana/src/governance/finalize_vote.rs @@ -0,0 +1,106 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{with_realm_config_accounts, GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "finalize_vote"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/finalize_vote.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_owner_record: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub max_voter_weight_record: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn finalize_vote( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + proposal_owner_record: &Pubkey, + governing_token_mint: &Pubkey, + max_voter_weight_record: Option, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_owner_record, false), + AccountMeta::new_readonly(*governing_token_mint, false), + ]; + + with_realm_config_accounts( + program_id, + &mut accounts, + realm, + None, + max_voter_weight_record, + ); + + let instruction = GovernanceInstruction::FinalizeVote {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = finalize_vote( + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.proposal_owner_record, + &input.governing_token_mint, + input.max_voter_weight_record, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/insert_transaction.rs b/crates/cmds-solana/src/governance/insert_transaction.rs new file mode 100644 index 00000000..54ee31db --- /dev/null +++ b/crates/cmds-solana/src/governance/insert_transaction.rs @@ -0,0 +1,131 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program, sysvar}; + +use crate::prelude::*; + +use super::{GovernanceInstruction, InstructionData, SPL_GOVERNANCE_ID}; + +const NAME: &str = "insert_transaction"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/insert_transaction.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub governance_authority: Wallet, + pub option_index: u8, + pub index: u16, + pub instructions: Vec, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +#[allow(clippy::too_many_arguments)] +pub fn insert_transaction( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + option_index: u8, + index: u16, + instructions: Vec, +) -> (Instruction, Pubkey) { + let seeds = [ + b"governance", + proposal.as_ref(), + &option_index.to_le_bytes(), + &index.to_le_bytes(), + ]; + let proposal_transaction_address = Pubkey::find_program_address(&seeds, program_id).0; + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(proposal_transaction_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ]; + + let data = GovernanceInstruction::InsertTransaction { + option_index, + index, + legacy: 0, + instructions, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, proposal_transaction_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, proposal_transaction_address) = insert_transaction( + &program_id, + &input.governance, + &input.proposal, + &input.token_owner_record, + &input.governance_authority.pubkey(), + &input.fee_payer.pubkey(), + input.option_index, + input.index, + input + .instructions + .into_iter() + .map(|i| i.into()) + .collect::>(), + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "proposal_transaction_address" => proposal_transaction_address,), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/mod.rs b/crates/cmds-solana/src/governance/mod.rs new file mode 100644 index 00000000..42cd0585 --- /dev/null +++ b/crates/cmds-solana/src/governance/mod.rs @@ -0,0 +1,1101 @@ +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use solana_sdk::{ + clock::UnixTimestamp, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; + +pub mod add_required_signatory; +pub mod add_signatory; +pub mod cancel_proposal; +pub mod cast_vote; +pub mod complete_proposal; +pub mod create_governance; +pub mod create_native_treasury; +pub mod create_proposal; +pub mod create_realm; +pub mod create_token_owner_record; +pub mod deposit_governing_tokens; +pub mod execute_transaction; +pub mod finalize_vote; +pub mod insert_transaction; +pub mod refund_proposal_deposit; +pub mod relinquish_token_owner_record_locks; +pub mod relinquish_vote; +pub mod remove_required_signatory; +pub mod remove_transaction; +pub mod revoke_governing_tokens; +pub mod set_governance_config; +pub mod set_governance_delegate; +pub mod set_realm_authority; +pub mod set_realm_config; +pub mod set_token_owner_record_locks; +pub mod sign_off_proposal; +pub mod withdraw_governing_tokens; +// CHAT +pub mod post_message; + +pub const SPL_GOVERNANCE_ID: &str = "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"; +pub const SPL_GOVERNANCE_CHAT_ID: &str = "gCHAtYKrUUktTVzE4hEnZdLV4LXrdBf6Hh9qMaJALET"; + +/// Instructions supported by the GovernanceChat program +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[allow(clippy::large_enum_variant)] +pub enum GovernanceChatInstruction { + /// Posts a message with a comment for a Proposal + /// + /// 0. `[]` Governance program id + /// 1. `[]` Realm account of the Proposal + /// 2. `[]` Governance account the Proposal is for + /// 3. `[]` Proposal account + /// 4. `[]` TokenOwnerRecord account for the message author + /// 5. `[signer]` Governance Authority (TokenOwner or Governance Delegate) + /// 6. `[writable, signer]` ChatMessage account + /// 7. `[signer]` Payer + /// 8. `[]` System program + /// 9. `[]` ReplyTo Message account (optional) + /// 10. `[]` Optional Voter Weight Record + PostMessage { + #[allow(dead_code)] + /// Message body (text or reaction) + body: MessageBody, + + #[allow(dead_code)] + /// Indicates whether the message is a reply to another message + /// If yes then ReplyTo Message account has to be provided + is_reply: bool, + }, +} + +/// Chat message body +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum MessageBody { + /// Text message encoded as utf-8 string + Text(String), + + /// Emoticon encoded using utf-8 characters + /// In the UI reactions are displayed together under the parent message (as + /// opposed to hierarchical replies) + Reaction(String), +} + +/// Instructions supported by the Governance program +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[allow(clippy::large_enum_variant)] +pub enum GovernanceInstruction { + /// Creates Governance Realm account which aggregates governances for given + CreateRealm { + #[allow(dead_code)] + /// UTF-8 encoded Governance Realm name + name: String, + + #[allow(dead_code)] + /// Realm config args + config_args: RealmConfigArgs, + }, + + /// Deposits governing tokens (Community or Council) to Governance Realm and + /// establishes your voter weight to be used for voting within the Realm + /// Note: If subsequent (top up) deposit is made and there are active votes + /// for the Voter then the vote weights won't be updated automatically + /// It can be done by relinquishing votes on active Proposals and voting + /// again with the new weight + DepositGoverningTokens { + /// The amount to deposit into the realm + #[allow(dead_code)] + amount: u64, + }, + + /// Withdraws governing tokens (Community or Council) from Governance Realm + /// and downgrades your voter weight within the Realm. + /// Note: It's only possible to withdraw tokens if the Voter doesn't have + /// any outstanding active votes. + /// If there are any outstanding votes then they must be relinquished + /// before tokens could be withdrawn + WithdrawGoverningTokens {}, + + /// Sets Governance Delegate for the given Realm and Governing Token Mint + /// (Community or Council). The Delegate would have voting rights and + /// could vote on behalf of the Governing Token Owner. The Delegate would + /// also be able to create Proposals on behalf of the Governing Token + /// Owner. + /// Note: This doesn't take voting rights from the Token Owner who still can + /// vote and change governance_delegate + SetGovernanceDelegate { + #[allow(dead_code)] + /// New Governance Delegate + new_governance_delegate: Option, + }, + + /// Creates Governance account which can be used to govern any arbitrary + /// Solana account or asset + CreateGovernance { + /// Governance config + #[allow(dead_code)] + config: GovernanceConfig, + }, + + /// Legacy CreateProgramGovernance instruction + /// Exists for backwards-compatibility + Legacy4, + + /// Creates Proposal account for Transactions which will be executed at some + /// point in the future + /// + /// 0. `[]` Realm account the created Proposal belongs to + /// 1. `[writable]` Proposal account. + /// * PDA seeds ['governance',governance, governing_token_mint, proposal_seed] + /// 2. `[writable]` Governance account + /// 3. `[writable]` TokenOwnerRecord account of the Proposal owner + /// 4. `[]` Governing Token Mint the Proposal is created for + /// 5. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 6. `[signer]` Payer + /// 7. `[]` System program + /// 8. `[]` RealmConfig account. + /// * PDA seeds: ['realm-config', realm] + /// 9. `[]` Optional Voter Weight Record + /// 10. `[writable]` Optional ProposalDeposit account. + /// * PDA seeds: ['proposal-deposit', proposal, deposit payer] + /// Proposal deposit is required when there are more active proposals + /// than the configured deposit exempt amount. + /// The deposit is paid by the Payer of the transaction and can be + /// reclaimed using RefundProposalDeposit once the Proposal is no + /// longer active. + CreateProposal { + #[allow(dead_code)] + /// UTF-8 encoded name of the proposal + name: String, + + #[allow(dead_code)] + /// Link to a gist explaining the proposal + description_link: String, + + #[allow(dead_code)] + /// Proposal vote type + vote_type: VoteType, + + #[allow(dead_code)] + /// Proposal options + options: Vec, + + #[allow(dead_code)] + /// Indicates whether the proposal has the deny option + /// A proposal without the rejecting option is a non binding survey + /// Only proposals with the rejecting option can have executable + /// transactions + use_deny_option: bool, + + #[allow(dead_code)] + /// Unique seed for the Proposal PDA + proposal_seed: Pubkey, + }, + + /// Adds a signatory to the Proposal which means this Proposal can't leave + /// Draft state until yet another Signatory signs + /// + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account associated with the governance + /// 2. `[writable]` Signatory Record Account + /// 3. `[signer]` Payer + /// 4. `[]` System program + /// + /// Either: + /// - 5. `[]` TokenOwnerRecord account of the Proposal owner + /// 6. `[signer]` Governance Authority (Token Owner or Governance Delegate) + /// + /// - 5. `[]` RequiredSignatory account associated with the governance. + AddSignatory { + #[allow(dead_code)] + /// Signatory to add to the Proposal + signatory: Pubkey, + }, + + /// Formerly RemoveSignatory. Exists for backwards-compatibility. + Legacy1, + + /// Inserts Transaction with a set of instructions for the Proposal at the + /// given index position New Transaction must be inserted at the end of + /// the range indicated by Proposal transactions_next_index + /// If a Transaction replaces an existing Transaction at a given index then + /// the old one must be removed using RemoveTransaction first + + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[]` TokenOwnerRecord account of the Proposal owner + /// 3. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 4. `[writable]` ProposalTransaction, account. + /// * PDA seeds: ['governance', proposal, option_index, index] + /// 5. `[signer]` Payer + /// 6. `[]` System program + /// 7. `[]` Rent sysvar + InsertTransaction { + #[allow(dead_code)] + /// The index of the option the transaction is for + option_index: u8, + #[allow(dead_code)] + /// Transaction index to be inserted at. + index: u16, + #[allow(dead_code)] + /// Legacy hold_up_time + legacy: u32, + + #[allow(dead_code)] + /// Instructions Data + instructions: Vec, + }, + + /// Removes Transaction from the Proposal + /// + /// 0. `[writable]` Proposal account + /// 1. `[]` TokenOwnerRecord account of the Proposal owner + /// 2. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 3. `[writable]` ProposalTransaction, account + /// 4. `[writable]` Beneficiary Account which would receive lamports from + /// the disposed ProposalTransaction account + RemoveTransaction, + + /// Cancels Proposal by changing its state to Canceled + /// + /// 0. `[]` Realm account + /// 1. `[writable]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[writable]` TokenOwnerRecord account of the Proposal owner + /// 4. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + CancelProposal, + + /// Signs off Proposal indicating the Signatory approves the Proposal + /// When the last Signatory signs off the Proposal it enters Voting state + /// Note: Adding signatories to a Proposal is a quality and not a security + /// gate and it's entirely at the discretion of the Proposal owner + /// If Proposal owner doesn't designate any signatories then can sign off + /// the Proposal themself + /// + /// 0. `[]` Realm account + /// 1. `[]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[signer]` Signatory account signing off the Proposal Or Proposal + /// owner if the owner hasn't appointed any signatories + /// 4. `[]` TokenOwnerRecord for the Proposal owner, required when the + /// owner signs off the Proposal Or `[writable]` SignatoryRecord + /// account, required when non owner sings off the Proposal + SignOffProposal, + + /// Uses your voter weight (deposited Community or Council tokens) to cast + /// a vote on a Proposal By doing so you indicate you approve or + /// disapprove of running the Proposal set of transactions If you tip + /// the consensus then the transactions can begin to be run after their hold + /// up time + /// + /// 0. `[]` Realm account + /// 1. `[writable]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[writable]` TokenOwnerRecord of the Proposal owner + /// 4. `[writable]` TokenOwnerRecord of the voter. + /// * PDA seeds: ['governance',realm, vote_governing_token_mint, governing_token_owner] + /// 5. `[signer]` Governance Authority (Token Owner or Governance Delegate) + /// 6. `[writable]` Proposal VoteRecord account. + /// * PDA seeds: ['governance',proposal,token_owner_record] + /// 7. `[]` The Governing Token Mint which is used to cast the vote + /// (vote_governing_token_mint). + /// The voting token mint is the governing_token_mint of the Proposal + /// for Approve, Deny and Abstain votes. + /// For Veto vote the voting token mint is the mint of the opposite + /// voting population Council mint to veto Community proposals and + /// Community mint to veto Council proposals. + /// Note: In the current version only Council veto is supported + /// 8. `[signer]` Payer + /// 9. `[]` System program + /// 10. `[]` RealmConfig account. + /// * PDA seeds: ['realm-config', realm] + /// 11. `[]` Optional Voter Weight Record + /// 12. `[]` Optional Max Voter Weight Record + CastVote { + #[allow(dead_code)] + /// User's vote + vote: Vote, + }, + + /// Finalizes vote in case the Vote was not automatically tipped within + /// max_voting_time period + /// + /// 0. `[]` Realm account + /// 1. `[writable]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[writable]` TokenOwnerRecord of the Proposal owner + /// 4. `[]` Governing Token Mint + /// 5. `[]` RealmConfig account. + /// * PDA seeds: ['realm-config', realm] + /// 6. `[]` Optional Max Voter Weight Record + FinalizeVote {}, + + /// Relinquish Vote removes voter weight from a Proposal and removes it + /// from voter's active votes. If the Proposal is still being voted on + /// then the voter's weight won't count towards the vote outcome. If the + /// Proposal is already in decided state then the instruction has no impact + /// on the Proposal and only allows voters to prune their outstanding + /// votes in case they wanted to withdraw Governing tokens from the Realm + /// + /// 0. `[]` Realm account + /// 1. `[]` Governance account + /// 2. `[writable]` Proposal account + /// 3. `[writable]` TokenOwnerRecord account. + /// * PDA seeds: ['governance',realm, vote_governing_token_mint, governing_token_owner] + /// 4. `[writable]` Proposal VoteRecord account. + /// * PDA seeds: ['governance', proposal, token_owner_record] + /// 5. `[]` The Governing Token Mint which was used to cast the vote + /// (vote_governing_token_mint) + /// 6. `[signer]` Optional Governance Authority (Token Owner or Governance + /// Delegate) It's required only when Proposal is still being voted on + /// 7. `[writable]` Optional Beneficiary account which would receive + /// lamports when VoteRecord Account is disposed It's required only + /// when Proposal is still being voted on + RelinquishVote, + + /// Executes a Transaction in the Proposal + /// Anybody can execute transaction once Proposal has been voted Yes and + /// transaction_hold_up time has passed The actual transaction being + /// executed will be signed by Governance PDA the Proposal belongs to + /// For example to execute Program upgrade the ProgramGovernance PDA would + /// be used as the signer + /// + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[writable]` ProposalTransaction account you wish to execute + /// 3. Any extra accounts that are part of the transaction, in order + ExecuteTransaction, + + /// Legacy CreateMintGovernance instruction + /// Exists for backwards-compatibility + Legacy2, + + /// Legacy CreateTokenGovernance instruction + /// Exists for backwards-compatibility + Legacy3, + + /// Sets GovernanceConfig for a Governance + /// + /// 0. `[]` Realm account the Governance account belongs to + /// 1. `[writable, signer]` The Governance account the config is for + SetGovernanceConfig { + #[allow(dead_code)] + /// New governance config + config: GovernanceConfig, + }, + + /// Legacy FlagTransactionError instruction + /// Exists for backwards-compatibility + Legacy5, + + /// Sets new Realm authority + /// + /// 0. `[writable]` Realm account + /// 1. `[signer]` Current Realm authority + /// 2. `[]` New realm authority. Must be one of the realm governances when + /// set + SetRealmAuthority { + #[allow(dead_code)] + /// Set action ( SetUnchecked, SetChecked, Remove) + action: SetRealmAuthorityAction, + }, + + /// Sets realm config + /// 0. `[writable]` Realm account + /// 1. `[signer]` Realm authority + /// 2. `[]` Council Token Mint - optional + /// Note: In the current version it's only possible to remove council + /// mint (set it to None). + /// After setting council to None it won't be possible to withdraw the + /// tokens from the Realm any longer. + /// If that's required then it must be done before executing this + /// instruction. + /// 3. `[writable]` Council Token Holding account - optional unless + /// council is used. + /// * PDA seeds: ['governance',realm,council_mint] The account will be + /// created with the Realm PDA as its owner + /// 4. `[]` System + /// 5. `[writable]` RealmConfig account. + /// * PDA seeds: ['realm-config', realm] + /// 6. `[]` Optional Community Voter Weight Addin Program Id + /// 7. `[]` Optional Max Community Voter Weight Addin Program Id + /// 8. `[]` Optional Council Voter Weight Addin Program Id + /// 9. `[]` Optional Max Council Voter Weight Addin Program Id + /// 10. `[signer]` Optional Payer. Required if RealmConfig doesn't exist + /// and needs to be created + SetRealmConfig { + #[allow(dead_code)] + /// Realm config args + config_args: RealmConfigArgs, + }, + + /// Creates TokenOwnerRecord with 0 deposit amount + /// It's used to register TokenOwner when voter weight addin is used and the + /// Governance program doesn't take deposits + /// + /// 0. `[]` Realm account + /// 1. `[]` Governing Token Owner account + /// 2. `[writable]` TokenOwnerRecord account. + /// * PDA seeds: ['governance',realm, governing_token_mint, + /// governing_token_owner] + /// 3. `[]` Governing Token Mint + /// 4. `[signer]` Payer + /// 5. `[]` System + CreateTokenOwnerRecord {}, + + /// Updates ProgramMetadata account + /// The instruction dumps information implied by the program's code into a + /// persistent account + /// + /// 0. `[writable]` ProgramMetadata account. + /// * PDA seeds: ['metadata'] + /// 1. `[signer]` Payer + /// 2. `[]` System + UpdateProgramMetadata {}, + + /// Creates native SOL treasury account for a Governance account + /// The account has no data and can be used as a payer for instructions + /// signed by Governance PDAs or as a native SOL treasury + /// + /// 0. `[]` Governance account the treasury account is for + /// 1. `[writable]` NativeTreasury account. + /// * PDA seeds: ['native-treasury', governance] + /// 2. `[signer]` Payer + /// 3. `[]` System + CreateNativeTreasury, + + /// Revokes (burns) membership governing tokens for the given + /// TokenOwnerRecord and hence takes away governance power from the + /// TokenOwner. Note: If there are active votes for the TokenOwner then + /// the vote weights won't be updated automatically + /// + /// 0. `[]` Realm account + /// 1. `[writable]` Governing Token Holding account. + /// * PDA seeds: ['governance',realm, governing_token_mint] + /// 2. `[writable]` TokenOwnerRecord account. + /// * PDA seeds: ['governance',realm, governing_token_mint, + /// governing_token_owner] + /// 3. `[writable]` GoverningTokenMint + /// 4. `[signer]` Revoke authority which can be either of: + /// 1) GoverningTokenMint mint_authority to forcefully revoke + /// the membership tokens + /// 2) GoverningTokenOwner who voluntarily revokes their own + /// membership + /// 5. `[]` RealmConfig account. + /// * PDA seeds: ['realm-config', realm] + /// 6. `[]` SPL Token program + RevokeGoverningTokens { + /// The amount to revoke + #[allow(dead_code)] + amount: u64, + }, + + /// Refunds ProposalDeposit once the given proposal is no longer active + /// (Draft, SigningOff, Voting) Once the condition is met the + /// instruction is permissionless and returns the deposit amount to the + /// deposit payer + /// + /// 0. `[]` Proposal account + /// 1. `[writable]` ProposalDeposit account. + /// * PDA seeds: ['proposal-deposit', proposal, deposit payer] + /// 2. `[writable]` Proposal deposit payer (beneficiary) account + RefundProposalDeposit {}, + + /// Transitions an off-chain or manually executable Proposal from Succeeded + /// into Completed state + /// + /// Upon a successful vote on an off-chain or manually executable proposal + /// it remains in Succeeded state Once the external actions are executed + /// the Proposal owner can use the instruction to manually transition it to + /// Completed state + /// + /// + /// 0. `[writable]` Proposal account + /// 1. `[]` TokenOwnerRecord account of the Proposal owner + /// 2. `[signer]` CompleteProposal authority (Token Owner or Delegate) + CompleteProposal {}, + + /// Adds a required signatory to the Governance, which will be applied to + /// all proposals created with it + /// + /// 0. `[writable, signer]` The Governance account the config is for + /// 1. `[writable]` RequiredSignatory Account + /// 2. `[signer]` Payer + /// 3. `[]` System program + AddRequiredSignatory { + #[allow(dead_code)] + /// Required signatory to add to the Governance + signatory: Pubkey, + }, + + /// Removes a required signatory from the Governance + /// + /// 0. `[writable, signer]` The Governance account the config is for + /// 1. `[writable]` RequiredSignatory Account + /// 2. `[writable]` Beneficiary Account which would receive lamports from + /// the disposed RequiredSignatory Account + RemoveRequiredSignatory, + + /// Sets TokenOwnerRecord lock for the given authority and lock id + /// + /// 0. `[]` Realm + /// 1. `[]` RealmConfig + /// 2. `[writable]` TokenOwnerRecord the lock is set for + /// 3. `[signer]` Lock authority issuing the lock + /// 4. `[signer]` Payer + /// 5. `[]` System + SetTokenOwnerRecordLock { + /// Custom lock id which can be used by the authority to issue + /// different locks + #[allow(dead_code)] + lock_id: u8, + + /// The timestamp when the lock expires or None if it never expires + #[allow(dead_code)] + expiry: Option, + }, + + /// Removes all expired TokenOwnerRecord locks and if specified + /// the locks identified by the given lock ids and authority + /// + /// + /// 0. `[]` Realm + /// 1. `[]` RealmConfig + /// 2. `[writable]` TokenOwnerRecord the locks are removed from + /// 3. `[signer]` Optional lock authority which issued the locks specified + /// by lock_ids. If the authority is configured in RealmConfig then it + /// must sign the transaction. If the authority is no longer configured + /// then the locks are removed without the authority signature + RelinquishTokenOwnerRecordLocks { + /// Custom lock ids identifying the lock to remove + /// If the lock_id is None then only expired locks are removed + #[allow(dead_code)] + lock_ids: Option>, + }, + // Sets Realm config item + // Note: + // This instruction is used to set a single RealmConfig item at a time + // In the current version it only supports TokenOwnerRecordLockAuthority + // however eventually all Realm configuration items should be set using + // this instruction and SetRealmConfig instruction should be deprecated + // + // 0. `[writable]` Realm account + // 1. `[writable]` RealmConfig account + // 2. `[signer]` Realm authority + // 3. `[signer]` Payer + // 4. `[]` System + SetRealmConfigItem { + #[allow(dead_code)] + /// Config args + args: SetRealmConfigItemArgs, + }, +} + +/// Realm Config instruction args +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct RealmConfigArgs { + /// Indicates whether council_mint should be used + /// If yes then council_mint account must also be passed to the instruction + pub use_council_mint: bool, + + /// Min number of community tokens required to create a governance + pub min_community_weight_to_create_governance: u64, + + /// The source used for community mint max vote weight source + pub community_mint_max_voter_weight_source: MintMaxVoterWeightSource, + + /// Community token config args + pub community_token_config_args: GoverningTokenConfigArgs, + + /// Council token config args + pub council_token_config_args: GoverningTokenConfigArgs, +} + +/// Realm Config instruction args with account parameters +#[derive( + Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Default, Deserialize, Serialize, +)] +pub struct GoverningTokenConfigAccountArgs { + /// Specifies an external plugin program which should be used to provide + /// voters weights for the given governing token + pub voter_weight_addin: Option, + + /// Specifies an external an external plugin program should be used to + /// provide max voters weight for the given governing token + pub max_voter_weight_addin: Option, + + /// Governing token type defines how the token is used for governance power + pub token_type: GoverningTokenType, +} + +/// Realm Config instruction args +#[derive( + Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Default, Deserialize, Serialize, +)] +pub struct GoverningTokenConfigArgs { + /// Indicates whether an external addin program should be used to provide + /// voters weights If yes then the voters weight program account must be + /// passed to the instruction + pub use_voter_weight_addin: bool, + + /// Indicates whether an external addin program should be used to provide + /// max voters weight for the token If yes then the max voter weight + /// program account must be passed to the instruction + pub use_max_voter_weight_addin: bool, + + /// Governing token type defines how the token is used for governance + pub token_type: GoverningTokenType, +} + +/// The type of the governing token defines: +/// 1) Who retains the authority over deposited tokens +/// 2) Which token instructions Deposit, Withdraw and Revoke (burn) are allowed +#[derive( + Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, Default, +)] +pub enum GoverningTokenType { + /// Liquid token is a token which is fully liquid and the token owner + /// retains full authority over it. + /// Deposit - Yes + /// Withdraw - Yes + /// Revoke - No, Realm authority cannot revoke liquid tokens + #[default] + Liquid, + + /// Membership token is a token controlled by Realm authority + /// Deposit - Yes, membership tokens can be deposited to gain governance + /// power. + /// The membership tokens are conventionally minted into the holding + /// account to keep them out of members possession. + /// Withdraw - No, after membership tokens are deposited they are no longer + /// transferable and can't be withdrawn. + /// Revoke - Yes, Realm authority can Revoke (burn) membership tokens. + Membership, + + /// Dormant token is a token which is only a placeholder and its deposits + /// are not accepted and not used for governance power within the Realm + /// + /// The Dormant token type is used when only a single voting population is + /// operational. For example a Multisig starter DAO uses Council only + /// and sets Community as Dormant to indicate its not utilized for any + /// governance power. Once the starter DAO decides to decentralise then + /// it can change the Community token to Liquid + /// + /// Note: When an external voter weight plugin which takes deposits of the + /// token is used then the type should be set to Dormant to make the + /// intention explicit + /// + /// Deposit - No, dormant tokens can't be deposited into the Realm + /// Withdraw - Yes, tokens can still be withdrawn from Realm to support + /// scenario where the config is changed while some tokens are still + /// deposited. + /// Revoke - No, Realm authority cannot revoke dormant tokens + Dormant, +} + +/// The source of max vote weight used for voting +/// Values below 100% mint supply can be used when the governing token is fully +/// minted but not distributed yet +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)] +pub enum MintMaxVoterWeightSource { + /// Fraction (10^10 precision) of the governing mint supply is used as max + /// vote weight The default is 100% (10^10) to use all available mint + /// supply for voting + SupplyFraction(u64), + + /// Absolute value, irrelevant of the actual mint supply, is used as max + /// voter weight + Absolute(u64), +} + +// /// Governance config +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct GovernanceConfig { + /// The type of the vote threshold used for community vote + /// Note: In the current version only YesVotePercentage and Disabled + /// thresholds are supported + pub community_vote_threshold: VoteThreshold, + + /// Minimum community weight a governance token owner must possess to be + /// able to create a proposal + pub min_community_weight_to_create_proposal: u64, + + /// The wait time in seconds before transactions can be executed after + /// proposal is successfully voted on + pub transactions_hold_up_time: u32, + + /// The base voting time in seconds for proposal to be open for voting + /// Voting is unrestricted during the base voting time and any vote types + /// can be cast The base voting time can be extend by optional cool off + /// time when only negative votes (Veto and Deny) are allowed + pub voting_base_time: u32, + + /// Conditions under which a Community vote will complete early + pub community_vote_tipping: VoteTipping, + + /// The type of the vote threshold used for council vote + /// Note: In the current version only YesVotePercentage and Disabled + /// thresholds are supported + pub council_vote_threshold: VoteThreshold, + + /// The threshold for Council Veto votes + pub council_veto_vote_threshold: VoteThreshold, + + /// Minimum council weight a governance token owner must possess to be able + /// to create a proposal + pub min_council_weight_to_create_proposal: u64, + + /// Conditions under which a Council vote will complete early + pub council_vote_tipping: VoteTipping, + + /// The threshold for Community Veto votes + pub community_veto_vote_threshold: VoteThreshold, + + /// Voting cool of time + pub voting_cool_off_time: u32, + + /// The number of active proposals exempt from the Proposal security deposit + pub deposit_exempt_proposal_count: u8, +} + +/// The type of vote tipping to use on a Proposal. +/// +/// Vote tipping means that under some conditions voting will complete early. +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum VoteTipping { + /// Tip when there is no way for another option to win and the vote + /// threshold has been reached. This ignores voters withdrawing their + /// votes. + /// + /// Currently only supported for the "yes" option in single choice votes. + Strict, + + /// Tip when an option reaches the vote threshold and has more vote weight + /// than any other options. + /// + /// Currently only supported for the "yes" option in single choice votes. + Early, + + /// Never tip the vote early. + Disabled, +} + +/// The type of the vote threshold used to resolve a vote on a Proposal +/// +/// Note: In the current version only YesVotePercentage and Disabled thresholds +/// are supported +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum VoteThreshold { + /// Voting threshold of Yes votes in % required to tip the vote (Approval + /// Quorum) It's the percentage of tokens out of the entire pool of + /// governance tokens eligible to vote Note: If the threshold is below + /// or equal to 50% then an even split of votes ex: 50:50 or 40:40 is always + /// resolved as Defeated In other words a '+1 vote' tie breaker is + /// always required to have a successful vote + YesVotePercentage(u8), + + /// The minimum number of votes in % out of the entire pool of governance + /// tokens eligible to vote which must be cast for the vote to be valid + /// Once the quorum is achieved a simple majority (50%+1) of Yes votes is + /// required for the vote to succeed Note: Quorum is not implemented in + /// the current version + QuorumPercentage(u8), + + /// Disabled vote threshold indicates the given voting population (community + /// or council) is not allowed to vote on proposals for the given + /// Governance + Disabled, + // + // Absolute vote threshold expressed in the voting mint units + // It can be implemented once Solana runtime supports accounts resizing to accommodate u64 + // size extension Alternatively we could use the reserved space if it becomes a priority + // Absolute(u64) + // + // Vote threshold which is always accepted + // It can be used in a setup where the only security gate is proposal creation + // and once created it's automatically approved + // Any +} + +/// Adds realm config account and accounts referenced by the config +/// 1) VoterWeightRecord +/// 2) MaxVoterWeightRecord +pub fn with_realm_config_accounts( + program_id: &Pubkey, + accounts: &mut Vec, + realm: &Pubkey, + voter_weight_record: Option, + max_voter_weight_record: Option, +) { + let seeds = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + accounts.push(AccountMeta::new_readonly(realm_config_address, false)); + + if let Some(voter_weight_record) = voter_weight_record { + accounts.push(AccountMeta::new_readonly(voter_weight_record, false)); + true + } else { + false + }; + + if let Some(max_voter_weight_record) = max_voter_weight_record { + accounts.push(AccountMeta::new_readonly(max_voter_weight_record, false)); + true + } else { + false + }; +} + +/// Proposal vote type +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum VoteType { + /// Single choice vote with mutually exclusive choices + /// In the SingeChoice mode there can ever be a single winner + /// If multiple options score the same highest vote then the Proposal is + /// not resolved and considered as Failed. + /// Note: Yes/No vote is a single choice (Yes) vote with the deny + /// option (No) + SingleChoice, + + /// Multiple options can be selected with up to max_voter_options per voter + /// and with up to max_winning_options of successful options + /// Ex. voters are given 5 options, can choose up to 3 (max_voter_options) + /// and only 1 (max_winning_options) option can win and be executed + MultiChoice { + /// Type of MultiChoice + #[allow(dead_code)] + choice_type: MultiChoiceType, + + /// The min number of options a voter must choose + /// + /// Note: In the current version the limit is not supported and not + /// enforced and must always be set to 1 + #[allow(dead_code)] + min_voter_options: u8, + + /// The max number of options a voter can choose + /// + /// Note: In the current version the limit is not supported and not + /// enforced and must always be set to the number of available + /// options + #[allow(dead_code)] + max_voter_options: u8, + + /// The max number of wining options + /// For executable proposals it limits how many options can be executed + /// for a Proposal + /// + /// Note: In the current version the limit is not supported and not + /// enforced and must always be set to the number of available + /// options + #[allow(dead_code)] + max_winning_options: u8, + }, +} + +/// Type of MultiChoice. +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum MultiChoiceType { + /// Multiple options can be approved with full weight allocated to each + /// approved option + FullWeight, + + /// Multiple options can be approved with weight allocated proportionally + /// to the percentage of the total weight. + /// The full weight has to be voted among the approved options, i.e., + /// 100% of the weight has to be allocated + Weighted, +} + +#[derive(Debug, Copy, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +/// Enum to specify the authority by which the instruction should add a +/// signatory +pub enum AddSignatoryAuthority { + /// Proposal owners can add optional signatories to a proposal + ProposalOwner { + /// Token owner or its delegate + governance_authority: Pubkey, + /// Token owner record of the Proposal owner + token_owner_record: Pubkey, + }, + /// Anyone can add signatories that are required by the governance to a + /// proposal + None, +} + +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +/// Enum to specify the authority by which the instruction should add a +/// signatory +pub enum AddSignatoryAuthoritySPO { + /// Proposal owners can add optional signatories to a proposal + ProposalOwner { + /// Token owner or its delegate + governance_authority: String, + /// Token owner record of the Proposal owner + token_owner_record: String, + }, + /// Anyone can add signatories that are required by the governance to a + /// proposal + None, +} + +impl From for AddSignatoryAuthority { + fn from(authority: AddSignatoryAuthoritySPO) -> Self { + match authority { + AddSignatoryAuthoritySPO::ProposalOwner { + governance_authority, + token_owner_record, + } => AddSignatoryAuthority::ProposalOwner { + governance_authority: Pubkey::from_str(&governance_authority).unwrap(), + token_owner_record: Pubkey::from_str(&token_owner_record).unwrap(), + }, + AddSignatoryAuthoritySPO::None => AddSignatoryAuthority::None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum Vote { + /// Vote approving choices + Approve(Vec), + + /// Vote rejecting proposal + Deny, + + /// Declare indifference to proposal + /// Note: Not supported in the current version + Abstain, + + /// Veto proposal + Veto, +} + +/// Voter choice for a proposal option +/// In the current version only 1) Single choice, 2) Multiple choices proposals +/// and 3) Weighted voting are supported. +/// In the future versions we can add support for 1) Quadratic voting and +/// 2) Ranked choice voting +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct VoteChoice { + /// The rank given to the choice by voter + /// Note: The field is not used in the current version + pub rank: u8, + + /// The voter's weight percentage given by the voter to the choice + pub weight_percentage: u8, +} + +/// InstructionData wrapper. It can be removed once Borsh serialization for +/// Instruction is supported in the SDK +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct InstructionData { + /// Pubkey of the instruction processor that executes this instruction + pub program_id: Pubkey, + /// Metadata for what accounts should be passed to the instruction processor + pub accounts: Vec, + /// Opaque data passed to the instruction processor + pub data: Vec, +} + +/// Account metadata used to define Instructions +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct AccountMetaData { + /// An account's public key + pub pubkey: Pubkey, + /// True if an Instruction requires a Transaction signature matching + /// `pubkey`. + pub is_signer: bool, + /// True if the `pubkey` can be loaded as a read-write account. + pub is_writable: bool, +} + +impl From for InstructionData { + fn from(instruction: Instruction) -> Self { + InstructionData { + program_id: instruction.program_id, + accounts: instruction + .accounts + .iter() + .map(|a| AccountMetaData { + pubkey: a.pubkey, + is_signer: a.is_signer, + is_writable: a.is_writable, + }) + .collect(), + data: instruction.data, + } + } +} + +impl From<&InstructionData> for Instruction { + fn from(instruction: &InstructionData) -> Self { + Instruction { + program_id: instruction.program_id, + accounts: instruction + .accounts + .iter() + .map(|a| AccountMeta { + pubkey: a.pubkey, + is_signer: a.is_signer, + is_writable: a.is_writable, + }) + .collect(), + data: instruction.data.clone(), + } + } +} + +/// SetRealmConfigItem instruction arguments to set a single Realm config item +/// Note: In the current version only TokenOwnerRecordLockAuthority is supported +/// Eventually all Realm config items should be supported for single config item +/// change +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum SetRealmConfigItemArgs { + /// Set TokenOwnerRecord lock authority + TokenOwnerRecordLockAuthority { + /// Action indicating whether to add or remove the lock authority + #[allow(dead_code)] + action: SetConfigItemActionType, + /// Mint of the governing token the lock authority is for + #[allow(dead_code)] + governing_token_mint: Pubkey, + /// Authority to change + #[allow(dead_code)] + authority: Pubkey, + }, +} + +/// Enum describing the action type for setting a config item +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum SetConfigItemActionType { + /// Add config item + Add, + + /// Remove config item + Remove, +} + +/// SetRealmAuthority instruction action +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum SetRealmAuthorityAction { + /// Sets realm authority without any checks + /// Uncheck option allows to set the realm authority to non governance + /// accounts + SetUnchecked, + + /// Sets realm authority and checks the new new authority is one of the + /// realm's governances + // Note: This is not a security feature because governance creation is only + // gated with min_community_weight_to_create_governance. + // The check is done to prevent scenarios where the authority could be + // accidentally set to a wrong or none existing account. + SetChecked, + + /// Removes realm authority + Remove, +} diff --git a/crates/cmds-solana/src/governance/post_message.rs b/crates/cmds-solana/src/governance/post_message.rs new file mode 100644 index 00000000..d010e5a9 --- /dev/null +++ b/crates/cmds-solana/src/governance/post_message.rs @@ -0,0 +1,142 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; + +use crate::prelude::*; + +use super::{ + with_realm_config_accounts, GovernanceChatInstruction, MessageBody, SPL_GOVERNANCE_CHAT_ID, + SPL_GOVERNANCE_ID, +}; + +const NAME: &str = "governance_post_message"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/post_message.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub governance_authority: Wallet, + + pub chat_message: Wallet, + pub body: MessageBody, + #[serde(default, with = "value::pubkey::opt")] + pub reply_to: Option, + #[serde(default, with = "value::pubkey::opt")] + pub voter_weight_record: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn post_message( + program_id: &Pubkey, + // Accounts + governance_program_id: &Pubkey, + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + reply_to: Option, + chat_message: &Pubkey, + payer: &Pubkey, + voter_weight_record: Option, + // Args + body: MessageBody, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new_readonly(*governance_program_id, false), + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governance, false), + AccountMeta::new_readonly(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(*chat_message, true), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let is_reply = if let Some(reply_to) = reply_to { + accounts.push(AccountMeta::new_readonly(reply_to, false)); + true + } else { + false + }; + + with_realm_config_accounts( + governance_program_id, + &mut accounts, + realm, + voter_weight_record, + None, + ); + + let instruction = GovernanceChatInstruction::PostMessage { body, is_reply }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + let chat_program_id = Pubkey::from_str(SPL_GOVERNANCE_CHAT_ID).unwrap(); + + let ix = post_message( + &chat_program_id, + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.token_owner_record, + &input.governance_authority.pubkey(), + input.reply_to, + &input.chat_message.pubkey(), + &input.fee_payer.pubkey(), + input.voter_weight_record, + input.body, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [ + input.fee_payer, + input.governance_authority, + input.chat_message, + ] + .into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/refund_proposal_deposit.rs b/crates/cmds-solana/src/governance/refund_proposal_deposit.rs new file mode 100644 index 00000000..953b4fc9 --- /dev/null +++ b/crates/cmds-solana/src/governance/refund_proposal_deposit.rs @@ -0,0 +1,96 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "refund_proposal_deposit"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/refund_proposal_deposit.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_deposit_payer: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn refund_proposal_deposit( + program_id: &Pubkey, + // Accounts + proposal: &Pubkey, + proposal_deposit_payer: &Pubkey, + // Args +) -> (Instruction, Pubkey) { + let seeds = [ + b"proposal-deposit", + proposal.as_ref(), + proposal_deposit_payer.as_ref(), + ]; + let proposal_deposit_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("Proposal deposit address: {}", proposal_deposit_address); + + let accounts = vec![ + AccountMeta::new_readonly(*proposal, false), + AccountMeta::new(proposal_deposit_address, false), + AccountMeta::new(*proposal_deposit_payer, false), + ]; + + let data = GovernanceInstruction::RefundProposalDeposit {}; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, proposal_deposit_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, proposal_deposit_address) = + refund_proposal_deposit(&program_id, &input.proposal, &input.proposal_deposit_payer); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "proposal_deposit_address" => proposal_deposit_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/relinquish_token_owner_record_locks.rs b/crates/cmds-solana/src/governance/relinquish_token_owner_record_locks.rs new file mode 100644 index 00000000..80de5ded --- /dev/null +++ b/crates/cmds-solana/src/governance/relinquish_token_owner_record_locks.rs @@ -0,0 +1,115 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "relinquish_token_owner_record_locks"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/relinquish_token_owner_record_locks.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub token_owner_record_lock_authority: Option, + pub lock_ids: Option>, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn relinquish_token_owner_record_locks( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + token_owner_record: &Pubkey, + token_owner_record_lock_authority: Option, + // Args + lock_ids: Option>, +) -> (Instruction, Pubkey) { + let seeds: [&[u8]; 2] = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("realm_config_address: {:?}", realm_config_address); + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(realm_config_address, false), + AccountMeta::new(*token_owner_record, false), + ]; + + if let Some(token_owner_record_lock_authority) = token_owner_record_lock_authority { + accounts.push(AccountMeta::new_readonly( + token_owner_record_lock_authority, + true, + )); + } + + let data = GovernanceInstruction::RelinquishTokenOwnerRecordLocks { lock_ids }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, realm_config_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, realm_config_address) = relinquish_token_owner_record_locks( + &program_id, + &input.realm, + &input.token_owner_record, + input + .token_owner_record_lock_authority + .as_ref() + .map(|k| k.pubkey()), + input.lock_ids, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: std::iter::once(input.fee_payer) + .chain(input.token_owner_record_lock_authority) + .collect(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm_config_address" => realm_config_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/relinquish_vote.rs b/crates/cmds-solana/src/governance/relinquish_vote.rs new file mode 100644 index 00000000..15f6502e --- /dev/null +++ b/crates/cmds-solana/src/governance/relinquish_vote.rs @@ -0,0 +1,132 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "relinquish_vote"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/relinquish_vote.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + #[serde(with = "value::pubkey")] + pub vote_governing_token_mint: Pubkey, + pub governance_authority: Option, + #[serde(default, with = "value::pubkey::opt")] + pub beneficiary: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +#[allow(clippy::too_many_arguments)] +pub fn relinquish_vote( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + vote_governing_token_mint: &Pubkey, + governance_authority: Option, + beneficiary: Option, +) -> (Instruction, Pubkey) { + let seeds = [ + b"governance", + proposal.as_ref(), + token_owner_record.as_ref(), + ]; + let vote_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + info!( + "Relinquish Vote: vote_record_address: {}", + vote_record_address + ); + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*token_owner_record, false), + AccountMeta::new(vote_record_address, false), + AccountMeta::new_readonly(*vote_governing_token_mint, false), + ]; + + if let Some(governance_authority) = governance_authority { + accounts.push(AccountMeta::new_readonly(governance_authority, true)); + accounts.push(AccountMeta::new(beneficiary.unwrap(), false)); + } + + let data = GovernanceInstruction::RelinquishVote {}; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + + (instruction, vote_record_address) +} +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, vote_record_address) = relinquish_vote( + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.token_owner_record, + &input.vote_governing_token_mint, + input.governance_authority.as_ref().map(|k| k.pubkey()), + input.beneficiary, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: std::iter::once(input.fee_payer) + .chain(input.governance_authority) + .collect(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "vote_record_address" => vote_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/remove_required_signatory.rs b/crates/cmds-solana/src/governance/remove_required_signatory.rs new file mode 100644 index 00000000..bd8e1cb9 --- /dev/null +++ b/crates/cmds-solana/src/governance/remove_required_signatory.rs @@ -0,0 +1,103 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "remove_required_signatory"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/remove_required_signatory.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + + pub governance: Wallet, + #[serde(with = "value::pubkey")] + pub signatory: Pubkey, + #[serde(with = "value::pubkey")] + pub beneficiary: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn remove_required_signatory( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + signatory: &Pubkey, + beneficiary: &Pubkey, +) -> (Instruction, Pubkey) { + let seeds = [ + b"required-signatory".as_ref(), + governance.as_ref(), + signatory.as_ref(), + ]; + let required_signatory_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("Required signatory address: {}", required_signatory_address); + + let accounts = vec![ + AccountMeta::new(*governance, true), + AccountMeta::new(required_signatory_address, false), + AccountMeta::new(*beneficiary, false), + ]; + + let data = GovernanceInstruction::RemoveRequiredSignatory; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, required_signatory_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, required_signatory_address) = remove_required_signatory( + &program_id, + &input.governance.pubkey(), + &input.signatory, + &input.beneficiary, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "required_signatory_address" => required_signatory_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/remove_transaction.rs b/crates/cmds-solana/src/governance/remove_transaction.rs new file mode 100644 index 00000000..83d5db59 --- /dev/null +++ b/crates/cmds-solana/src/governance/remove_transaction.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "remove_transaction"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/remove_transaction.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal_transaction: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub governance_authority: Wallet, + #[serde(with = "value::pubkey")] + pub beneficiary: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn remove_transaction( + program_id: &Pubkey, + // Accounts + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + proposal_transaction: &Pubkey, + beneficiary: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(*proposal_transaction, false), + AccountMeta::new(*beneficiary, false), + ]; + + let instruction = GovernanceInstruction::RemoveTransaction {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = remove_transaction( + &program_id, + &input.proposal, + &input.token_owner_record, + &input.governance_authority.pubkey(), + &input.proposal_transaction, + &input.beneficiary, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/revoke_governing_tokens.rs b/crates/cmds-solana/src/governance/revoke_governing_tokens.rs new file mode 100644 index 00000000..c29a2809 --- /dev/null +++ b/crates/cmds-solana/src/governance/revoke_governing_tokens.rs @@ -0,0 +1,128 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "revoke_governing_tokens"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/revoke_governing_tokens.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_owner: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + + pub revoke_authority: Wallet, + pub amount: u64, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +#[allow(clippy::too_many_arguments)] +pub fn revoke_governing_tokens( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_owner: &Pubkey, + governing_token_mint: &Pubkey, + revoke_authority: &Pubkey, + // Args + amount: u64, +) -> (Instruction, Pubkey, Pubkey, Pubkey) { + let seeds = [ + b"governance", + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ]; + let token_owner_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"governance", realm.as_ref(), governing_token_mint.as_ref()]; + let governing_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governing_token_holding_address, false), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new(*governing_token_mint, false), + AccountMeta::new_readonly(*revoke_authority, true), + AccountMeta::new_readonly(realm_config_address, false), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + + let data = GovernanceInstruction::RevokeGoverningTokens { amount }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + ( + instruction, + realm_config_address, + governing_token_holding_address, + token_owner_record_address, + ) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, realm_config_address, governing_token_holding_address, token_owner_record_address) = + revoke_governing_tokens( + &program_id, + &input.realm, + &input.governing_token_owner, + &input.governing_token_mint, + &input.revoke_authority.pubkey(), + input.amount, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.revoke_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm_config_address" => realm_config_address, + "governing_token_holding_address" => governing_token_holding_address, + "token_owner_record_address" => token_owner_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/set_governance_config.rs b/crates/cmds-solana/src/governance/set_governance_config.rs new file mode 100644 index 00000000..b4a29542 --- /dev/null +++ b/crates/cmds-solana/src/governance/set_governance_config.rs @@ -0,0 +1,72 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceConfig, GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "set_governance_config"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/set_governance_config.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + + pub governance: Wallet, + pub config: GovernanceConfig, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn set_governance_config( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + // Args + config: GovernanceConfig, +) -> Instruction { + let accounts = vec![AccountMeta::new(*governance, true)]; + + let instruction = GovernanceInstruction::SetGovernanceConfig { config }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = set_governance_config(&program_id, &input.governance.pubkey(), input.config); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/set_governance_delegate.rs b/crates/cmds-solana/src/governance/set_governance_delegate.rs new file mode 100644 index 00000000..4544613b --- /dev/null +++ b/crates/cmds-solana/src/governance/set_governance_delegate.rs @@ -0,0 +1,114 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "set_governance_delegate"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/set_governance_delegate.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + + pub governance_authority: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_owner: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub new_governance_delegate: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} +pub fn set_governance_delegate( + program_id: &Pubkey, + // Accounts + governance_authority: &Pubkey, + // Args + realm: &Pubkey, + governing_token_mint: &Pubkey, + governing_token_owner: &Pubkey, + new_governance_delegate: &Option, +) -> (Instruction, Pubkey) { + let seeds = [ + b"governance", + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ]; + let vote_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + info!("vote_record_address: {:?}", vote_record_address); + + let accounts = vec![ + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(vote_record_address, false), + ]; + + let data = GovernanceInstruction::SetGovernanceDelegate { + new_governance_delegate: *new_governance_delegate, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + + (instruction, vote_record_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, vote_record_address) = set_governance_delegate( + &program_id, + &input.governance_authority.pubkey(), + &input.realm, + &input.governing_token_mint, + &input.governing_token_owner, + &input.new_governance_delegate, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governance_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "vote_record_address" => vote_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/set_realm_authority.rs b/crates/cmds-solana/src/governance/set_realm_authority.rs new file mode 100644 index 00000000..f1791766 --- /dev/null +++ b/crates/cmds-solana/src/governance/set_realm_authority.rs @@ -0,0 +1,97 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SetRealmAuthorityAction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "set_realm_authority"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/set_realm_authority.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + + pub realm_authority: Wallet, + #[serde(with = "value::pubkey::opt")] + pub new_realm_authority: Option, + pub action: SetRealmAuthorityAction, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn set_realm_authority( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + realm_authority: &Pubkey, + new_realm_authority: Option<&Pubkey>, + // Args + action: SetRealmAuthorityAction, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new(*realm, false), + AccountMeta::new_readonly(*realm_authority, true), + ]; + + match action { + SetRealmAuthorityAction::SetChecked | SetRealmAuthorityAction::SetUnchecked => { + accounts.push(AccountMeta::new_readonly( + *new_realm_authority.unwrap(), + false, + )); + } + SetRealmAuthorityAction::Remove => {} + } + + let instruction = GovernanceInstruction::SetRealmAuthority { action }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = set_realm_authority( + &program_id, + &input.realm, + &input.realm_authority.pubkey(), + input.new_realm_authority.as_ref(), + input.action, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.realm_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/set_realm_config.rs b/crates/cmds-solana/src/governance/set_realm_config.rs new file mode 100644 index 00000000..2ab020c2 --- /dev/null +++ b/crates/cmds-solana/src/governance/set_realm_config.rs @@ -0,0 +1,149 @@ +use std::str::FromStr; + +use solana_sdk::{instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::{ + governance::{create_realm::with_governing_token_config_args, RealmConfigArgs}, + prelude::*, +}; + +use super::{ + GovernanceInstruction, GoverningTokenConfigAccountArgs, MintMaxVoterWeightSource, + SPL_GOVERNANCE_ID, +}; + +const NAME: &str = "set_realm_config"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/set_realm_config.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + + pub realm_authority: Wallet, + #[serde(with = "value::pubkey")] + pub community_token_mint: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub council_token_mint: Option, + pub community_token_config_args: Option, + pub council_token_config_args: Option, + pub min_weight: u64, + pub max_weight_source: MintMaxVoterWeightSource, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +#[allow(clippy::too_many_arguments)] +pub fn set_realm_config( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + realm_authority: &Pubkey, + council_token_mint: Option, + payer: &Pubkey, + // Accounts Args + community_token_config_args: Option, + council_token_config_args: Option, + // Args + min_community_weight_to_create_governance: u64, + community_mint_max_voter_weight_source: MintMaxVoterWeightSource, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new(*realm, false), + AccountMeta::new_readonly(*realm_authority, true), + ]; + + let use_council_mint = if let Some(council_token_mint) = council_token_mint { + let seeds = [b"governance", realm.as_ref(), council_token_mint.as_ref()]; + let council_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + info!( + "council_token_holding_address: {:?}", + council_token_holding_address + ); + + accounts.push(AccountMeta::new_readonly(council_token_mint, false)); + accounts.push(AccountMeta::new(council_token_holding_address, false)); + true + } else { + false + }; + + accounts.push(AccountMeta::new_readonly(system_program::id(), false)); + + // Always pass realm_config_address because it's needed when + // use_community_voter_weight_addin is set to true but also when it's set to + // false and the addin is being removed from the realm + let seeds = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("realm_config_address: {:?}", realm_config_address); + accounts.push(AccountMeta::new(realm_config_address, false)); + + let community_token_config_args = + with_governing_token_config_args(&mut accounts, community_token_config_args); + + let council_token_config_args = + with_governing_token_config_args(&mut accounts, council_token_config_args); + + accounts.push(AccountMeta::new(*payer, true)); + + let instruction = GovernanceInstruction::SetRealmConfig { + config_args: RealmConfigArgs { + use_council_mint, + min_community_weight_to_create_governance, + community_mint_max_voter_weight_source, + community_token_config_args, + council_token_config_args, + }, + }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = set_realm_config( + &program_id, + &input.realm, + &input.realm_authority.pubkey(), + input.council_token_mint, + &input.fee_payer.pubkey(), + input.community_token_config_args, + input.council_token_config_args, + input.min_weight, + input.max_weight_source, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.realm_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/set_token_owner_record_locks.rs b/crates/cmds-solana/src/governance/set_token_owner_record_locks.rs new file mode 100644 index 00000000..1324bd24 --- /dev/null +++ b/crates/cmds-solana/src/governance/set_token_owner_record_locks.rs @@ -0,0 +1,111 @@ +use std::str::FromStr; + +use solana_sdk::{clock::UnixTimestamp, instruction::AccountMeta, system_program}; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "set_token_owner_record_locks"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/set_token_owner_record_locks.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner_record: Pubkey, + + pub token_owner_record_lock_authority: Wallet, + pub lock_id: u8, + pub expiry: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn set_token_owner_record_lock( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + token_owner_record: &Pubkey, + token_owner_record_lock_authority: &Pubkey, + payer: &Pubkey, + // Args + lock_id: u8, + expiry: Option, +) -> (Instruction, Pubkey) { + let seeds: [&[u8]; 2] = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("realm_config_address: {:?}", realm_config_address); + + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(realm_config_address, false), + AccountMeta::new(*token_owner_record, false), + AccountMeta::new_readonly(*token_owner_record_lock_authority, true), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let data = GovernanceInstruction::SetTokenOwnerRecordLock { lock_id, expiry }; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + (instruction, realm_config_address) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, realm_config_address) = set_token_owner_record_lock( + &program_id, + &input.realm, + &input.token_owner_record, + &input.token_owner_record_lock_authority.pubkey(), + &input.fee_payer.pubkey(), + input.lock_id, + input.expiry, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.token_owner_record_lock_authority].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm_config_address" => realm_config_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/sign_off_proposal.rs b/crates/cmds-solana/src/governance/sign_off_proposal.rs new file mode 100644 index 00000000..6a9deb7d --- /dev/null +++ b/crates/cmds-solana/src/governance/sign_off_proposal.rs @@ -0,0 +1,103 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; +use tracing::info; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "sign_off_proposal"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/governance/sign_off_proposal.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governance: Pubkey, + #[serde(with = "value::pubkey")] + pub proposal: Pubkey, + + pub signatory: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub proposal_owner_record: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn sign_off_proposal( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + signatory: &Pubkey, + proposal_owner_record: Option<&Pubkey>, +) -> Instruction { + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*signatory, true), + ]; + + if let Some(proposal_owner_record) = proposal_owner_record { + accounts.push(AccountMeta::new_readonly(*proposal_owner_record, false)) + } else { + let seeds = [b"governance", proposal.as_ref(), signatory.as_ref()]; + let signatory_record_address = Pubkey::find_program_address(&seeds, program_id).0; + info!("signatory_record_address: {}", signatory_record_address); + accounts.push(AccountMeta::new(signatory_record_address, false)); + } + + let data = GovernanceInstruction::SignOffProposal; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let ix = sign_off_proposal( + &program_id, + &input.realm, + &input.governance, + &input.proposal, + &input.signatory.pubkey(), + input.proposal_owner_record.as_ref(), + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.signatory].into(), + instructions: [ix].into(), + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/governance/withdraw_governing_tokens.rs b/crates/cmds-solana/src/governance/withdraw_governing_tokens.rs new file mode 100644 index 00000000..7b025b94 --- /dev/null +++ b/crates/cmds-solana/src/governance/withdraw_governing_tokens.rs @@ -0,0 +1,125 @@ +use std::str::FromStr; + +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{GovernanceInstruction, SPL_GOVERNANCE_ID}; + +const NAME: &str = "withdraw_governing_tokens"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = + flow_lib::node_definition!("/governance/withdraw_governing_tokens.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub realm: Pubkey, + #[serde(with = "value::pubkey")] + pub governing_token_destination: Pubkey, + + pub governing_token_owner: Wallet, + #[serde(with = "value::pubkey")] + pub governing_token_mint: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub fn withdraw_governing_tokens( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_destination: &Pubkey, + governing_token_owner: &Pubkey, + // Args + governing_token_mint: &Pubkey, +) -> (Instruction, Pubkey, Pubkey, Pubkey) { + let seeds = [ + b"governance", + realm.as_ref(), + governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ]; + let token_owner_record_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"governance", realm.as_ref(), governing_token_mint.as_ref()]; + let governing_token_holding_address = Pubkey::find_program_address(&seeds, program_id).0; + + let seeds = [b"realm-config", realm.as_ref()]; + let realm_config_address = Pubkey::find_program_address(&seeds, program_id).0; + + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governing_token_holding_address, false), + AccountMeta::new(*governing_token_destination, false), + AccountMeta::new_readonly(*governing_token_owner, true), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(realm_config_address, false), + ]; + + let data = GovernanceInstruction::WithdrawGoverningTokens {}; + + let instruction = Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&data).unwrap(), + }; + ( + instruction, + realm_config_address, + governing_token_holding_address, + token_owner_record_address, + ) +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = Pubkey::from_str(SPL_GOVERNANCE_ID).unwrap(); + + let (ix, realm_config_address, governing_token_holding_address, token_owner_record_address) = + withdraw_governing_tokens( + &program_id, + &input.realm, + &input.governing_token_destination, + &input.governing_token_owner.pubkey(), + &input.governing_token_mint, + ); + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.governing_token_owner].into(), + instructions: [ix].into(), + }; + + let signature = ctx + .execute( + instructions, + value::map!( + "realm_config_address" => realm_config_address, + "governing_token_holding_address" => governing_token_holding_address, + "token_owner_record_address" => token_owner_record_address, + ), + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/jupiter/mod.rs b/crates/cmds-solana/src/jupiter/mod.rs new file mode 100644 index 00000000..2e82136b --- /dev/null +++ b/crates/cmds-solana/src/jupiter/mod.rs @@ -0,0 +1 @@ +pub mod swap; diff --git a/crates/cmds-solana/src/jupiter/swap.rs b/crates/cmds-solana/src/jupiter/swap.rs new file mode 100644 index 00000000..87e9c9bf --- /dev/null +++ b/crates/cmds-solana/src/jupiter/swap.rs @@ -0,0 +1,162 @@ +use crate::prelude::*; + +use jupiter_swap_api_client::{ + quote::{QuoteRequest, SwapMode}, + swap::SwapRequest, + transaction_config::{ComputeUnitPriceMicroLamports, TransactionConfig}, + JupiterSwapApiClient, +}; +use tracing::info; + +const NAME: &str = "jupiter_swap"; + +const DEFINITION: &str = flow_lib::node_definition!("jupiter/swap.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + #[serde(with = "value::pubkey")] + input_mint: Pubkey, + #[serde(with = "value::pubkey")] + output_mint: Pubkey, + #[serde(default = "value::default::bool_true")] + auto_slippage: bool, + slippage_percent: Option, + amount: u64, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + const API_BASE_URL: &str = "https://quote-api.jup.ag/v6"; + const MAX_AUTO_SLIPPAGE_BPS: u16 = 300; + const DEXES: &str = "Whirlpool,Meteora DLMM,Raydium CLMM"; + + info!("Using base url: {}", API_BASE_URL); + info!("Using max auto slippage: {}", MAX_AUTO_SLIPPAGE_BPS); + info!("Using dexes: {}", DEXES); + + let jupiter_swap_api_client = JupiterSwapApiClient::new(API_BASE_URL.into()); + + let mut quote_request = QuoteRequest { + amount: input.amount, + input_mint: input.input_mint, + output_mint: input.output_mint, + dexes: Some(DEXES.into()), + swap_mode: Some(SwapMode::ExactIn), + as_legacy_transaction: Some(true), + restrict_intermediate_tokens: Some(true), + // only_direct_routes: Some(true), + ..QuoteRequest::default() + }; + + if input.auto_slippage { + quote_request.auto_slippage = Some(true); + quote_request.max_auto_slippage_bps = Some(MAX_AUTO_SLIPPAGE_BPS); + quote_request.compute_auto_slippage = true; + } else if let Some(slippage_percent) = input.slippage_percent { + quote_request.auto_slippage = Some(false); + quote_request.slippage_bps = slippage_percent as u16 * 100; + }; + + // GET /quote + let quote_response = jupiter_swap_api_client.quote("e_request).await.unwrap(); + info!("{quote_response:#?}"); + + // POST /swap-instructions + let swap_instructions: jupiter_swap_api_client::swap::SwapInstructionsResponse = + jupiter_swap_api_client + .swap_instructions(&SwapRequest { + user_public_key: input.fee_payer.pubkey(), + quote_response, + config: TransactionConfig { + wrap_and_unwrap_sol: true, + allow_optimized_wrapped_sol_token_account: true, + compute_unit_price_micro_lamports: Some(ComputeUnitPriceMicroLamports::Auto), + dynamic_compute_unit_limit: true, + as_legacy_transaction: true, + use_shared_accounts: true, + ..Default::default() + }, + }) + .await + .map_err(|e| anyhow::anyhow!(format!("Error getting swap instructions: {}", e)))?; + + info!("swap_instructions: {swap_instructions:?}"); + + let mut instructions = Vec::new(); + // instructions.extend(swap_instructions.compute_budget_instructions); + instructions.extend(swap_instructions.setup_instructions); + instructions.push(swap_instructions.swap_instruction); + // instructions.extend(swap_instructions.cleanup_instruction); + + let ins = Instructions { + lookup_tables: Some(swap_instructions.address_lookup_table_addresses), + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: instructions.into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +// !! NOTE: Swap instructions not executable on devnet +// +// #[cfg(test)] +// mod tests { +// use solana_sdk::pubkey; + +// use super::*; + +// #[tokio::test] +// async fn test_build() { +// let ctx = Context::default(); + +// let payer: Wallet = Keypair::from_base58_string("4rQanLxTFvdgtLsGirizXejgYXACawB5ShoZgvz4wwXi4jnii7XHSyUFJbvAk4ojRiEAHvzK6Qnjq7UyJFNbydeQ").into(); + +// const INPUT_MINT: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +// const OUTPUT_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); +// let input = value::map! { +// "fee_payer" => value::to_value(&payer).unwrap(), +// "input_mint" => INPUT_MINT, +// "output_mint" => OUTPUT_MINT, +// "amount" => 10, +// "submit" => true, +// }; +// build().unwrap().run(ctx, input).await.unwrap(); +// } + +// #[test] +// fn test_base64_decode() { +// let base64_str = "AQALGBkDEyVwHzVDiUOUxcGExcXKZZDyXRbt28+XpQBbpElaDgRKZH3R9xSERJ3WqurSlGA/bPQtOB0BqT6Jy6sKbJQQL8MhtzVyNwTTRcIEgSRctdCUhihwfFsqsFqnVwuegC9AcoTLPf2P06zMP6TlpArAEmFWOgt2JOyZrRRrFxRuVKJMFsG3ZY84R6OmOYDLWVFJyrLHjMlASSyEqUCgN7teu/B1KY1rHqjAfyDQA4RQ314Cd6sLsfznEsAbm7Eg8qIdmklnVTL3GrXKI61rcFwE4jsO/6OYROzRbst6fL4CsUt7cYFk34PLGbd9g1wstE9d4kXgAKnwKlDnKM53dDy16+kBCjw6zyWmncodNsdnIA0t2Bghwu8Hbthjg9BCWb147LXiSdQboCwNNzSwAt7ChKByDU4TTkQFXtHpo8p6496l3tIKIz6gt/KurQX9t33ssgsFcgohw2Jdh3k7SljnbLorLZ2ovMn87AV5qWf4sgVkYtGjLWzeY90y/fNh6+rYkFPvGkDQXa5j8bN5jWCQEzEFFVdRaQLh/XC+skAFAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwVKU1D4XciC1hSlVnJ4iilt3x6rq9CmBniISTL07vagBpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAEG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqU9LbA5BCP0qaiR46uCsxZ2yG7Tt9RHBYs9gLR4MCQH7hcSznTxVnguaIJxiZkAnurDmn3MWR0PC2GLghp2KJqGl1cqeBM9dtZC3FLov4yyxWRM/wcGStyJX/QfTnLBAHrQ/+if11/ZKdMCbHylYed5LCas238ndUUsyGqezjOXowjkmLIsm7an7+wTPuDHYXXk/MpA3dy+Cpgqowz8g5xLjRKUuABk5m+8C92Lh7YzAX4z9p72lV5a5uYQar+AGP7s4NL8Zq0BTMCnYuaviMo6ppVUawJB+I6R1qddVS9TfBA0BFgkDZAAAAAAAAAANAAUCfUsAAA4cERIAAwYLChAXDg4VDhQSEwUGCwQMAhEHCAEJDiTBIJszQdacgQcBAAAAGmQAAQDKmjsAAAAAiIR6JgAAAAAeAAAPAMABc29sYW5hLWFjdGlvbjpFNUFkdWRYR1Q3WmV4Y0hydFFxY3I5MW1QeGpURVExVGlKYWpTNTVxcTN3RjpCdFpUZnRCMzhFcXJBRXJYVHVudERBemtkMTJRV29VY2N3YmZOb2s5SmRrajo1a2NTbURFY1RSazlNNkxGRFREOUNVeXc5VkRFd0cxWlNYV1FHS2tIdFltY3RpbjliUkprelFzZkpBOXhFd1NQcXVxaHpXdDFNbzVDZjF2SnFzRVJtZXBw"; + +// let decoded = base64::decode(base64_str).expect("Failed to decode base64"); +// let transaction_size = decoded.len(); + +// println!("Transaction size: {} bytes", transaction_size); +// assert!( +// transaction_size <= 1232, +// "Transaction exceeds Solana's size limit" +// ); +// } diff --git a/crates/cmds-solana/src/lib.rs b/crates/cmds-solana/src/lib.rs new file mode 100644 index 00000000..959f6b7d --- /dev/null +++ b/crates/cmds-solana/src/lib.rs @@ -0,0 +1,101 @@ +#![allow(clippy::too_many_arguments)] + +use flow_lib::solana::Pubkey; +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack}; +use tracing::info; + +pub mod error; + +pub mod associated_token_account; +// pub mod clockwork; +pub mod compression; +pub mod create_mint_account; +pub mod create_token_account; +pub mod find_pda; +pub mod generate_keypair; +pub mod get_balance; +// pub mod metaboss; +pub mod mint_token; +pub mod nft; +// pub mod proxy_authority; +pub mod request_airdrop; +pub mod transfer_sol; +pub mod transfer_token; +pub mod utils; +pub mod wallet; +pub mod wormhole; +// pub mod xnft; +pub mod das; +pub mod governance; +pub mod jupiter; +pub mod memo; +pub mod pyth_price; +pub mod record; +pub mod spl; +pub mod spl_token_2022; +pub mod streamflow; + +pub use error::{Error, Result}; + +pub use flow_lib::solana::WalletOrPubkey; + +pub mod prelude { + pub use crate::utils::{execute, submit_transaction, try_sign_wallet}; + pub use async_trait::async_trait; + pub use flow_lib::{ + command::prelude::*, + solana::{Instructions, KeypairExt, Wallet}, + CmdInputDescription as CmdInput, CmdOutputDescription as CmdOutput, SolanaNet, + }; + pub use solana_client::nonblocking::rpc_client::RpcClient; + pub use solana_sdk::{instruction::Instruction, signer::Signer}; + pub use std::sync::Arc; + pub use value::HashMap; +} + +// make a nodes out of this +pub async fn get_decimals(client: &RpcClient, mint_account: Pubkey) -> crate::Result { + let commitment = CommitmentConfig::confirmed(); + info!("commitment: {:?}", commitment); + + let response = client + .get_account_with_commitment(&mint_account, commitment) + .await + .map_err(|e| { + tracing::error!("Error: {:?}", e); + crate::Error::AccountNotFound(mint_account) + })?; + info!("response: {:?}", response); + + let source_account = match response.value { + Some(account) => account, + None => return Err(crate::Error::AccountNotFound(mint_account)), + }; + + // let source_account = client.get_account(&mint_account).await.map_err(|e| { + // tracing::error!("Error: {:?}", e); + // crate::Error::AccountNotFound(mint_account) + // })?; + let source_account = spl_token::state::Mint::unpack(&source_account.data)?; + info!("source_account: {:?}", source_account); + Ok(source_account.decimals) +} + +#[cfg(test)] +pub mod tests { + use crate::prelude::*; + + #[test] + fn test_name_unique() { + let mut m = ::std::collections::HashSet::new(); + let mut dup = false; + for CommandDescription { name, .. } in inventory::iter::() { + if !m.insert(name) { + println!("Dupicated: {}", name); + dup = true; + } + } + assert!(!dup); + } +} diff --git a/crates/cmds-solana/src/memo.rs b/crates/cmds-solana/src/memo.rs new file mode 100644 index 00000000..b9bd42a0 --- /dev/null +++ b/crates/cmds-solana/src/memo.rs @@ -0,0 +1,47 @@ +use crate::prelude::*; + +const NAME: &str = "memo"; + +const DEFINITION: &str = flow_lib::node_definition!("memo.json"); + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + memo: String, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let instruction = spl_memo::build_memo(input.memo.as_bytes(), &[&input.fee_payer.pubkey()]); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [instruction].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/mint_token.rs b/crates/cmds-solana/src/mint_token.rs new file mode 100644 index 00000000..6fe488cb --- /dev/null +++ b/crates/cmds-solana/src/mint_token.rs @@ -0,0 +1,81 @@ +use crate::{get_decimals, prelude::*, utils::ui_amount_to_amount}; + +use spl_token::instruction::mint_to_checked; + +const SOLANA_MINT_TOKEN: &str = "mint_token"; + +const DEFINITION: &str = flow_lib::node_definition!("mint_token.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(SOLANA_MINT_TOKEN)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(SOLANA_MINT_TOKEN, |_| build())); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + mint_authority: Wallet, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + #[serde(with = "value::pubkey")] + recipient: Pubkey, + #[serde(with = "value::decimal")] + amount: Decimal, + decimals: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let decimals = match input.decimals { + Some(d) => d, + None => get_decimals(&ctx.solana_client, input.mint_account).await?, + }; + + let amount = ui_amount_to_amount(input.amount, decimals)?; + + let instruction = mint_to_checked( + &spl_token::id(), + &input.mint_account, + &input.recipient, + &input.mint_authority.pubkey(), + &[&input.fee_payer.pubkey(), &input.mint_authority.pubkey()], + amount, + decimals, + )?; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.mint_authority].into(), + instructions: [instruction].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/nft/approve_collection_authority.rs b/crates/cmds-solana/src/nft/approve_collection_authority.rs new file mode 100644 index 00000000..9467b53e --- /dev/null +++ b/crates/cmds-solana/src/nft/approve_collection_authority.rs @@ -0,0 +1,94 @@ +use crate::prelude::*; + +const NAME: &str = "approve_collection_authority"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("NFT/approve_collection_authority.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature")?) + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub new_collection_authority: Pubkey, + pub update_authority: Wallet, + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let program_id = mpl_token_metadata::id(); + + let metadata_seeds = &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + program_id.as_ref(), + input.mint_account.as_ref(), + ]; + + let (metadata_pubkey, _) = Pubkey::find_program_address(metadata_seeds, &program_id); + + let (collection_authority_record, _) = + mpl_token_metadata::pda::find_collection_authority_account( + &input.mint_account, + &input.new_collection_authority, + ); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + mpl_token_metadata::state::CollectionAuthorityRecord, + >()) + .await?; + + let instruction = mpl_token_metadata::instruction::approve_collection_authority( + mpl_token_metadata::id(), + collection_authority_record, + input.new_collection_authority, + input.update_authority.pubkey(), + input.fee_payer.pubkey(), + metadata_pubkey, + input.mint_account, + ); + + let instructions = if input.submit { + Instructions { +lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.update_authority].into(), + minimum_balance_for_rent_exemption, + instructions: [instruction].into(), + } + } else { + <_>::default() + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/nft/approve_use_authority.rs b/crates/cmds-solana/src/nft/approve_use_authority.rs new file mode 100644 index 00000000..9522caca --- /dev/null +++ b/crates/cmds-solana/src/nft/approve_use_authority.rs @@ -0,0 +1,211 @@ +use crate::prelude::*; +use solana_program::instruction::Instruction; + +#[derive(Debug, Clone)] +pub struct ApproveUseAuthority; + +impl ApproveUseAuthority { + #[allow(clippy::too_many_arguments)] + async fn command_approve_use_authority( + &self, + rpc_client: &RpcClient, + use_authority_record_pubkey: Pubkey, + user: Pubkey, + owner: Pubkey, + payer: Pubkey, + token_account: Pubkey, + metadata_pubkey: Pubkey, + mint: Pubkey, + burner: Pubkey, + number_of_uses: u64, + ) -> crate::Result<(u64, Vec)> { + let minimum_balance_for_rent_exemption = rpc_client + .get_minimum_balance_for_rent_exemption(std::mem::size_of::< + mpl_token_metadata::state::UseAuthorityRecord, + >()) + .await?; + + let instructions = vec![mpl_token_metadata::instruction::approve_use_authority( + mpl_token_metadata::id(), + use_authority_record_pubkey, + user, + owner, + payer, + token_account, + metadata_pubkey, + mint, + burner, + number_of_uses, + )]; + + Ok((minimum_balance_for_rent_exemption, instructions)) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub use_authority: Pubkey, + pub owner: Wallet, + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub token_account: Pubkey, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub burner: Pubkey, + pub number_of_uses: u64, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +const APPROVE_USE_AUTHORITY: &str = "approve_use_authority"; + +// Inputs +const USE_AUTHORITY: &str = "use_authority"; +const OWNER: &str = "owner"; +const FEE_PAYER: &str = "fee_payer"; +const TOKEN_ACCOUNT: &str = "token_account"; +const MINT_ACCOUNT: &str = "mint_account"; +const BURNER: &str = "burner"; +const NUMBER_OF_USES: &str = "number_of_uses"; +const SUBMIT: &str = "submit"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[async_trait] +impl CommandTrait for ApproveUseAuthority { + fn name(&self) -> Name { + APPROVE_USE_AUTHORITY.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: USE_AUTHORITY.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: OWNER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: FEE_PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: TOKEN_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: BURNER.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: NUMBER_OF_USES.into(), + type_bounds: [ValueType::U64].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: SUBMIT.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: false, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + use_authority, + owner, + fee_payer, + token_account, + mint_account, + burner, + number_of_uses, + submit, + } = value::from_map(inputs)?; + + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&mint_account); + + let (use_authority_record_pubkey, _) = + mpl_token_metadata::pda::find_use_authority_account(&mint_account, &use_authority); + + let (minimum_balance_for_rent_exemption, instructions) = self + .command_approve_use_authority( + &ctx.solana_client, + use_authority_record_pubkey, + use_authority, + owner.pubkey(), + fee_payer.pubkey(), + token_account, + metadata_account, + mint_account, + burner, + number_of_uses, + ) + .await?; + + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &fee_payer.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&owner, &fee_payer], + recent_blockhash, + ) + .await?; + + let signature = if submit { + Some(submit_transaction(&ctx.solana_client, transaction).await?) + } else { + None + }; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(APPROVE_USE_AUTHORITY, |_| Ok( + Box::new(ApproveUseAuthority) +))); diff --git a/crates/cmds-solana/src/nft/arweave_file_upload.rs b/crates/cmds-solana/src/nft/arweave_file_upload.rs new file mode 100644 index 00000000..96d30835 --- /dev/null +++ b/crates/cmds-solana/src/nft/arweave_file_upload.rs @@ -0,0 +1,94 @@ +use super::arweave_nft_upload::Uploader; +use crate::prelude::*; + +#[derive(Debug)] +pub struct ArweaveFileUpload; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub file_path: String, + #[serde(default = "value::default::bool_true")] + pub fund_bundlr: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub file_url: String, +} + +const ARWEAVE_FILE_UPLOAD: &str = "arweave_file_upload"; + +// Inputs +const FEE_PAYER: &str = "fee_payer"; +const FILE_PATH: &str = "file_path"; +const FUND_BUNDLR: &str = "fund_bundlr"; + +// Outputs +const FILE_URL: &str = "file_url"; + +#[async_trait] +impl CommandTrait for ArweaveFileUpload { + fn name(&self) -> Name { + ARWEAVE_FILE_UPLOAD.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: FEE_PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: FILE_PATH.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: FUND_BUNDLR.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: FILE_URL.into(), + r#type: ValueType::String, + optional: false, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + fee_payer, + file_path, + fund_bundlr, + } = value::from_map(inputs)?; + + let mut uploader = Uploader::new( + ctx.solana_client.clone(), + ctx.cfg.solana_client.cluster, + fee_payer, + )?; + + if fund_bundlr { + uploader.lazy_fund(&file_path, &ctx).await?; + } + + let file_url = uploader.upload_file(ctx, &file_path).await?; + + Ok(value::to_map(&Output { file_url })?) + } +} + +flow_lib::submit!(CommandDescription::new(ARWEAVE_FILE_UPLOAD, |_| Ok( + Box::new(ArweaveFileUpload) +))); diff --git a/crates/cmds-solana/src/nft/arweave_nft_upload.rs b/crates/cmds-solana/src/nft/arweave_nft_upload.rs new file mode 100644 index 00000000..1ee659b1 --- /dev/null +++ b/crates/cmds-solana/src/nft/arweave_nft_upload.rs @@ -0,0 +1,406 @@ +use super::NftMetadata; +use crate::prelude::*; +use bundlr_sdk::{error::BundlrError, tags::Tag, Bundlr, Ed25519Signer}; +use flow_lib::solana::SIGNATURE_TIMEOUT; +use std::collections::HashSet; + +pub struct BundlrSigner { + keypair: Wallet, + ctx: Context, +} + +impl BundlrSigner { + pub fn new(keypair: Wallet, ctx: Context) -> Self { + Self { keypair, ctx } + } +} + +impl bundlr_sdk::Signer for BundlrSigner { + const SIG_TYPE: u16 = Ed25519Signer::SIG_TYPE; + const SIG_LENGTH: u16 = Ed25519Signer::SIG_LENGTH; + const PUB_LENGTH: u16 = Ed25519Signer::PUB_LENGTH; + + fn sign(&self, msg: bytes::Bytes) -> Result { + let sig = if let Some(keypair) = self.keypair.keypair() { + keypair.sign_message(&msg) + } else { + let rt = self + .ctx + .get::() + .ok_or_else(|| BundlrError::SigningError("tokio runtime not found".to_owned()))? + .clone(); + let ctx = self.ctx.clone(); + let pubkey = self.keypair.pubkey(); + rt.block_on(async move { + tokio::time::timeout( + SIGNATURE_TIMEOUT, + ctx.request_signature(pubkey, msg, SIGNATURE_TIMEOUT), + ) + .await + .map(|res| res.map(|res| res.signature)) + }) + .map_err(|e| BundlrError::SigningError(e.to_string()))? + .map_err(|e| BundlrError::SigningError(e.to_string()))? + }; + Ok(<[u8; 64]>::from(sig).to_vec().into()) + } + + fn pub_key(&self) -> bytes::Bytes { + self.keypair.pubkey().to_bytes().to_vec().into() + } +} + +#[derive(Debug, Clone)] +pub struct ArweaveNftUpload; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub metadata: NftMetadata, + #[serde(default = "value::default::bool_true")] + pub fund_bundlr: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub metadata_url: String, + pub updated_metadata: NftMetadata, +} + +const ARWEAVE_NFT_UPLOAD: &str = "arweave_nft_upload"; + +// Inputs +const FEE_PAYER: &str = "fee_payer"; +const METADATA: &str = "metadata"; +const FUND_BUNDLR: &str = "fund_bundlr"; + +// Outputs +const METADATA_URL: &str = "metadata_url"; +const UPDATED_METADATA: &str = "updated_metadata"; + +#[async_trait] +impl CommandTrait for ArweaveNftUpload { + fn name(&self) -> Name { + ARWEAVE_NFT_UPLOAD.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: FEE_PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: METADATA.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: FUND_BUNDLR.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [ + CmdOutput { + name: METADATA_URL.into(), + r#type: ValueType::String, + optional: false, + }, + CmdOutput { + name: UPDATED_METADATA.into(), + r#type: ValueType::Free, + optional: false, + }, + ] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + fee_payer, + mut metadata, + fund_bundlr, + } = value::from_map(inputs)?; + + let mut uploader = Uploader::new( + ctx.solana_client.clone(), + ctx.cfg.solana_client.cluster, + fee_payer, + )?; + + if fund_bundlr { + uploader.lazy_fund_metadata(&metadata, &ctx).await?; + } + + metadata.image = uploader.upload_file(ctx.clone(), &metadata.image).await?; + + if let Some(properties) = metadata.properties.as_mut() { + if let Some(files) = properties.files.as_mut() { + for file in files.iter_mut() { + file.uri = uploader.upload_file(ctx.clone(), &file.uri).await?; + } + } + } + + let metadata_url = uploader + .upload( + ctx, + serde_json::to_vec(&metadata).unwrap().into(), + "application/json".to_owned(), + ) + .await?; + + Ok(value::to_map(&Output { + metadata_url, + updated_metadata: metadata, + })?) + } +} + +flow_lib::submit!(CommandDescription::new(ARWEAVE_NFT_UPLOAD, |_| Ok( + Box::new(ArweaveNftUpload) +))); + +pub(crate) struct Uploader { + cache: HashMap, + content_cache: HashMap, + fee_payer: Wallet, + node_url: String, + client: Arc, +} + +impl Uploader { + pub fn new( + client: Arc, + cluster: SolanaNet, + fee_payer: Wallet, + ) -> crate::Result { + // Get Bundlr Network URL + let node_url = match cluster { + SolanaNet::Mainnet => "https://node1.bundlr.network".to_owned(), + SolanaNet::Devnet => "https://devnet.bundlr.network".to_owned(), + SolanaNet::Testnet => return Err(crate::Error::BundlrNotAvailableOnTestnet), + }; + + Ok(Uploader { + cache: HashMap::new(), + content_cache: HashMap::new(), + fee_payer, + node_url, + client, + }) + } + + pub async fn lazy_fund(&mut self, file_path: &str, signer: &Context) -> crate::Result<()> { + let mut needed_size = self.get_file_size(file_path).await?; + needed_size += 10_000; + + let needed_balance = self.get_price(needed_size).await?; + let needed_balance = needed_balance + needed_balance / 10; + + let current_balance = self.get_current_balance().await?; + + if current_balance < needed_balance { + self.fund(needed_balance - current_balance, signer).await?; + } + + Ok(()) + } + + pub async fn lazy_fund_metadata( + &mut self, + metadata: &NftMetadata, + signer: &Context, + ) -> crate::Result<()> { + let mut processed = HashSet::new(); + let mut needed_size = 0; + + let metadata_size = serde_json::to_vec(metadata).unwrap().len() as u64; + + needed_size += metadata_size; + needed_size += self.get_file_size(&metadata.image).await?; + processed.insert(metadata.image.clone()); + + if let Some(properties) = metadata.properties.as_ref() { + if let Some(files) = properties.files.as_ref() { + for file in files.iter() { + if processed.contains(&file.uri) { + continue; + } + + needed_size += self.get_file_size(&file.uri).await?; + processed.insert(file.uri.clone()); + } + } + } + + needed_size += 100_000; // tx_fee + some offset + needed_size += metadata_size * 4 / 10; // metadata change offset + + let needed_balance = self.get_price(needed_size).await?; + let needed_balance = needed_balance + needed_balance / 10; + + let current_balance = self.get_current_balance().await?; + + if current_balance < needed_balance { + self.fund(needed_balance - current_balance, signer).await?; + } + + Ok(()) + } + + async fn get_file(&mut self, path: &str) -> crate::Result { + if let Some(content) = self.content_cache.get(path) { + Ok(content.clone()) + } else { + let resp = reqwest::get(path).await?; + let data = resp.bytes().await?; + self.content_cache.insert(path.to_owned(), data.clone()); + Ok(data) + } + } + + async fn get_file_size(&mut self, path: &str) -> crate::Result { + Ok(self.get_file(path).await?.len() as u64) + } + + async fn get_price(&self, size: u64) -> crate::Result { + let resp = reqwest::get(format!("{}/price/solana/{}", &self.node_url, size)).await?; + let text = resp.text().await?; + text.parse::() + .map_err(|_| crate::Error::BundlrApiInvalidResponse(text.clone())) + } + + async fn get_current_balance(&self) -> crate::Result { + #[serde_with::serde_as] + #[derive(Deserialize)] + struct Resp { + #[serde_as(as = "serde_with::DisplayFromStr")] + balance: u64, + } + + let resp = reqwest::get(format!( + "{}/account/balance/solana/?address={}", + &self.node_url, + self.fee_payer.pubkey() + )) + .await?; + + if resp.status().is_success() { + let resp = resp.json::().await?; + Ok(resp.balance) + } else { + let text = resp.text().await?; + Err(crate::Error::BundlrApiInvalidResponse(text)) + } + } + + async fn fund(&self, amount: u64, signer: &Context) -> crate::Result<()> { + #[derive(Deserialize, Serialize)] + struct Addresses { + solana: String, + } + + #[derive(Deserialize, Serialize)] + struct Info { + addresses: Addresses, + } + + let resp = reqwest::get(format!("{}/info", &self.node_url)).await?; + + let info: Info = serde_json::from_str(&resp.text().await?)?; + + let recipient = info + .addresses + .solana + .parse::() + .map_err(crate::Error::custom)?; + + let instruction = + solana_sdk::system_instruction::transfer(&self.fee_payer.pubkey(), &recipient, amount); + let (mut tx, recent_blockhash) = + execute(&self.client, &self.fee_payer.pubkey(), &[instruction]).await?; + + try_sign_wallet(signer, &mut tx, &self.fee_payer, recent_blockhash).await?; + + let signature = submit_transaction(&self.client, tx).await?; + + let resp = reqwest::Client::new() + .post(format!("{}/account/balance/solana", &self.node_url)) + .json(&serde_json::json!({ + "tx_id": signature.to_string(), + })) + .send() + .await?; + + if !resp.status().is_success() { + return Err(crate::Error::BundlrTxRegisterFailed(signature.to_string())); + } + + Ok(()) + } + + pub async fn upload_file(&mut self, ctx: Context, file_path: &str) -> crate::Result { + if let Some(url) = self.cache.get(file_path) { + return Ok(url.clone()); + } + + let content_type = mime_guess::from_path(file_path) + .first() + .ok_or(crate::Error::MimeTypeNotFound)? + .to_string(); + let data = self.get_file(file_path).await?; + + let url = self.upload(ctx, data, content_type).await?; + + self.cache.insert(file_path.to_owned(), url.clone()); + + Ok(url) + } + + pub async fn upload( + &self, + ctx: Context, + data: bytes::Bytes, + content_type: String, + ) -> crate::Result { + let bundlr = Bundlr::new( + self.node_url.clone(), + "solana".to_string(), + "sol".to_string(), + BundlrSigner::new(self.fee_payer.clone(), ctx), + ); + + let (bundlr, tx) = tokio::task::spawn_blocking(move || { + let tx = bundlr.create_transaction_with_tags( + data.to_vec(), + vec![Tag::new("Content-Type".into(), content_type)], + ); + (bundlr, tx) + }) + .await + .map_err(|_| { + crate::Error::custom(anyhow::anyhow!( + "failed to create and sign bundlr transaction" + )) + })?; + + let resp: BundlrResponse = serde_json::from_value(bundlr.send_transaction(tx).await?)?; + + Ok(format!("https://arweave.net/{}", resp.id)) + } +} + +#[derive(Deserialize)] +struct BundlrResponse { + id: String, +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/add_config_lines_core.rs b/crates/cmds-solana/src/nft/candy_machine_core/add_config_lines_core.rs new file mode 100644 index 00000000..e51c365e --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/add_config_lines_core.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +use super::ConfigLine; +use mpl_core_candy_machine_core::instruction::AddConfigLines; + +// Command Name +const NAME: &str = "add_config_lines_core"; + +const DEFINITION: &str = + flow_lib::node_definition!("nft/candy_machine_core/add_config_lines_core.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + pub authority: Wallet, + pub payer: Wallet, + pub index: u32, + pub config_lines: Vec, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let accounts = mpl_core_candy_machine_core::accounts::AddConfigLines { + candy_machine: input.candy_machine, + authority: input.authority.pubkey(), + } + .to_account_metas(None); + + let data = AddConfigLines { + index: input.index, + config_lines: input.config_lines.into_iter().map(Into::into).collect(), + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.authority].into(), + instructions: [Instruction { + program_id: mpl_core_candy_machine_core::id(), + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_guards.rs b/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_guards.rs new file mode 100644 index 00000000..83af66fd --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_guards.rs @@ -0,0 +1,93 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use mpl_core_candy_guard::instruction::Initialize; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const NAME: &str = "initialize_core_candy_guards"; + +const DEFINITION: &str = + flow_lib::node_definition!("nft/candy_machine_core/initialize_core_candy_guards.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub base: Wallet, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + pub candy_guards: super::CandyGuardData, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let candy_guard_program = mpl_core_candy_guard::ID; + + let base_pubkey = input.base.pubkey(); + + let seeds = &["candy_guard".as_ref(), base_pubkey.as_ref()]; + let candy_guard = Pubkey::find_program_address(seeds, &candy_guard_program).0; + + let accounts = mpl_core_candy_guard::accounts::Initialize { + authority: input.authority, + candy_guard, + base: input.base.pubkey(), + payer: input.payer.pubkey(), + system_program: system_program::ID, + } + .to_account_metas(None); + + // serialize input.candy_guards + let data: mpl_core_candy_guard::state::CandyGuardData = input.candy_guards.into(); + let mut serialized_data = vec![0; data.size()]; + data.save(&mut serialized_data)?; + + let data = Initialize { + data: serialized_data, + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.base].into(), + instructions: [Instruction { + program_id: candy_guard_program, + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "candy_guard" => candy_guard, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_machine.rs b/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_machine.rs new file mode 100644 index 00000000..1f8a2448 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/initialize_core_candy_machine.rs @@ -0,0 +1,116 @@ +use super::CandyMachineData as CandyMachineDataAlias; +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +use mpl_core_candy_machine_core::{instruction::Initialize, CandyMachineData}; + +const NAME: &str = "initialize_candy_machine_core"; + +const DEFINITION: &str = + flow_lib::node_definition!("nft/candy_machine_core/initialize_candy_machine_core.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub candy_machine: Wallet, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint: Pubkey, + pub collection_update_authority: Wallet, + pub candy_machine_data: CandyMachineDataAlias, + // Optional + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let candy_machine_program = mpl_core_candy_machine_core::id(); + let mpl_core_program = mpl_core::ID; + let candy_pubkey = input.candy_machine.pubkey(); + + // Authority PDA + let seeds = &["candy_machine".as_ref(), candy_pubkey.as_ref()]; + let authority_pda = Pubkey::find_program_address(seeds, &candy_machine_program).0; + + let candy_machine_data: CandyMachineData = input.candy_machine_data.into(); + + let accounts = mpl_core_candy_machine_core::accounts::Initialize { + candy_machine: candy_pubkey, + authority_pda, + authority: input.authority, + payer: input.payer.pubkey(), + collection: input.collection_mint, + collection_update_authority: input.collection_update_authority.pubkey(), + system_program: system_program::ID, + sysvar_instructions: solana_program::sysvar::instructions::id(), + mpl_core_program, + } + .to_account_metas(None); + + let data = Initialize { + data: candy_machine_data.clone(), + } + .data(); + + // TODO check size + let candy_account_size = candy_machine_data.get_space_for_candy().unwrap_or(216); + + let lamports = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(candy_account_size) + .await?; + + let create_ix = system_instruction::create_account( + &input.payer.pubkey(), + &candy_pubkey, + lamports, + candy_account_size as u64, + &candy_machine_program, + ); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.candy_machine, + input.collection_update_authority, + ] + .into(), + instructions: [ + create_ix, + Instruction { + program_id: candy_machine_program, + accounts, + data, + }, + ] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/mint_core.rs b/crates/cmds-solana/src/nft/candy_machine_core/mint_core.rs new file mode 100644 index 00000000..354c5614 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/mint_core.rs @@ -0,0 +1,119 @@ +use super::CandyGuardData; +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use mpl_core_candy_guard::instruction::MintV1; +use solana_program::{instruction::Instruction, system_program, sysvar}; +use solana_sdk::{compute_budget::ComputeBudgetInstruction, pubkey::Pubkey}; + +// Command Name +const NAME: &str = "mint_candy_machine_core"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine_core/mint_core.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + pub minter: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub owner: Option, + pub mint_account: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint: Pubkey, + #[serde(with = "value::pubkey")] + pub collection_update_authority: Pubkey, + pub candy_guards: CandyGuardData, + pub group_label: Option, + // Optional + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + static CANDY_MACHINE_PROGRAM_ID: Pubkey = mpl_core_candy_machine_core::ID; + static CANDY_GUARD_PROGRAM_ID: Pubkey = mpl_core_candy_guard::ID; + static MPL_CORE_PROGRAM_ID: Pubkey = mpl_core::ID; + + // Authority PDA + let seeds = &["candy_machine".as_ref(), input.candy_machine.as_ref()]; + let candy_machine_authority_pda = + Pubkey::find_program_address(seeds, &CANDY_MACHINE_PROGRAM_ID).0; + + // Candy Guard PDA + let seeds = &["candy_guard".as_ref(), input.candy_machine.as_ref()]; + let candy_guard = Pubkey::find_program_address(seeds, &CANDY_GUARD_PROGRAM_ID).0; + + let accounts = mpl_core_candy_guard::accounts::MintV1 { + candy_guard, + candy_machine: input.candy_machine, + candy_machine_authority_pda, + payer: input.payer.pubkey(), + minter: input.minter.pubkey(), + owner: input.owner, + asset: input.mint_account.pubkey(), + collection: input.collection_mint, + mpl_core_program: MPL_CORE_PROGRAM_ID, + candy_machine_program: CANDY_MACHINE_PROGRAM_ID, + system_program: system_program::ID, + sysvar_instructions: sysvar::instructions::ID, + recent_slothashes: sysvar::slot_hashes::ID, + } + .to_account_metas(None); + + // serialize input.candy_guards + let data: mpl_core_candy_guard::state::CandyGuardData = input.candy_guards.into(); + let mut serialized_data = vec![0; data.size()]; + data.save(&mut serialized_data)?; + + let data = MintV1 { + mint_args: serialized_data, + label: input.group_label, + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.minter, + // input.mint_account, + ] + .into(), + instructions: [ + ComputeBudgetInstruction::set_compute_unit_limit(1_000_000u32), + Instruction { + program_id: CANDY_GUARD_PROGRAM_ID, + accounts, + data, + }, + ] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/mod.rs b/crates/cmds-solana/src/nft/candy_machine_core/mod.rs new file mode 100644 index 00000000..709c1924 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/mod.rs @@ -0,0 +1,606 @@ +use anchor_lang::error::Error; +use flow_lib::solana::Pubkey; +use mpl_core_candy_guard::guards::err; +use mpl_core_candy_machine_core::{ + constants::{HIDDEN_SECTION, MAX_NAME_LENGTH, MAX_URI_LENGTH}, + errors::CandyError, + replace_patterns, +}; +use serde::{Deserialize, Serialize}; +use struct_convert::Convert; + +pub mod add_config_lines_core; +pub mod initialize_core_candy_guards; +pub mod initialize_core_candy_machine; +pub mod mint_core; +pub mod wrap_core; + +#[derive(Serialize, Deserialize, Clone, Default, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_machine_core::CandyMachineData")] +pub struct CandyMachineData { + /// Number of assets available + pub items_available: u64, + /// Max supply of each individual asset (default 0) + pub max_supply: u64, + /// Indicates if the asset is mutable or not (default yes) + pub is_mutable: bool, + /// Config line settings + pub config_line_settings: Option, + /// Hidden setttings + pub hidden_settings: Option, +} + +/// Hidden settings for large mints used with off-chain data. +#[derive(Serialize, Deserialize, Clone, Default, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_machine_core::HiddenSettings")] +pub struct HiddenSettings { + /// Asset prefix name + pub name: String, + /// Shared URI + pub uri: String, + /// Hash of the hidden settings file + pub hash: [u8; 32], +} + +/// Config line struct for storing asset (NFT) data pre-mint. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_machine_core::ConfigLine")] +pub struct ConfigLine { + /// Name of the asset. + pub name: String, + /// URI to JSON metadata. + pub uri: String, +} + +/// Config line settings to allocate space for individual name + URI. +#[derive(Serialize, Deserialize, Clone, Default, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_machine_core::ConfigLineSettings")] +pub struct ConfigLineSettings { + /// Common name prefix + pub prefix_name: String, + /// Length of the remaining part of the name + pub name_length: u32, + /// Common URI prefix + pub prefix_uri: String, + /// Length of the remaining part of the URI + pub uri_length: u32, + /// Indicates whether to use a senquential index generator or not + pub is_sequential: bool, +} + +impl CandyMachineData { + pub fn get_space_for_candy(&self) -> Result { + Ok(if self.hidden_settings.is_some() { + HIDDEN_SECTION + } else { + HIDDEN_SECTION + + 4 + + (self.items_available as usize) * self.get_config_line_size() + + (self + .items_available + .checked_div(8) + .ok_or(CandyError::NumericalOverflowError)? + + 1) as usize + + (self.items_available as usize) * 4 + }) + } + + pub fn get_config_line_size(&self) -> usize { + if let Some(config_line) = &self.config_line_settings { + (config_line.name_length + config_line.uri_length) as usize + } else { + 0 + } + } + + /// Validates the hidden and config lines settings against the maximum + /// allowed values for name and URI. + /// + /// Hidden settings take precedence over config lines since when hidden + /// settings are used, the account does not need to include space for + /// config lines. + pub fn validate(&self) -> Result<(), Error> { + // validation substitutes any variable for the maximum allowed index + // to check the longest possible name and uri that can result from the + // replacement of the variables + + if let Some(hidden) = &self.hidden_settings { + // config line settings should not be enabled at the same time as hidden settings + if self.config_line_settings.is_some() { + return err!(CandyError::HiddenSettingsDoNotHaveConfigLines); + } + + let expected = replace_patterns(hidden.name.clone(), self.items_available as usize); + if MAX_NAME_LENGTH < expected.len() { + return err!(CandyError::ExceededLengthError); + } + + let expected = replace_patterns(hidden.uri.clone(), self.items_available as usize); + if MAX_URI_LENGTH < expected.len() { + return err!(CandyError::ExceededLengthError); + } + } else if let Some(config_line) = &self.config_line_settings { + let expected = replace_patterns( + config_line.prefix_name.clone(), + self.items_available as usize, + ); + if MAX_NAME_LENGTH < (expected.len() + config_line.name_length as usize) { + return err!(CandyError::ExceededLengthError); + } + + let expected = replace_patterns( + config_line.prefix_uri.clone(), + self.items_available as usize, + ); + if MAX_URI_LENGTH < (expected.len() + config_line.uri_length as usize) { + return err!(CandyError::ExceededLengthError); + } + } else { + return err!(CandyError::MissingConfigLinesSettings); + } + + Ok(()) + } +} + +/// A group represent a specific set of guards. When groups are used, transactions +/// must specify which group should be used during validation. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::state::Group")] +pub struct Group { + pub label: String, + pub guards: GuardSet, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CandyGuardData { + pub default: GuardSet, + pub groups: Option>, +} + +impl From for mpl_core_candy_guard::state::CandyGuardData { + fn from(value: CandyGuardData) -> Self { + Self { + default: value.default.into(), + groups: value + .groups + .map(|vec| vec.into_iter().map(Into::into).collect()), + } + } +} + +/// The set of guards available. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::state::GuardSet")] +pub struct GuardSet { + /// Last instruction check and bot tax (penalty for invalid transactions). + pub bot_tax: Option, + /// Sol payment guard (set the price for the mint in lamports). + pub sol_payment: Option, + /// Token payment guard (set the price for the mint in spl-token amount). + pub token_payment: Option, + /// Start data guard (controls when minting is allowed). + pub start_date: Option, + /// Third party signer guard (requires an extra signer for the transaction). + pub third_party_signer: Option, + /// Token gate guard (restrict access to holders of a specific token). + pub token_gate: Option, + /// Gatekeeper guard (captcha challenge). + pub gatekeeper: Option, + /// End date guard (set an end date to stop the mint). + pub end_date: Option, + /// Allow list guard (curated list of allowed addresses). + pub allow_list: Option, + /// Mint limit guard (add a limit on the number of mints per wallet). + pub mint_limit: Option, + /// NFT Payment (charge an NFT in order to mint). + pub nft_payment: Option, + /// Redeemed amount guard (add a limit on the overall number of items minted). + pub redeemed_amount: Option, + /// Address gate (check access against a specified address). + pub address_gate: Option, + /// NFT gate guard (check access based on holding a specified NFT). + pub nft_gate: Option, + /// NFT burn guard (burn a specified NFT). + pub nft_burn: Option, + /// Token burn guard (burn a specified amount of spl-token). + pub token_burn: Option, + /// Freeze sol payment guard (set the price for the mint in lamports with a freeze period). + pub freeze_sol_payment: Option, + /// Freeze token payment guard (set the price for the mint in spl-token amount with a freeze period). + pub freeze_token_payment: Option, + /// Program gate guard (restricts the programs that can be in a mint transaction). + pub program_gate: Option, + /// Allocation guard (specify the maximum number of mints in a group). + pub allocation: Option, + /// Token2022 payment guard (set the price for the mint in spl-token-2022 amount). + pub token2022_payment: Option, + /// Sol fixed fee for launchpads, marketplaces to define custom fees + pub sol_fixed_fee: Option, + /// NFT mint limit guard (add a limit on the number of mints per NFT). + pub nft_mint_limit: Option, + /// NFT mint limit guard (add a limit on the number of mints per NFT). + pub edition: Option, + /// Asset Payment (charge an Asset in order to mint). + pub asset_payment: Option, + /// Asset Burn (burn an Asset). + pub asset_burn: Option, + /// Asset mint limit guard (add a limit on the number of mints per asset). + pub asset_mint_limit: Option, + /// Asset Burn Multi (multi burn Assets). + pub asset_burn_multi: Option, + /// Asset Payment Multi (multi pay Assets). + pub asset_payment_multi: Option, + /// Asset Gate (restrict access to holders of a specific asset). + pub asset_gate: Option, + /// Vanity Mint (the address of the new asset must match a pattern). + pub vanity_mint: Option, +} + +/// Available guard types. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum GuardType { + BotTax, + SolPayment, + TokenPayment, + StartDate, + ThirdPartySigner, + TokenGate, + Gatekeeper, + EndDate, + AllowList, + MintLimit, + NftPayment, + RedeemedAmount, + AddressGate, + NftGate, + NftBurn, + TokenBurn, + FreezeSolPayment, + FreezeTokenPayment, + ProgramGate, + Allocation, + Token2022Payment, + SolFixedFee, + NftMintLimit, + Edition, + AssetPayment, + AssetBurn, + AssetMintLimit, + AssetBurnMulti, + AssetPaymentMulti, + AssetGate, + VanityMint, +} + +impl GuardType { + pub fn as_mask(guard_type: GuardType) -> u64 { + 0b1u64 << (guard_type as u8) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetPayment")] +pub struct AssetPayment { + pub required_collection: Pubkey, + pub destination: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetBurn")] +pub struct AssetBurn { + pub required_collection: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetMintLimit")] +pub struct AssetMintLimit { + /// Unique identifier of the mint limit. + pub id: u8, + /// Limit of mints per individual mint address. + pub limit: u16, + /// Required collection of the mint. + pub required_collection: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetBurnMulti")] +pub struct AssetBurnMulti { + pub required_collection: Pubkey, + pub num: u8, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetPaymentMulti")] +pub struct AssetPaymentMulti { + pub required_collection: Pubkey, + pub destination: Pubkey, + pub num: u8, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AssetGate")] +pub struct AssetGate { + pub required_collection: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::VanityMint")] +pub struct VanityMint { + pub regex: String, +} + +/// Guard that restricts access to a specific address. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AddressGate")] +pub struct AddressGate { + pub address: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::Allocation")] +pub struct Allocation { + /// Unique identifier of the allocation. + pub id: u8, + /// The limit of the allocation. + pub limit: u32, +} + +/// Guard is used to: +/// * charge a penalty for invalid transactions +/// * validate that the mint transaction is the last transaction +/// * verify that only authorized programs have instructions +/// +/// The `bot_tax` is applied to any error that occurs during the +/// validation of the guards. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::BotTax")] +pub struct BotTax { + pub lamports: u64, + pub last_instruction: bool, +} + +/// Guard that uses a merkle tree to specify the addresses allowed to mint. +/// +/// List of accounts required: +/// +/// 0. `[]` Pda created by the merkle proof instruction (seeds `["allow_list", merke tree root, +/// payer key, candy guard pubkey, candy machine pubkey]`). +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::AllowList")] +pub struct AllowList { + /// Merkle root of the addresses allowed to mint. + pub merkle_root: [u8; 32], +} + +/// Guard that adds an edition plugin to the asset. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::Edition")] +pub struct Edition { + pub edition_start_offset: u32, +} + +/// Guard that sets a specific date for the mint to stop. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::EndDate")] +pub struct EndDate { + pub date: i64, +} + +/// Guard that charges an amount in SOL (lamports) for the mint with a freeze period. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow", +/// destination pubkey, candy guard pubkey, candy machine pubkey]`). +/// 1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token +/// program pubkey, nft mint pubkey]`). +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::FreezeSolPayment")] +pub struct FreezeSolPayment { + pub lamports: u64, + pub destination: Pubkey, +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint with a freeze period. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow", +/// destination_ata pubkey, candy guard pubkey, candy machine pubkey]`). +/// 1. `[writable]` Token account holding the required amount. +/// 2. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA +/// pubkey, token program pubkey, nft mint pubkey]`). +/// 3. `[]` SPL Token program. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::FreezeTokenPayment")] +pub struct FreezeTokenPayment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} + +/// Guard that validates if the payer of the transaction has a token from a specified +/// gateway network — in most cases, a token after completing a captcha challenge. +/// +/// List of accounts required: +/// +/// 0. `[writeable]` Gatekeeper token account. +/// 1. `[]` Gatekeeper program account. +/// 2. `[]` Gatekeeper expire account. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::Gatekeeper")] +pub struct Gatekeeper { + /// The network for the gateway token required + pub gatekeeper_network: Pubkey, + /// Whether or not the token should expire after minting. + /// The gatekeeper network must support this if true. + pub expire_on_use: bool, +} + +/// Guard to set a limit of mints per wallet. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Mint counter PDA. The PDA is derived +/// using the seed `["mint_limit", mint guard id, payer key, +/// candy guard pubkey, candy machine pubkey]`. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::MintLimit")] +pub struct MintLimit { + /// Unique identifier of the mint limit. + pub id: u8, + /// Limit of mints per individual address. + pub limit: u16, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::NftBurn")] +pub struct NftBurn { + pub required_collection: Pubkey, +} + +/// Guard that restricts the transaction to holders of a specified collection. +/// +/// List of accounts required: +/// +/// 0. `[]` Token account of the NFT. +/// 1. `[]` Metadata account of the NFT. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::NftGate")] +pub struct NftGate { + pub required_collection: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::NftPayment")] +pub struct NftPayment { + pub required_collection: Pubkey, + pub destination: Pubkey, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::NftMintLimit")] +pub struct NftMintLimit { + /// Unique identifier of the mint limit. + pub id: u8, + /// Limit of mints per individual mint address. + pub limit: u16, + /// Required collection of the mint. + pub required_collection: Pubkey, +} + +/// Guard that restricts the programs that can be in a mint transaction. The guard allows the +/// necessary programs for the mint and any other program specified in the configuration. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::ProgramGate")] +pub struct ProgramGate { + pub additional: Vec, +} + +/// Guard that stop the mint once the specified amount of items +/// redeenmed is reached. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::RedeemedAmount")] +pub struct RedeemedAmount { + pub maximum: u64, +} + +/// Guard that charges an amount in SOL (lamports) for the mint. +/// +/// List of accounts required: +/// +/// 0. `[]` Account to receive the fees. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::SolFixedFee")] +pub struct SolFixedFee { + pub lamports: u64, + pub destination: Pubkey, +} + +/// Guard that charges an amount in SOL (lamports) for the mint. +/// +/// List of accounts required: +/// +/// 0. `[]` Account to receive the funds. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::SolPayment")] +pub struct SolPayment { + pub lamports: u64, + pub destination: Pubkey, +} + +/// Guard that sets a specific start date for the mint. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::StartDate")] +pub struct StartDate { + pub date: i64, +} + +/// Guard that requires a specified signer to validate the transaction. +/// +/// List of accounts required: +/// +/// 0. `[signer]` Signer of the transaction. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::ThirdPartySigner ")] +pub struct ThirdPartySigner { + pub signer_key: Pubkey, +} + +/// Guard that requires addresses that hold an amount of a specified spl-token +/// and burns them. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Token mint account. +/// 2. `[]` SPL token program. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::TokenBurn")] +pub struct TokenBurn { + pub amount: u64, + pub mint: Pubkey, +} + +/// Guard that restricts access to addresses that hold the specified spl-token. +/// +/// List of accounts required: +/// +/// 0. `[]` Token account holding the required amount. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::TokenGate")] +pub struct TokenGate { + pub amount: u64, + pub mint: Pubkey, +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Address of the ATA to receive the tokens. +/// 2. `[]` SPL token program. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::TokenPayment")] +pub struct TokenPayment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint. +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Address of the ATA to receive the tokens. +/// 2. `[]` Mint account. +/// 3. `[]` SPL Token-2022 program account. +#[derive(Serialize, Deserialize, Clone, Debug, Convert)] +#[convert(from_on = "mpl_core_candy_guard::guards::Token2022Payment")] +pub struct Token2022Payment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} diff --git a/crates/cmds-solana/src/nft/candy_machine_core/wrap_core.rs b/crates/cmds-solana/src/nft/candy_machine_core/wrap_core.rs new file mode 100644 index 00000000..77f14c95 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_core/wrap_core.rs @@ -0,0 +1,78 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const NAME: &str = "wrap_core"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine_core/wrap_core.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + pub candy_machine_authority: Wallet, + #[serde(with = "value::pubkey")] + pub candy_guard: Pubkey, + pub candy_guard_authority: Wallet, + pub payer: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + static CANDY_GUARD_PROGRAM_ID: Pubkey = mpl_core_candy_guard::ID; + static CANDY_MACHINE_PROGRAM_ID: Pubkey = mpl_core_candy_machine_core::ID; + + let accounts = mpl_core_candy_guard::accounts::Wrap { + authority: input.candy_guard_authority.pubkey(), + candy_machine: input.candy_machine, + candy_machine_program: CANDY_MACHINE_PROGRAM_ID, + candy_machine_authority: input.candy_machine_authority.pubkey(), + candy_guard: input.candy_guard, + } + .to_account_metas(None); + + let data = mpl_core_candy_guard::instruction::Wrap {}.data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.candy_guard_authority, + input.candy_machine_authority, + ] + .into(), + instructions: [Instruction { + program_id: CANDY_GUARD_PROGRAM_ID, + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/add_config_lines.rs b/crates/cmds-solana/src/nft/candy_machine_v3/add_config_lines.rs new file mode 100644 index 00000000..e1f24dfd --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/add_config_lines.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +use mpl_candy_machine_core::instruction::AddConfigLines as MPLAddConfigLines; + +use super::ConfigLine; + +// Command Name +const ADD_CONFIG_LINES: &str = "add_config_lines"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine/add_config_lines.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(ADD_CONFIG_LINES)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(ADD_CONFIG_LINES, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + pub authority: Wallet, + pub payer: Wallet, + pub index: u32, + pub config_lines: Vec, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let accounts = mpl_candy_machine_core::accounts::AddConfigLines { + candy_machine: input.candy_machine, + authority: input.authority.pubkey(), + } + .to_account_metas(None); + + let data = MPLAddConfigLines { + index: input.index, + config_lines: input.config_lines.into_iter().map(Into::into).collect(), + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.authority].into(), + instructions: [Instruction { + program_id: mpl_candy_machine_core::id(), + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/guards.json b/crates/cmds-solana/src/nft/candy_machine_v3/guards.json new file mode 100644 index 00000000..ad6f051d --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/guards.json @@ -0,0 +1,77 @@ +{ + "default": { + "bot_tax": { + "lamports": 100, + "last_instruction": true + }, + "sol_payment": { + "lamports": 100, + "destination": "some_pubkey" + }, + "token_payment": { + "amount": 100, + "mint": "some_pubkey", + "destination_ata": "some_pubkey" + }, + "start_date": { + "date": "shoud be i64 unix timestamp" + }, + "third_party_signer": { + "signer_key": "some_pubkey" + }, + "token_gate": { + "amount": 100, + "mint": "some_pubkey" + }, + "gatekeeper": { + "gatekeeper_network": "some_pubkey", + "expire_on_use": true + }, + "end_date": { + "date": "shoud be i64 unix timestamp" + }, + "allow_list": { + "merkle_root": "[u8; 32] merkle_root of addresses" + }, + "mint_limit": { + "id": 1, + "limit": 3 + }, + "nft_payment": { + "required_collection": "some_pubkey", + "destination": "some_pubkey" + }, + "redeemed_amount": { + "maximum": 1000 + }, + "address_gate": { + "address": "some_pubkey The only address that is allowed to mint from the Candy Machine" + }, + "nft_gate": { + "required_collection": "some_pubkey" + }, + "nft_burn": { + "required_collection": "some_pubkey" + }, + "token_burn": { + "amount": 100, + "mint": "some_pubkey" + }, + "freeze_sol_payment": { + "lamports": 100, + "destination": "some_pubkey" + }, + "freeze_token_payment": { + "amount": 100, + "mint": "some_pubkey", + "destination_ata": "some_pubkey" + }, + "program_gate": { + "additional": ["some_pubkey", "some_pubkey"] + }, + "allocation": { + "id": 1, + "size": 100 + } + } +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/initialize.rs b/crates/cmds-solana/src/nft/candy_machine_v3/initialize.rs new file mode 100644 index 00000000..84012d4b --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/initialize.rs @@ -0,0 +1,182 @@ +use crate::{ + nft::{CandyMachineDataAlias, TokenStandard}, + prelude::*, +}; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::{instruction::Instruction, system_instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +use mpl_candy_machine_core::{instruction::InitializeV2, CandyMachineData}; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata}, + types::MetadataDelegateRole, +}; + +// Command Name +const INITIALIZE_CANDY_MACHINE: &str = "initialize_candy_machine"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine/initialize.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(INITIALIZE_CANDY_MACHINE)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(INITIALIZE_CANDY_MACHINE, |_| { + build() +})); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub candy_machine: Wallet, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint: Pubkey, + pub collection_update_authority: Wallet, + pub candy_machine_data: CandyMachineDataAlias, + pub token_standard: TokenStandard, + // Optional + #[serde(default = "value::default::bool_true")] + submit: bool, + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub rule_set: Pubkey, + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub authorization_rules_program: Pubkey, + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub authorization_rules: Pubkey, +} + +fn rule_set_default() -> Pubkey { + mpl_candy_machine_core::id() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let token_metadata_program = mpl_token_metadata::ID; + let candy_machine_program = mpl_candy_machine_core::id(); + let candy_pubkey = input.candy_machine.pubkey(); + + // Authority PDA + let seeds = &["candy_machine".as_ref(), candy_pubkey.as_ref()]; + let authority_pda = Pubkey::find_program_address(seeds, &candy_machine_program).0; + + // Collection Metadata PDA + let collection_metadata = Metadata::find_pda(&input.collection_mint).0; + + // Master Edition PDA + let collection_master_edition = MasterEdition::find_pda(&input.collection_mint).0; + + // Collection Delegate Record PDA + let collection_delegate_record = + mpl_token_metadata::accounts::MetadataDelegateRecord::find_pda( + &input.collection_mint, + MetadataDelegateRole::Collection, + &input.collection_update_authority.pubkey(), + &authority_pda, + ) + .0; + + let candy_machine_data = CandyMachineData::from(input.candy_machine_data); + + let accounts = mpl_candy_machine_core::accounts::InitializeV2 { + candy_machine: candy_pubkey, + authority_pda, + authority: input.authority, + payer: input.payer.pubkey(), + rule_set: Some(input.rule_set), + collection_metadata, + collection_mint: input.collection_mint, + collection_master_edition, + collection_update_authority: input.collection_update_authority.pubkey(), + collection_delegate_record, + token_metadata_program, + system_program: system_program::ID, + sysvar_instructions: solana_program::sysvar::instructions::id(), + authorization_rules_program: Some(input.authorization_rules_program), + authorization_rules: Some(input.authorization_rules), + } + .to_account_metas(None); + + let token_standard = input.token_standard as u8; + + let data = InitializeV2 { + data: candy_machine_data.clone(), + token_standard, + } + .data(); + + // TODO check size + let candy_account_size = candy_machine_data.get_space_for_candy().unwrap_or(216); + + let lamports = ctx + .solana_client + .get_minimum_balance_for_rent_exemption(candy_account_size) + .await?; + + let create_ix = system_instruction::create_account( + &input.payer.pubkey(), + &input.candy_machine.pubkey(), + lamports, + candy_account_size as u64, + &mpl_candy_machine_core::id(), + ); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.candy_machine, + input.collection_update_authority, + ] + .into(), + instructions: [ + create_ix, + Instruction { + program_id: mpl_candy_machine_core::id(), + accounts, + data, + }, + ] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +// { +// "items_available": 10, +// "symbol": "CORE", +// "seller_fee_basis_points": 500, +// "max_supply": 0, +// "is_mutable": true, +// "creators": [ +// { +// "address": "2gdutJtCz1f9P3NJGP4HbBYFCHMh8rVAhmT2QDSb9dN9", +// "verified": false, +// "share": 100 +// }], +// "config_line_settings": { +// "prefix_name": "TEST", +// "name_length": 10, +// "prefix_uri": "https://arweave.net/", +// "uri_length": 50, +// "is_sequential": false +// }, +// "hiddenSettings": null +// } diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/initialize_candy_guard.rs b/crates/cmds-solana/src/nft/candy_machine_v3/initialize_candy_guard.rs new file mode 100644 index 00000000..287570ba --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/initialize_candy_guard.rs @@ -0,0 +1,95 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use mpl_candy_guard::instruction::Initialize; +use solana_program::{instruction::Instruction, system_program}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const INITIALIZE_CANDY_GUARD: &str = "initialize_candy_guard"; + +const DEFINITION: &str = + flow_lib::node_definition!("nft/candy_machine/initialize_candy_guard.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(INITIALIZE_CANDY_GUARD)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(INITIALIZE_CANDY_GUARD, |_| { + build() +})); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub base: Wallet, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + pub candy_guards: super::CandyGuardData, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let candy_guard_program = mpl_candy_guard::id(); + + let base_pubkey = input.base.pubkey(); + + let seeds = &["candy_guard".as_ref(), base_pubkey.as_ref()]; + let candy_guard = Pubkey::find_program_address(seeds, &candy_guard_program).0; + + let accounts = mpl_candy_guard::accounts::Initialize { + authority: input.authority, + candy_guard, + base: input.base.pubkey(), + payer: input.payer.pubkey(), + system_program: system_program::id(), + } + .to_account_metas(None); + + // serialize input.candy_guards + let data: mpl_candy_guard::state::CandyGuardData = input.candy_guards.into(); + let mut serialized_data = vec![0; data.size()]; + data.save(&mut serialized_data)?; + + let data = Initialize { + data: serialized_data, + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.base].into(), + instructions: [Instruction { + program_id: mpl_candy_guard::id(), + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "candy_guard" => candy_guard, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/mint.rs b/crates/cmds-solana/src/nft/candy_machine_v3/mint.rs new file mode 100644 index 00000000..1a22d7b4 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/mint.rs @@ -0,0 +1,176 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use mpl_candy_guard::instruction::MintV2; +use solana_program::{instruction::Instruction, system_program, sysvar}; +use solana_sdk::{compute_budget::ComputeBudgetInstruction, pubkey::Pubkey}; + +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata}, + types::MetadataDelegateRole, +}; + +use super::CandyGuardData; + +// Command Name +const MINT: &str = "mint"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine/mint.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(MINT)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(MINT, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + #[serde(with = "value::pubkey")] + pub authority: Pubkey, + pub payer: Wallet, + pub minter: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub mint_authority: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint: Pubkey, + #[serde(with = "value::pubkey")] + pub collection_update_authority: Pubkey, + pub candy_guards: CandyGuardData, + pub group_label: Option, + // Optional + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub rule_set: Pubkey, + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub authorization_rules_program: Pubkey, + #[serde(default = "rule_set_default", with = "value::pubkey")] + pub authorization_rules: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +fn rule_set_default() -> Pubkey { + mpl_candy_machine_core::id() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let token_metadata_program = mpl_token_metadata::ID; + let candy_machine_program = mpl_candy_machine_core::id(); + let candy_guard_program = mpl_candy_guard::id(); + + // Authority PDA + let seeds = &["candy_machine".as_ref(), input.candy_machine.as_ref()]; + let candy_machine_authority_pda = Pubkey::find_program_address(seeds, &candy_machine_program).0; + + // Candy Guard PDA + let seeds = &["candy_guard".as_ref(), input.candy_machine.as_ref()]; + let candy_guard = Pubkey::find_program_address(seeds, &candy_guard_program).0; + + // Metadata PDA + let nft_metadata = Metadata::find_pda(&input.mint_account).0; + + // Master Edition PDA + let nft_master_edition = MasterEdition::find_pda(&input.mint_account).0; + + // NFT Associated Token Account + let nft_associated_token_account = spl_associated_token_account::get_associated_token_address( + &input.minter.pubkey(), + &input.mint_account, + ); + + // Metadata TokenRecord Account + let nft_token_record = mpl_token_metadata::accounts::TokenRecord::find_pda( + &input.mint_account, + &nft_associated_token_account, + ) + .0; + + // Collection Delegate Record PDA + let collection_delegate_record = + mpl_token_metadata::accounts::MetadataDelegateRecord::find_pda( + &input.collection_mint, + MetadataDelegateRole::Collection, + &input.collection_update_authority, + &candy_machine_authority_pda, + ) + .0; + + // Collection Metadata PDA + let collection_metadata = Metadata::find_pda(&input.collection_mint).0; + + // Collection Master Edition PDA + let collection_master_edition = MasterEdition::find_pda(&input.collection_mint).0; + + let accounts = mpl_candy_guard::accounts::MintV2 { + candy_guard, + candy_machine_program, + candy_machine: input.candy_machine, + candy_machine_authority_pda, + payer: input.payer.pubkey(), + minter: input.minter.pubkey(), + nft_mint: input.mint_account, + nft_mint_authority: input.mint_authority.pubkey(), + nft_metadata, + nft_master_edition, + token: Some(nft_associated_token_account), + token_record: Some(nft_token_record), + collection_delegate_record, + collection_mint: input.collection_mint, + collection_metadata, + collection_master_edition, + collection_update_authority: input.collection_update_authority, + token_metadata_program, + spl_token_program: spl_token::id(), + spl_ata_program: Some(spl_associated_token_account::id()), + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + recent_slothashes: sysvar::slot_hashes::id(), + authorization_rules_program: Some(mpl_candy_guard::id()), + authorization_rules: Some(mpl_candy_guard::id()), + } + .to_account_metas(None); + + // serialize input.candy_guards + let data: mpl_candy_guard::state::CandyGuardData = input.candy_guards.into(); + let mut serialized_data = vec![0; data.size()]; + data.save(&mut serialized_data)?; + + let data = MintV2 { + mint_args: serialized_data, + label: input.group_label, + } + .data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.minter, input.mint_authority].into(), + instructions: [ + ComputeBudgetInstruction::set_compute_unit_limit(1_000_000u32), + Instruction { + program_id: mpl_candy_guard::id(), + accounts, + data, + }, + ] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/mod.rs b/crates/cmds-solana/src/nft/candy_machine_v3/mod.rs new file mode 100644 index 00000000..a9d1d124 --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/mod.rs @@ -0,0 +1,594 @@ +use serde::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; + +pub mod add_config_lines; +pub mod initialize; +pub mod initialize_candy_guard; +pub mod mint; +pub mod wrap; + +/// Config line struct for storing asset (NFT) data pre-mint. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ConfigLine { + /// Name of the asset. + pub name: String, + /// URI to JSON metadata. + pub uri: String, +} + +// implement from ConfigLine mpl_candy_machine_core::ConfigLine +impl From for mpl_candy_machine_core::ConfigLine { + fn from(config_line: ConfigLine) -> Self { + Self { + name: config_line.name, + uri: config_line.uri, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CandyGuardData { + pub default: GuardSet, + pub groups: Option>, +} + +impl From for mpl_candy_guard::state::CandyGuardData { + fn from(candy_guard_data: CandyGuardData) -> Self { + Self { + default: candy_guard_data.default.into(), + groups: candy_guard_data + .groups + .map(|groups| groups.into_iter().map(|group| group.into()).collect()), + } + } +} + +// A group represent a specific set of guards. When groups are used, transactions +/// must specify which group should be used during validation. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Group { + pub label: String, + pub guards: GuardSet, +} + +impl From for mpl_candy_guard::state::Group { + fn from(group: Group) -> Self { + Self { + label: group.label, + guards: group.guards.into(), + } + } +} + +/// The set of guards available. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GuardSet { + /// Last instruction check and bot tax (penalty for invalid transactions). + pub bot_tax: Option, + /// Sol payment guard (set the price for the mint in lamports). + pub sol_payment: Option, + /// Token payment guard (set the price for the mint in spl-token amount). + pub token_payment: Option, + /// Start data guard (controls when minting is allowed). + pub start_date: Option, + /// Third party signer guard (requires an extra signer for the transaction). + pub third_party_signer: Option, + /// Token gate guard (restrict access to holders of a specific token). + pub token_gate: Option, + /// Gatekeeper guard (captcha challenge). + pub gatekeeper: Option, + /// End date guard (set an end date to stop the mint). + pub end_date: Option, + /// Allow list guard (curated list of allowed addresses). + pub allow_list: Option, + /// Mint limit guard (add a limit on the number of mints per wallet). + pub mint_limit: Option, + /// NFT Payment (charge an NFT in order to mint). + pub nft_payment: Option, + /// Redeemed amount guard (add a limit on the overall number of items minted). + pub redeemed_amount: Option, + /// Address gate (check access against a specified address). + pub address_gate: Option, + /// NFT gate guard (check access based on holding a specified NFT). + pub nft_gate: Option, + /// NFT burn guard (burn a specified NFT). + pub nft_burn: Option, + /// Token burn guard (burn a specified amount of spl-token). + pub token_burn: Option, + /// Freeze sol payment guard (set the price for the mint in lamports with a freeze period). + pub freeze_sol_payment: Option, + /// Freeze token payment guard (set the price for the mint in spl-token amount with a freeze period). + pub freeze_token_payment: Option, + /// Program gate guard (restricts the programs that can be in a mint transaction). + pub program_gate: Option, + /// Allocation guard (specify the maximum number of mints in a group). + pub allocation: Option, + pub token2022_payment: Option, +} + +impl From for mpl_candy_guard::state::GuardSet { + fn from(guard_set: GuardSet) -> Self { + Self { + bot_tax: guard_set.bot_tax.map(|bot_tax| bot_tax.into()), + sol_payment: guard_set.sol_payment.map(|sol_payment| sol_payment.into()), + token_payment: guard_set + .token_payment + .map(|token_payment| token_payment.into()), + start_date: guard_set.start_date.map(|start_date| start_date.into()), + third_party_signer: guard_set + .third_party_signer + .map(|third_party_signer| third_party_signer.into()), + token_gate: guard_set.token_gate.map(|token_gate| token_gate.into()), + gatekeeper: guard_set.gatekeeper.map(|gatekeeper| gatekeeper.into()), + end_date: guard_set.end_date.map(|end_date| end_date.into()), + allow_list: guard_set.allow_list.map(|allow_list| allow_list.into()), + mint_limit: guard_set.mint_limit.map(|mint_limit| mint_limit.into()), + nft_payment: guard_set.nft_payment.map(|nft_payment| nft_payment.into()), + redeemed_amount: guard_set + .redeemed_amount + .map(|redeemed_amount| redeemed_amount.into()), + address_gate: guard_set + .address_gate + .map(|address_gate| address_gate.into()), + nft_gate: guard_set.nft_gate.map(|nft_gate| nft_gate.into()), + nft_burn: guard_set.nft_burn.map(|nft_burn| nft_burn.into()), + token_burn: guard_set.token_burn.map(|token_burn| token_burn.into()), + freeze_sol_payment: guard_set + .freeze_sol_payment + .map(|freeze_sol_payment| freeze_sol_payment.into()), + freeze_token_payment: guard_set + .freeze_token_payment + .map(|freeze_token_payment| freeze_token_payment.into()), + program_gate: guard_set + .program_gate + .map(|program_gate| program_gate.into()), + allocation: guard_set.allocation.map(|allocation| allocation.into()), + token2022_payment: guard_set + .token2022_payment + .map(|token2022_payment| token2022_payment.into()), + } + } +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Address of the ATA to receive the tokens. +/// 2. `[]` Mint account. +/// 3. `[]` SPL Token-2022 program account. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Token2022Payment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} + +impl From for mpl_candy_guard::guards::Token2022Payment { + fn from(token2022_payment: Token2022Payment) -> Self { + Self { + amount: token2022_payment.amount, + mint: token2022_payment.mint, + destination_ata: token2022_payment.destination_ata, + } + } +} + +/// Guard that restricts access to a specific address. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AddressGate { + pub address: Pubkey, +} + +impl From for mpl_candy_guard::guards::AddressGate { + fn from(address_gate: AddressGate) -> Self { + Self { + address: address_gate.address, + } + } +} + +/// Gaurd to specify the maximum number of mints in a guard set. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Mint tracker PDA. The PDA is derived +/// using the seed `["allocation", allocation id, +/// candy guard pubkey, candy machine pubkey]`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Allocation { + /// Unique identifier of the allocation. + pub id: u8, + /// The size of the allocation. + pub size: u32, +} + +impl From for mpl_candy_guard::guards::Allocation { + fn from(allocation: Allocation) -> Self { + Self { + id: allocation.id, + limit: allocation.size, + } + } +} + +/// Guard that uses a merkle tree to specify the addresses allowed to mint. +/// +/// List of accounts required: +/// +/// 0. `[]` Pda created by the merkle proof instruction (seeds `["allow_list", merke tree root, +/// payer key, candy guard pubkey, candy machine pubkey]`). +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AllowList { + /// Merkle root of the addresses allowed to mint. + pub merkle_root: [u8; 32], +} + +impl From for mpl_candy_guard::guards::AllowList { + fn from(allow_list: AllowList) -> Self { + Self { + merkle_root: allow_list.merkle_root, + } + } +} + +/// Guard is used to: +/// * charge a penalty for invalid transactions +/// * validate that the mint transaction is the last transaction +/// * verify that only authorized programs have instructions +/// +/// The `bot_tax` is applied to any error that occurs during the +/// validation of the guards. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct BotTax { + pub lamports: u64, + pub last_instruction: bool, +} + +impl From for mpl_candy_guard::guards::BotTax { + fn from(bot_tax: BotTax) -> Self { + Self { + lamports: bot_tax.lamports, + last_instruction: bot_tax.last_instruction, + } + } +} + +/// Guard that sets a specific date for the mint to stop. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct EndDate { + pub date: i64, +} + +impl From for mpl_candy_guard::guards::EndDate { + fn from(end_date: EndDate) -> Self { + Self { + date: end_date.date, + } + } +} + +/// Guard that charges an amount in SOL (lamports) for the mint with a freeze period. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow", +/// destination pubkey, candy guard pubkey, candy machine pubkey]`). +/// 1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token +/// program pubkey, nft mint pubkey]`). +/// 2. `[optional]` Authorization rule set for the minted pNFT. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FreezeSolPayment { + pub lamports: u64, + pub destination: Pubkey, +} + +impl From for mpl_candy_guard::guards::FreezeSolPayment { + fn from(freeze_sol_payment: FreezeSolPayment) -> Self { + Self { + lamports: freeze_sol_payment.lamports, + destination: freeze_sol_payment.destination, + } + } +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint with a freeze period. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow", +/// destination_ata pubkey, candy guard pubkey, candy machine pubkey]`). +/// 1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token +/// program pubkey, nft mint pubkey]`). +/// 2. `[writable]` Token account holding the required amount. +/// 3. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA +/// pubkey, token program pubkey, nft mint pubkey]`). +/// 4. `[optional]` Authorization rule set for the minted pNFT. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FreezeTokenPayment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} + +impl From for mpl_candy_guard::guards::FreezeTokenPayment { + fn from(freeze_token_payment: FreezeTokenPayment) -> Self { + Self { + amount: freeze_token_payment.amount, + mint: freeze_token_payment.mint, + destination_ata: freeze_token_payment.destination_ata, + } + } +} + +/// Guard that validates if the payer of the transaction has a token from a specified +/// gateway network — in most cases, a token after completing a captcha challenge. +/// +/// List of accounts required: +/// +/// 0. `[writeable]` Gatekeeper token account. +/// 1. `[]` Gatekeeper program account. +/// 2. `[]` Gatekeeper expire account. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Gatekeeper { + /// The network for the gateway token required + pub gatekeeper_network: Pubkey, + /// Whether or not the token should expire after minting. + /// The gatekeeper network must support this if true. + pub expire_on_use: bool, +} + +impl From for mpl_candy_guard::guards::Gatekeeper { + fn from(gatekeeper: Gatekeeper) -> Self { + Self { + gatekeeper_network: gatekeeper.gatekeeper_network, + expire_on_use: gatekeeper.expire_on_use, + } + } +} + +/// Gaurd to set a limit of mints per wallet. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Mint counter PDA. The PDA is derived +/// using the seed `["mint_limit", mint guard id, payer key, +/// candy guard pubkey, candy machine pubkey]`. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MintLimit { + /// Unique identifier of the mint limit. + pub id: u8, + /// Limit of mints per individual address. + pub limit: u16, +} + +impl From for mpl_candy_guard::guards::MintLimit { + fn from(mint_limit: MintLimit) -> Self { + Self { + id: mint_limit.id, + limit: mint_limit.limit, + } + } +} + +/// Guard that requires another NFT (token) from a specific collection to be burned. +/// +/// List of accounts required: +/// +/// 0. `[writeable]` Token account of the NFT. +/// 1. `[writeable]` Metadata account of the NFT. +/// 2. `[writeable]` Master Edition account of the NFT. +/// 3. `[writeable]` Mint account of the NFT. +/// 4. `[writeable]` Collection metadata account of the NFT. +/// 5. `[writeable]` Token Record of the NFT (pNFT). +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NftBurn { + pub required_collection: Pubkey, +} + +impl From for mpl_candy_guard::guards::NftBurn { + fn from(nft_burn: NftBurn) -> Self { + Self { + required_collection: nft_burn.required_collection, + } + } +} + +/// Guard that restricts the transaction to holders of a specified collection. +/// +/// List of accounts required: +/// +/// 0. `[]` Token account of the NFT. +/// 1. `[]` Metadata account of the NFT. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NftGate { + pub required_collection: Pubkey, +} + +impl From for mpl_candy_guard::guards::NftGate { + fn from(nft_gate: NftGate) -> Self { + Self { + required_collection: nft_gate.required_collection, + } + } +} + +/// Guard that charges another NFT (token) from a specific collection as payment +/// for the mint. +/// +/// List of accounts required: +/// +/// 0. `[writeable]` Token account of the NFT. +/// 1. `[writeable]` Metadata account of the NFT. +/// 2. `[]` Mint account of the NFT. +/// 3. `[]` Account to receive the NFT. +/// 4. `[writeable]` Destination PDA key (seeds [destination pubkey, token program id, nft mint pubkey]). +/// 5. `[]` spl-associate-token program ID. +/// 6. `[]` Master edition (pNFT) +/// 7. `[writable]` Owner token record (pNFT) +/// 8. `[writable]` Destination token record (pNFT) +/// 9. `[]` Token Authorization Rules program (pNFT) +/// 10. `[]` Token Authorization Rules account (pNFT) +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NftPayment { + pub required_collection: Pubkey, + pub destination: Pubkey, +} + +impl From for mpl_candy_guard::guards::NftPayment { + fn from(nft_payment: NftPayment) -> Self { + Self { + required_collection: nft_payment.required_collection, + destination: nft_payment.destination, + } + } +} + +/// Guard that restricts the programs that can be in a mint transaction. The guard allows the +/// necessary programs for the mint and any other program specified in the configuration. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ProgramGate { + pub additional: Vec, +} + +impl From for mpl_candy_guard::guards::ProgramGate { + fn from(program_gate: ProgramGate) -> Self { + Self { + additional: program_gate.additional, + } + } +} + +/// Guard that stop the mint once the specified amount of items +/// redeenmed is reached. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RedeemedAmount { + pub maximum: u64, +} + +impl From for mpl_candy_guard::guards::RedeemedAmount { + fn from(redeemed_amount: RedeemedAmount) -> Self { + Self { + maximum: redeemed_amount.maximum, + } + } +} + +/// Guard that charges an amount in SOL (lamports) for the mint. +/// +/// List of accounts required: +/// +/// 0. `[]` Account to receive the funds. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SolPayment { + pub lamports: u64, + pub destination: Pubkey, +} + +// convert SolPayment to mpl_candy_machine_core::SolPayment +impl From for mpl_candy_guard::guards::SolPayment { + fn from(sol_payment: SolPayment) -> Self { + Self { + destination: sol_payment.destination, + lamports: sol_payment.lamports, + } + } +} + +/// Guard that sets a specific start date for the mint. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct StartDate { + pub date: i64, +} + +impl From for mpl_candy_guard::guards::StartDate { + fn from(start_date: StartDate) -> Self { + Self { + date: start_date.date, + } + } +} + +/// Guard that requires a specified signer to validate the transaction. +/// +/// List of accounts required: +/// +/// 0. `[signer]` Signer of the transaction. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ThirdPartySigner { + pub signer_key: Pubkey, +} + +impl From for mpl_candy_guard::guards::ThirdPartySigner { + fn from(third_party_signer: ThirdPartySigner) -> Self { + Self { + signer_key: third_party_signer.signer_key, + } + } +} + +/// Guard that requires addresses that hold an amount of a specified spl-token +/// and burns them. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Token mint account. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TokenBurn { + pub amount: u64, + pub mint: Pubkey, +} + +impl From for mpl_candy_guard::guards::TokenBurn { + fn from(token_burn: TokenBurn) -> Self { + Self { + amount: token_burn.amount, + mint: token_burn.mint, + } + } +} + +/// Guard that restricts access to addresses that hold the specified spl-token. +/// +/// List of accounts required: +/// +/// 0. `[]` Token account holding the required amount. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TokenGate { + pub amount: u64, + pub mint: Pubkey, +} + +impl From for mpl_candy_guard::guards::TokenGate { + fn from(token_gate: TokenGate) -> Self { + Self { + amount: token_gate.amount, + mint: token_gate.mint, + } + } +} + +/// Guard that charges an amount in a specified spl-token as payment for the mint. +/// +/// List of accounts required: +/// +/// 0. `[writable]` Token account holding the required amount. +/// 1. `[writable]` Address of the ATA to receive the tokens. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TokenPayment { + pub amount: u64, + pub mint: Pubkey, + pub destination_ata: Pubkey, +} + +impl From for mpl_candy_guard::guards::TokenPayment { + fn from(token_payment: TokenPayment) -> Self { + Self { + amount: token_payment.amount, + mint: token_payment.mint, + destination_ata: token_payment.destination_ata, + } + } +} diff --git a/crates/cmds-solana/src/nft/candy_machine_v3/wrap.rs b/crates/cmds-solana/src/nft/candy_machine_v3/wrap.rs new file mode 100644 index 00000000..50a9af1a --- /dev/null +++ b/crates/cmds-solana/src/nft/candy_machine_v3/wrap.rs @@ -0,0 +1,75 @@ +use crate::prelude::*; +use anchor_lang::{InstructionData, ToAccountMetas}; +use solana_program::instruction::Instruction; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const WRAP: &str = "wrap"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/candy_machine/wrap.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(WRAP)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(WRAP, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub candy_machine: Pubkey, + pub candy_machine_authority: Wallet, + #[serde(with = "value::pubkey")] + pub candy_guard: Pubkey, + pub candy_guard_authority: Wallet, + pub payer: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let accounts = mpl_candy_guard::accounts::Wrap { + authority: input.candy_guard_authority.pubkey(), + candy_machine: input.candy_machine, + candy_machine_program: mpl_candy_machine_core::id(), + candy_machine_authority: input.candy_machine_authority.pubkey(), + candy_guard: input.candy_guard, + } + .to_account_metas(None); + + let data = mpl_candy_guard::instruction::Wrap {}.data(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [ + input.payer, + input.candy_guard_authority, + input.candy_machine_authority, + ] + .into(), + instructions: [Instruction { + program_id: mpl_candy_guard::id(), + accounts, + data, + }] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/core/create_asset.rs b/crates/cmds-solana/src/nft/core/create_asset.rs new file mode 100644 index 00000000..944c5038 --- /dev/null +++ b/crates/cmds-solana/src/nft/core/create_asset.rs @@ -0,0 +1,146 @@ +use mpl_core::{ + instructions::CreateV2Builder, + types::{Plugin, PluginAuthorityPair}, +}; +use tracing::info; + +use crate::prelude::*; + +// Command Name +const NAME: &str = "create_core_v2"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/core/mpl_core_create_asset.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub asset: Wallet, + pub authority: Option, + pub name: String, + pub uri: String, + pub plugins: Vec, + pub verified_creator: Option, + #[serde(default, with = "value::pubkey::opt")] + pub collection: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // let mut additional_signers: Vec = Vec::new(); + let mut creators: Vec = Vec::new(); + + let plugins: Vec = input + .plugins + .iter() + .map(|plugin_authority_pair| { + let plugin = match &plugin_authority_pair.plugin { + Plugin::Royalties(royalties) => Plugin::Royalties(royalties.clone()), + Plugin::FreezeDelegate(freeze_delegate) => { + Plugin::FreezeDelegate(freeze_delegate.clone()) + } + Plugin::BurnDelegate(burn_delegate) => Plugin::BurnDelegate(burn_delegate.clone()), + Plugin::TransferDelegate(transfer_delegate) => { + Plugin::TransferDelegate(transfer_delegate.clone()) + } + Plugin::UpdateDelegate(update_delegate) => { + Plugin::UpdateDelegate(update_delegate.clone()) + } + Plugin::PermanentFreezeDelegate(permanent_freeze_delegate) => { + Plugin::PermanentFreezeDelegate(permanent_freeze_delegate.clone()) + } + Plugin::Attributes(attributes) => Plugin::Attributes(attributes.clone()), + Plugin::PermanentTransferDelegate(permanent_transfer_delegate) => { + Plugin::PermanentTransferDelegate(permanent_transfer_delegate.clone()) + } + Plugin::PermanentBurnDelegate(permanent_burn_delegate) => { + Plugin::PermanentBurnDelegate(permanent_burn_delegate.clone()) + } + Plugin::Edition(edition) => Plugin::Edition(edition.clone()), + Plugin::MasterEdition(master_edition) => { + Plugin::MasterEdition(master_edition.clone()) + } + Plugin::AddBlocker(add_blocker) => Plugin::AddBlocker(add_blocker.clone()), + Plugin::ImmutableMetadata(immutable_metadata) => { + Plugin::ImmutableMetadata(immutable_metadata.clone()) + } + Plugin::VerifiedCreators(verified_creators) => { + for signature in &verified_creators.signatures { + if signature.verified { + info!("verified creator: {}", signature.address); + creators.push(signature.address); + } + } + Plugin::VerifiedCreators(verified_creators.clone()) + } + Plugin::Autograph(autograph) => Plugin::Autograph(autograph.clone()), + }; + PluginAuthorityPair { + plugin, + authority: plugin_authority_pair.authority.clone(), + } + }) + .collect(); + + let mut builder = CreateV2Builder::new(); + + let builder = builder + .asset(input.asset.pubkey()) + .payer(input.fee_payer.pubkey()) + .name(input.name) + .uri(input.uri); + + let builder = if let Some(ref authority) = input.authority { + builder.authority(Some(authority.pubkey())) + } else { + builder.authority(None) + }; + + let builder = if let Some(collection) = input.collection { + builder.collection(Some(collection)) + } else { + builder + }; + + let builder = if !plugins.is_empty() { + builder.plugins(plugins) + } else { + builder + }; + + let ins = builder.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.asset] + .into_iter() + .chain(input.authority) + .chain(input.verified_creator) + .collect(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/core/create_collection.rs b/crates/cmds-solana/src/nft/core/create_collection.rs new file mode 100644 index 00000000..3260412d --- /dev/null +++ b/crates/cmds-solana/src/nft/core/create_collection.rs @@ -0,0 +1,176 @@ +use std::borrow::BorrowMut; + +use mpl_core::{ + instructions::CreateCollectionV2Builder, + types::{Plugin, PluginAuthorityPair}, +}; +use tracing::info; + +use crate::prelude::*; + +// Command Name +const NAME: &str = "create_core_collection_v2"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/core/mpl_core_create_collection.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub collection: Wallet, + pub update_authority: Option, + // TODO: should be an array of keypairs + pub verified_creator: Option, + pub name: String, + pub uri: String, + pub plugins: Vec, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // let mut additional_signers: Vec = Vec::new(); + let mut creators: Vec = Vec::new(); + + let plugins: Vec = input + .plugins + .iter() + .map(|plugin_authority_pair| { + let plugin = match &plugin_authority_pair.plugin { + Plugin::Royalties(royalties) => Plugin::Royalties(royalties.clone()), + Plugin::FreezeDelegate(freeze_delegate) => { + Plugin::FreezeDelegate(freeze_delegate.clone()) + } + Plugin::BurnDelegate(burn_delegate) => Plugin::BurnDelegate(burn_delegate.clone()), + Plugin::TransferDelegate(transfer_delegate) => { + Plugin::TransferDelegate(transfer_delegate.clone()) + } + Plugin::UpdateDelegate(update_delegate) => { + Plugin::UpdateDelegate(update_delegate.clone()) + } + Plugin::PermanentFreezeDelegate(permanent_freeze_delegate) => { + Plugin::PermanentFreezeDelegate(permanent_freeze_delegate.clone()) + } + Plugin::Attributes(attributes) => Plugin::Attributes(attributes.clone()), + Plugin::PermanentTransferDelegate(permanent_transfer_delegate) => { + Plugin::PermanentTransferDelegate(permanent_transfer_delegate.clone()) + } + Plugin::PermanentBurnDelegate(permanent_burn_delegate) => { + Plugin::PermanentBurnDelegate(permanent_burn_delegate.clone()) + } + Plugin::Edition(edition) => Plugin::Edition(edition.clone()), + Plugin::MasterEdition(master_edition) => { + Plugin::MasterEdition(master_edition.clone()) + } + Plugin::AddBlocker(add_blocker) => Plugin::AddBlocker(add_blocker.clone()), + Plugin::ImmutableMetadata(immutable_metadata) => { + Plugin::ImmutableMetadata(immutable_metadata.clone()) + } + Plugin::VerifiedCreators(verified_creators) => { + for signature in &verified_creators.signatures { + if signature.verified { + info!("verified creator: {}", signature.address); + creators.push(signature.address); + } + } + Plugin::VerifiedCreators(verified_creators.clone()) + } + Plugin::Autograph(autograph) => Plugin::Autograph(autograph.clone()), + }; + PluginAuthorityPair { + plugin, + authority: plugin_authority_pair.authority.clone(), + } + }) + .collect(); + + let mut builder = CreateCollectionV2Builder::new(); + + let builder = builder.borrow_mut(); + + let builder = builder + .collection(input.collection.pubkey()) + .payer(input.fee_payer.pubkey()) + .name(input.name) + // .external_plugin_adapters(vec![ExternalPluginAdapterInitInfo::DataStore( + // DataStoreInitInfo { + // data_authority: mpl_core::types::PluginAuthority::Owner, + // init_plugin_authority: Some(mpl_core::types::PluginAuthority::UpdateAuthority), + // schema: Some(ExternalPluginAdapterSchema::Binary), + // }, + // )]) + .uri(input.uri); + + let builder = if !plugins.is_empty() { + builder.plugins(plugins) + } else { + builder + }; + + // TODO: check this: update_authority is not in signer list + let builder = if let Some(update_authority) = input.update_authority { + builder.update_authority(Some(update_authority.pubkey())) + } else { + builder + }; + + let ins = builder.instruction(); + + let collection_pubkey = input.collection.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.collection] + .into_iter() + .chain(input.verified_creator) + .collect(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "collection" => collection_pubkey, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +// [ +// { +// "plugin": { +// "VerifiedCreators": { +// "signatures": [ +// { +// "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", +// "verified": true +// } +// ] +// } +// }, +// "authority": "Owner" +// } +// ] diff --git a/crates/cmds-solana/src/nft/core/fetch_assets.rs b/crates/cmds-solana/src/nft/core/fetch_assets.rs new file mode 100644 index 00000000..b1cc6ac3 --- /dev/null +++ b/crates/cmds-solana/src/nft/core/fetch_assets.rs @@ -0,0 +1,85 @@ +use mpl_core::{accounts::BaseAssetV1, types::Key}; +use solana_client::{ + rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, + rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, +}; +use tracing::info; + +use crate::prelude::*; + +// Command Name +const NAME: &str = "fetch_assets"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/core/fetch_assets.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(default, with = "value::pubkey")] + pub collection: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub assets: Vec, +} + +async fn run(ctx: Context, input: Input) -> Result { + // let rpc_data = ctx + // .solana_client + // .get_account_data(&input.collection) + // .await + // .unwrap(); + + let collection = input.collection; + info!("Collection {:?}", collection); + + let rpc_data = ctx + .solana_client + .get_program_accounts_with_config( + &mpl_core::ID, + RpcProgramAccountsConfig { + filters: Some(vec![ + RpcFilterType::Memcmp(Memcmp::new( + 0, + MemcmpEncodedBytes::Bytes(vec![Key::AssetV1 as u8]), + )), + RpcFilterType::Memcmp(Memcmp::new(34, MemcmpEncodedBytes::Bytes(vec![2_u8]))), + RpcFilterType::Memcmp(Memcmp::new( + 35, + MemcmpEncodedBytes::Base58(collection.to_string()), + )), + ]), + account_config: RpcAccountInfoConfig { + encoding: None, + data_slice: None, + commitment: None, + min_context_slot: None, + }, + with_context: None, + }, + ) + .await + .unwrap(); + + let accounts_iter = rpc_data.into_iter().map(|(_, account)| account); + + let mut assets: Vec = vec![]; + + for account in accounts_iter { + info!("Account {:?}", account); + let asset: BaseAssetV1 = BaseAssetV1::from_bytes(&account.data).unwrap(); + assets.push(asset); + } + info!("Assets {:?}", assets); + + Ok(Output { assets }) +} diff --git a/crates/cmds-solana/src/nft/core/mod.rs b/crates/cmds-solana/src/nft/core/mod.rs new file mode 100644 index 00000000..fa71a465 --- /dev/null +++ b/crates/cmds-solana/src/nft/core/mod.rs @@ -0,0 +1,5 @@ +pub mod create_asset; +pub mod create_collection; +pub mod fetch_assets; +pub mod update_asset; +pub mod update_plugin; diff --git a/crates/cmds-solana/src/nft/core/update_asset.rs b/crates/cmds-solana/src/nft/core/update_asset.rs new file mode 100644 index 00000000..9a206f70 --- /dev/null +++ b/crates/cmds-solana/src/nft/core/update_asset.rs @@ -0,0 +1,85 @@ +use mpl_core::{instructions::UpdateV1Builder, types::UpdateAuthority}; + +use crate::prelude::*; + +// Command Name +const NAME: &str = "update_core_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/core/mpl_core_update_asset.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub asset: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub collection: Option, + pub new_name: Option, + pub new_uri: Option, + pub new_update_authority: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut builder = UpdateV1Builder::new(); + + let builder = builder + .asset(input.asset.pubkey()) + .payer(input.fee_payer.pubkey()); + + let builder = if let Some(collection) = input.collection { + builder.collection(Some(collection)) + } else { + builder + }; + + let builder = if let Some(new_name) = input.new_name { + builder.new_name(new_name) + } else { + builder + }; + + let builder = if let Some(new_uri) = input.new_uri { + builder.new_uri(new_uri) + } else { + builder + }; + + let builder = if let Some(new_update_authority) = input.new_update_authority { + builder.new_update_authority(new_update_authority) + } else { + builder + }; + + let ins = builder.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/core/update_plugin.rs b/crates/cmds-solana/src/nft/core/update_plugin.rs new file mode 100644 index 00000000..ce5b5d7d --- /dev/null +++ b/crates/cmds-solana/src/nft/core/update_plugin.rs @@ -0,0 +1,74 @@ +use mpl_core::{instructions::UpdatePluginV1Builder, types::Plugin}; + +use crate::prelude::*; + +// Command Name +const NAME: &str = "mpl_core_update_plugin"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/core/mpl_core_update_plugin.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub asset: Pubkey, + pub update_authority: Option, + #[serde(default, with = "value::pubkey::opt")] + pub collection: Option, + pub plugin: Plugin, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let plugin: Plugin = input.plugin; + + let mut builder = UpdatePluginV1Builder::new(); + + let builder = builder + .asset(input.asset) + .payer(input.fee_payer.pubkey()) + .collection(input.collection.map(Into::into)) + .plugin(plugin); + + let builder = if let Some(ref update_authority) = input.update_authority { + builder.authority(Some(update_authority.pubkey())) + } else { + builder + }; + + let ins = builder.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer] + .into_iter() + .chain(input.update_authority) + .collect(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/create_master_edition.rs b/crates/cmds-solana/src/nft/create_master_edition.rs new file mode 100644 index 00000000..bdd66640 --- /dev/null +++ b/crates/cmds-solana/src/nft/create_master_edition.rs @@ -0,0 +1,87 @@ +use crate::prelude::*; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata}, + instructions::CreateMasterEditionV3InstructionArgs, +}; +use solana_program::system_program; + +// Command Name +const NAME: &str = "create_master_edition"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/create_master_edition.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + update_authority: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub mint_authority: Pubkey, + pub fee_payer: Wallet, + pub max_supply: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + let (master_edition_account, _) = MasterEdition::find_pda(&input.mint_account); + + let create_ix = mpl_token_metadata::instructions::CreateMasterEditionV3 { + edition: master_edition_account, + mint: input.mint_account, + update_authority: input.update_authority.pubkey(), + mint_authority: input.mint_authority, + payer: input.fee_payer.pubkey(), + metadata: metadata_account, + token_program: spl_token::id(), + system_program: system_program::id(), + rent: None, + }; + + let args = CreateMasterEditionV3InstructionArgs { + max_supply: input.max_supply, + }; + + let create_ix = create_ix.instruction(args); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.update_authority].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "metadata_account" => metadata_account, + "master_edition_account" => master_edition_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/create_metadata_account.rs b/crates/cmds-solana/src/nft/create_metadata_account.rs new file mode 100644 index 00000000..de526d41 --- /dev/null +++ b/crates/cmds-solana/src/nft/create_metadata_account.rs @@ -0,0 +1,160 @@ +use super::{CollectionDetails, NftDataV2}; +use crate::prelude::*; +use mpl_token_metadata::accounts::Metadata; +use solana_program::system_program; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const NAME: &str = "create_metadata_account"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/create_metadata_account.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub update_authority: Wallet, + pub is_mutable: bool, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub mint_authority: Pubkey, + pub fee_payer: Wallet, + pub metadata: NftDataV2, + pub collection_details: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + let create_ix = mpl_token_metadata::instructions::CreateMetadataAccountV3 { + metadata: metadata_account, + mint: input.mint_account, + mint_authority: input.mint_authority, + payer: input.fee_payer.pubkey(), + //TODO when would this be false? + update_authority: (input.update_authority.pubkey(), true), + system_program: system_program::id(), + //TODO double check this + rent: Some(input.fee_payer.pubkey()), + }; + + let args = mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs { + data: input.metadata.into(), + is_mutable: input.is_mutable, + collection_details: input.collection_details.map(|details| details.into()), + }; + + let ins = create_ix.instruction(args); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.update_authority].into(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "metadata_account" => metadata_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_inputs() { +// let metadata: Value = serde_json::from_str::( +// r#" +// { +// "name": "SO #11111", +// "symbol": "SPOP", +// "description": "Space Operator is a dynamic PFP collection", +// "seller_fee_basis_points": 250, +// "image": "https://arweave.net/vb1tD7tfAyrhZceA1MOYvvyqzZWgzHGDVZF37yDNH1Q", +// "attributes": [ +// { +// "trait_type": "Season", +// "value": "Fall" +// }, +// { +// "trait_type": "Light Color", +// "value": "Orange" +// } +// ], +// "properties": { +// "files": [ +// { +// "uri": "https://arweave.net/vb1tD7tfAyrhZceA1MOYvvyqzZWgzHGDVZF37yDNH1Q", +// "type": "image/jpeg" +// } +// ], +// "category": null +// } +// }"#, +// ) +// .unwrap() +// .into(); +// let uses: Value = serde_json::from_str::( +// r#" +// { +// "use_method": "Burn", +// "remaining": 500, +// "total": 500 +// } +// "#, +// ) +// .unwrap() +// .into(); +// let creators: Value = serde_json::from_str::( +// r#" +// [{ +// "address": "DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc", +// "share": 100 +// }]"#, +// ) +// .unwrap() +// .into(); +// let inputs = value::map! { +// PROXY_AS_UPDATE_AUTHORITY => "3G3ixjPdvg7NhazP932tCk88jgLJLzaDBe84mPa43Zyp", +// IS_MUTABLE => true, +// MINT_ACCOUNT => "C3EbZLYQ7Axv4PS9o4s4bSruFaiAVcynHZYds18VyWdZ", +// MINT_AUTHORITY => "C3EbZLYQ7Axv4PS9o4s4bSruFaiAVcynHZYds18VyWdZ", +// FEE_PAYER => "5s8bKTTgKLh2TudJBQwU6sx9DfFEtHcBP85aYZquEsqHrvipcWWCXxuyz4fsGsxTZ8NGMqMHFowUoQcoqcJSwLrP", +// METADATA => metadata, +// METADATA_URI => "https://arweave.net/3FxpIIbpySnfTTXIrpojhF2KHHjevI8Mrt3pACmEbSY", +// USES => uses, +// CREATORS => creators, +// }; +// let inputs: Input = value::from_map(inputs).unwrap(); +// dbg!(inputs); +// } +// } diff --git a/crates/cmds-solana/src/nft/get_left_uses.rs b/crates/cmds-solana/src/nft/get_left_uses.rs new file mode 100644 index 00000000..fc0183c8 --- /dev/null +++ b/crates/cmds-solana/src/nft/get_left_uses.rs @@ -0,0 +1,73 @@ +use crate::prelude::*; +use async_trait::async_trait; +use mpl_token_metadata::state::Metadata; +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug, Clone)] +pub struct GetLeftUses; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub left_uses: Option, +} + +const GET_LEFT_USES: &str = "get_left_uses"; + +// Inputs +const MINT_ACCOUNT: &str = "mint_account"; + +// Outputs +const LEFT_USES: &str = "left_uses"; + +#[async_trait] +impl CommandTrait for GetLeftUses { + fn name(&self) -> Name { + GET_LEFT_USES.into() + } + + fn inputs(&self) -> Vec { + [CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + fn outputs(&self) -> Vec { + [CmdOutput { + name: LEFT_USES.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { mint_account } = value::from_map(inputs)?; + + let (metadata_account, _) = Metadata::find_pda(&mint_account); + + let account_data = ctx + .solana_client + .get_account_data(&metadata_account) + .await?; + + let mut account_data_ptr = account_data.as_slice(); + + let metadata = ::deserialize(&mut account_data_ptr)?; + + let left_uses = metadata.uses.map(|v| v.remaining); + + Ok(value::to_map(&Output { left_uses })?) + } +} + +flow_lib::submit!(CommandDescription::new(GET_LEFT_USES, |_| Ok(Box::new( + GetLeftUses +)))); diff --git a/crates/cmds-solana/src/nft/mod.rs b/crates/cmds-solana/src/nft/mod.rs new file mode 100644 index 00000000..678542ae --- /dev/null +++ b/crates/cmds-solana/src/nft/mod.rs @@ -0,0 +1,332 @@ +use crate::prelude::Pubkey; +use mpl_candy_machine_core::{CandyMachineData as MPLCandyMachineData, HiddenSettings}; +use mpl_token_metadata::types::{Collection, DataV2, UseMethod, Uses}; +use serde::{Deserialize, Serialize}; + +// pub mod approve_collection_authority; +// pub mod approve_use_authority; +pub mod arweave_file_upload; +pub mod arweave_nft_upload; +pub mod candy_machine_core; +pub mod candy_machine_v3; +pub mod core; +pub mod create_master_edition; +pub mod create_metadata_account; +pub mod v1; +// pub mod get_left_uses; +// pub mod set_token_standard; +// pub mod sign_metadata; +// pub mod update_metadata_account; +// pub mod verify_collection; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftDataV2 { + pub name: String, + pub symbol: String, + pub uri: String, + pub seller_fee_basis_points: u16, + pub creators: Option>, + pub collection: Option, + pub uses: Option, +} + +impl From for DataV2 { + fn from(v: NftDataV2) -> Self { + Self { + name: v.name, + symbol: v.symbol, + uri: v.uri, + seller_fee_basis_points: v.seller_fee_basis_points, + creators: v.creators.map(|v| v.into_iter().map(Into::into).collect()), + collection: v.collection.map(Into::into), + uses: v.uses.map(Into::into), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftCollection { + pub verified: bool, + #[serde(with = "value::pubkey")] + pub key: Pubkey, +} + +impl From for Collection { + fn from(v: NftCollection) -> Self { + Self { + verified: v.verified, + key: v.key, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftMetadata { + pub name: String, + pub symbol: String, + pub description: String, + pub seller_fee_basis_points: u16, + pub image: String, + pub animation_url: Option, + pub external_url: Option, + pub attributes: Vec, + pub properties: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftMetadataAttribute { + pub trait_type: String, + pub value: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftMetadataProperties { + pub files: Option>, + pub category: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NftMetadataFile { + pub uri: String, + #[serde(rename = "type")] + pub kind: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct NftCreator { + #[serde(with = "value::pubkey")] + pub address: Pubkey, + pub verified: Option, + pub share: u8, // in percentage not basis points +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct NftUses { + pub use_method: NftUseMethod, + pub remaining: u64, + pub total: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum NftUseMethod { + Burn, + Single, + Multiple, +} + +impl From for Uses { + fn from(v: NftUses) -> Self { + Uses { + use_method: UseMethod::from(v.use_method.clone()), + remaining: v.remaining, + total: v.total, + } + } +} + +impl From for UseMethod { + fn from(v: NftUseMethod) -> Self { + match v { + NftUseMethod::Burn => UseMethod::Burn, + NftUseMethod::Single => UseMethod::Single, + NftUseMethod::Multiple => UseMethod::Multiple, + } + } +} + +impl From for mpl_token_metadata::types::Creator { + fn from(v: NftCreator) -> Self { + mpl_token_metadata::types::Creator { + address: v.address, + verified: v.verified.unwrap_or(false), + share: v.share, + } + } +} + +impl From for mpl_candy_machine_core::Creator { + fn from(v: NftCreator) -> Self { + mpl_candy_machine_core::Creator { + address: v.address, + verified: v.verified.unwrap_or(false), + percentage_share: v.share, + } + } +} + +// Candy machine configuration data. +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct CandyMachineDataAlias { + /// Number of assets available + pub items_available: u64, + /// Symbol for the asset + pub symbol: String, + /// Secondary sales royalty basis points (0-10000) + pub seller_fee_basis_points: u16, + /// Max supply of each individual asset (default 0) + pub max_supply: u64, + /// Indicates if the asset is mutable or not (default yes) + pub is_mutable: bool, + /// List of creators + pub creators: Vec, + /// Config line settings + pub config_line_settings: Option, + /// Hidden setttings + pub hidden_settings: Option, +} + +// +impl From for MPLCandyMachineData { + fn from(v: CandyMachineDataAlias) -> Self { + MPLCandyMachineData { + items_available: v.items_available, + symbol: v.symbol, + seller_fee_basis_points: v.seller_fee_basis_points, + max_supply: v.max_supply, + is_mutable: v.is_mutable, + creators: v.creators.into_iter().map(|c| c.into()).collect(), + config_line_settings: v.config_line_settings.map(|c| c.into()), + hidden_settings: v.hidden_settings.map(|h| h.into()), + } + } +} + +/// Hidden settings for large mints used with off-chain data. +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct HiddenSettingsAlias { + /// Asset prefix name + pub name: String, + /// Shared URI + pub uri: String, + /// Hash of the hidden settings file + pub hash: [u8; 32], +} + +// implement From for HiddenSettingsAlias +impl From for HiddenSettings { + fn from(v: HiddenSettingsAlias) -> Self { + HiddenSettings { + name: v.name, + uri: v.uri, + hash: v.hash, + } + } +} + +/// Config line settings to allocate space for individual name + URI. +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +pub struct ConfigLineSettingsAlias { + /// Common name prefix + pub prefix_name: String, + /// Length of the remaining part of the name + pub name_length: u32, + /// Common URI prefix + pub prefix_uri: String, + /// Length of the remaining part of the URI + pub uri_length: u32, + /// Indicates whether to use a senquential index generator or not + pub is_sequential: bool, +} + +// implement From for ConfigLineSettingsAlias +impl From for mpl_candy_machine_core::ConfigLineSettings { + fn from(v: ConfigLineSettingsAlias) -> Self { + mpl_candy_machine_core::ConfigLineSettings { + prefix_name: v.prefix_name, + name_length: v.name_length, + prefix_uri: v.prefix_uri, + uri_length: v.uri_length, + is_sequential: v.is_sequential, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum CollectionDetails { + V1 { size: u64 }, +} + +// implement From for CollectionDetails +impl From for mpl_token_metadata::types::CollectionDetails { + fn from(v: CollectionDetails) -> Self { + match v { + CollectionDetails::V1 { size } => { + mpl_token_metadata::types::CollectionDetails::V1 { size } + } + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum TokenStandard { + NonFungible, + FungibleAsset, + Fungible, + NonFungibleEdition, + ProgrammableNonFungible, + ProgrammableNonFungibleEdition, +} + +// Convert string to TokenStandard +impl From for TokenStandard { + fn from(v: String) -> Self { + match v.as_str() { + "non_fungible" => TokenStandard::NonFungible, + "fungible_asset" => TokenStandard::FungibleAsset, + "fungible" => TokenStandard::Fungible, + "non_fungible_edition" => TokenStandard::NonFungibleEdition, + "programmable_non_fungible" => TokenStandard::ProgrammableNonFungible, + "programmable_non_fungible_edition" => TokenStandard::ProgrammableNonFungibleEdition, + _ => panic!("Invalid token standard"), + } + } +} + +// implement From for TokenStandard +impl From for mpl_token_metadata::types::TokenStandard { + fn from(v: TokenStandard) -> Self { + match v { + TokenStandard::NonFungible => mpl_token_metadata::types::TokenStandard::NonFungible, + TokenStandard::FungibleAsset => mpl_token_metadata::types::TokenStandard::FungibleAsset, + TokenStandard::Fungible => mpl_token_metadata::types::TokenStandard::Fungible, + TokenStandard::NonFungibleEdition => { + mpl_token_metadata::types::TokenStandard::NonFungibleEdition + } + TokenStandard::ProgrammableNonFungible => { + mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible + } + TokenStandard::ProgrammableNonFungibleEdition => { + mpl_token_metadata::types::TokenStandard::ProgrammableNonFungibleEdition + } + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum PrintSupply { + Zero, + Limited(u64), + Unlimited, +} + +// convert 0,u64, none to PrintSupply +impl From> for PrintSupply { + fn from(v: Option) -> Self { + match v { + Some(0) => PrintSupply::Zero, + Some(supply) => PrintSupply::Limited(supply), + None => PrintSupply::Unlimited, + } + } +} + +// implement From for PrintSupply +impl From for mpl_token_metadata::types::PrintSupply { + fn from(v: PrintSupply) -> Self { + match v { + PrintSupply::Zero => mpl_token_metadata::types::PrintSupply::Zero, + PrintSupply::Limited(supply) => mpl_token_metadata::types::PrintSupply::Limited(supply), + PrintSupply::Unlimited => mpl_token_metadata::types::PrintSupply::Unlimited, + } + } +} diff --git a/crates/cmds-solana/src/nft/set_token_standard.rs b/crates/cmds-solana/src/nft/set_token_standard.rs new file mode 100644 index 00000000..d12d3b1a --- /dev/null +++ b/crates/cmds-solana/src/nft/set_token_standard.rs @@ -0,0 +1,67 @@ +use crate::prelude::*; + +// Command Name +const NAME: &str = "set_token_standard"; + +const DEFINITION: &str = flow_lib::node_definition!("NFT/set_token_standard.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub update_authority: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub edition_account: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&input.mint_account); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption( + 100, // std::mem::size_of::< + // mpl_token_metadata::state::VerifyCollection, + // >(), + ) + .await?; + + let instructions = vec![mpl_token_metadata::instruction::set_token_standard( + mpl_token_metadata::id(), + metadata_account, + input.update_authority.pubkey(), + input.mint_account, + input.edition_account, + )]; + + let ins = Instructions { +lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.update_authority].into(), + instructions, + minimum_balance_for_rent_exemption, + }; + + let signature: Option = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/sign_metadata.rs b/crates/cmds-solana/src/nft/sign_metadata.rs new file mode 100644 index 00000000..0ef7099c --- /dev/null +++ b/crates/cmds-solana/src/nft/sign_metadata.rs @@ -0,0 +1,135 @@ +use crate::prelude::*; +use solana_program::instruction::Instruction; + +#[derive(Debug, Clone)] +pub struct SignMetadata; + +impl SignMetadata { + fn command_sign_metadata( + &self, + metadata: Pubkey, + creator: Pubkey, + ) -> crate::Result<(u64, Vec)> { + let instructions = vec![mpl_token_metadata::instruction::sign_metadata( + mpl_token_metadata::id(), + metadata, + creator, + )]; + + Ok((0, instructions)) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + creator: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +const SIGN_METADATA: &str = "sign_metadata"; + +// Inputs +const FEE_PAYER: &str = "fee_payer"; +const MINT_ACCOUNT: &str = "mint_account"; +const CREATOR: &str = "creator"; +const SUBMIT: &str = "submit"; + +// Outputs +const SIGNATURE: &str = "signature"; + +#[async_trait] +impl CommandTrait for SignMetadata { + fn name(&self) -> Name { + SIGN_METADATA.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: FEE_PAYER.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: MINT_ACCOUNT.into(), + type_bounds: [ValueType::Pubkey].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: CREATOR.into(), + type_bounds: [ValueType::Keypair].to_vec(), + required: true, + passthrough: true, + }, + CmdInput { + name: SUBMIT.into(), + type_bounds: [ValueType::Bool].to_vec(), + required: false, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: SIGNATURE.into(), + r#type: ValueType::String, + }] + .to_vec() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let Input { + fee_payer, + mint_account, + creator, + submit, + } = value::from_map(inputs)?; + + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&mint_account); + + let (minimum_balance_for_rent_exemption, instructions) = + self.command_sign_metadata(metadata_account, creator.pubkey())?; + + let (mut transaction, recent_blockhash) = execute( + &ctx.solana_client, + &fee_payer.pubkey(), + &instructions, + minimum_balance_for_rent_exemption, + ) + .await?; + + try_sign_wallet( + &ctx, + &mut transaction, + &[&creator, &fee_payer], + recent_blockhash, + ) + .await?; + + let signature = if submit { + Some(submit_transaction(&ctx.solana_client, transaction).await?) + } else { + None + }; + + Ok(value::to_map(&Output { signature })?) + } +} + +flow_lib::submit!(CommandDescription::new(SIGN_METADATA, |_| Ok(Box::new( + SignMetadata +)))); diff --git a/crates/cmds-solana/src/nft/update_metadata_account.rs b/crates/cmds-solana/src/nft/update_metadata_account.rs new file mode 100644 index 00000000..67b10716 --- /dev/null +++ b/crates/cmds-solana/src/nft/update_metadata_account.rs @@ -0,0 +1,95 @@ +use crate::prelude::*; + +const NAME: &str = "update_metadata_account"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("NFT/update_metadata_account.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Deserialize, Debug)] +struct Input { + fee_payer: Wallet, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + update_authority: Wallet, + #[serde(default, with = "value::pubkey::opt")] + new_update_authority: Option, + data: Option, + primary_sale_happen: Option, + is_mutable: Option, +} + +#[derive(Serialize, Debug)] +struct Output0 { + #[serde(with = "value::pubkey")] + metadata_account: Pubkey, +} + +#[derive(Serialize, Debug)] +struct Output1 { + #[serde(with = "value::signature")] + signature: Signature, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&input.mint_account); + let signature = ctx + .execute( + Instructions { +lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [ + input.fee_payer, + input.update_authority, + ] + .into(), + + instructions: [ + mpl_token_metadata::instruction::update_metadata_accounts_v2( + mpl_token_metadata::id(), + metadata_account, + input.update_authority.pubkey(), + input.new_update_authority, + input.data.map(Into::into), + input.primary_sale_happen, + input.is_mutable, + ), + ] + .into(), + }, + value::to_map(&Output0 { metadata_account }).unwrap(), + ) + .await? + .signature + .expect("instructions is not empty"); + + Ok(Output1 { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[test] + fn test_minimal_input() { + value::from_map::(value::map! { + "fee_payer" => Keypair::new(), + "mint_account" => Pubkey::new_unique(), + "update_authority" => Keypair::new(), + }) + .unwrap(); + } +} diff --git a/crates/cmds-solana/src/nft/v1/burn_v1.rs b/crates/cmds-solana/src/nft/v1/burn_v1.rs new file mode 100644 index 00000000..610d7385 --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/burn_v1.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; +use mpl_token_metadata::{ + accounts::{Metadata, TokenRecord}, + types::TokenStandard, +}; + +// Command Name +const NAME: &str = "burn_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/burn_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + authority: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub amount: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + // // get associated token account pda + let token_account = spl_associated_token_account::get_associated_token_address( + &input.authority.pubkey(), + &input.mint_account, + ); + + let mut create_ix_builder = mpl_token_metadata::instructions::BurnV1Builder::new(); + create_ix_builder + .authority(input.authority.pubkey()) + .mint(input.mint_account) + .metadata(metadata_account) + .token(token_account); + + if let Some(acc) = ctx + .solana_client + .get_account_with_commitment(&metadata_account, ctx.solana_client.commitment()) + .await? + .value + { + let metadata = Metadata::safe_deserialize(&acc.data)?; + + if let Some(standard) = metadata.token_standard { + if standard == TokenStandard::ProgrammableNonFungible { + let token_record = + Some(TokenRecord::find_pda(&input.mint_account, &token_account).0); + create_ix_builder.token_record(token_record); + } + }; + } + + if let Some(amount) = input.amount { + create_ix_builder.amount(amount); + }; + + // if let Some(collection_metadata) = input.collection_metadata { + // create_ix_builder.collection_metadata(Some(collection_metadata)); + // }; + + // TODO implement editions burning + // https://github.com/metaplex-foundation/mpl-token-metadata/blob/main/programs/token-metadata/program/tests/utils/digital_asset.rs + // https://github.com/metaplex-foundation/mpl-token-metadata/blob/main/programs/token-metadata/program/tests/burn.rs + // let (master_edition_account, _) = MasterEdition::find_pda(&input.mint_account); + // if let Some(edition) = input.edition { + // create_ix_builder.edition(input.edition); + // }; + + let create_ix = create_ix_builder.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/v1/create_v1.rs b/crates/cmds-solana/src/nft/v1/create_v1.rs new file mode 100644 index 00000000..4a9e4b68 --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/create_v1.rs @@ -0,0 +1,173 @@ +use crate::prelude::*; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata}, + instructions::CreateV1InstructionArgs, + types::Collection, +}; +use solana_program::{system_program, sysvar}; + +use crate::nft::{ + CollectionDetails, NftCollection, NftCreator, NftDataV2, NftUses, PrintSupply, TokenStandard, +}; + +// Command Name +const NAME: &str = "create_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/create_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + update_authority: Wallet, + pub mint_account: Wallet, + #[serde(with = "value::pubkey")] + pub mint_authority: Pubkey, + pub data: NftDataV2, + pub print_supply: Option, + #[serde(default, with = "value::pubkey::opt")] + pub collection_mint_account: Option, + pub collection_details: Option, + pub is_mutable: bool, + pub token_standard: String, + pub decimals: Option, + pub creators: Option>, + pub uses: Option, + #[serde(default, with = "value::pubkey::opt")] + pub rule_set: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account.pubkey()); + + let (master_edition_account, _) = MasterEdition::find_pda(&input.mint_account.pubkey()); + + // // get associated token account pda + // let token_account = spl_associated_token_account::get_associated_token_address( + // &input.fee_payer.pubkey(), + // &input.mint_account.pubkey(), + // ); + + let create_ix = mpl_token_metadata::instructions::CreateV1 { + metadata: metadata_account, + master_edition: Some(master_edition_account), + mint: (input.mint_account.pubkey(), true), + authority: input.mint_authority, + payer: input.fee_payer.pubkey(), + update_authority: (input.update_authority.pubkey(), true), + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + spl_token_program: Some(spl_token::id()), + }; + + // Creators + let creators_input = input.creators.map(|creators| { + creators + .into_iter() + .map(|creator| creator.into()) + .collect::>() + }); + + let creators_data = input.data.creators.map(|creators| { + creators + .into_iter() + .map(|creator| creator.into()) + .collect::>() + }); + + let creators = creators_input.or(creators_data); + + // Uses + let uses = input + .uses + .map(Into::into) + .or_else(|| input.data.uses.map(Into::into)); + + // Token Standard + let token_standard: TokenStandard = input.token_standard.into(); + let token_standard: mpl_token_metadata::types::TokenStandard = token_standard.into(); + + // Collection + let collection = input + .collection_mint_account + .map(|key| { + Collection::from(NftCollection { + verified: false, + key, + }) + }) + .or(input.data.collection.map(Into::into)); + + // Print Supply + let print_supply = match input.print_supply { + Some(_) => { + let print_supply: PrintSupply = input.print_supply.into(); + let print_supply: mpl_token_metadata::types::PrintSupply = print_supply.into(); + Some(print_supply) + } + None => None, + }; + + let args = CreateV1InstructionArgs { + name: input.data.name, + symbol: input.data.symbol, + uri: input.data.uri, + seller_fee_basis_points: input.data.seller_fee_basis_points, + creators, + primary_sale_happened: false, + is_mutable: input.is_mutable, + token_standard, + collection, + uses, + collection_details: input.collection_details.map(|details| details.into()), + rule_set: input.rule_set, + decimals: input.decimals, + print_supply, + }; + + let create_ix = create_ix.instruction(args); + + let mint_account_pubkey = input.mint_account.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.update_authority, input.mint_account].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "metadata_account" => metadata_account, + "master_edition_account" => master_edition_account, + "mint_account" => mint_account_pubkey, + // "token"=> token_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/v1/delegate_v1.rs b/crates/cmds-solana/src/nft/v1/delegate_v1.rs new file mode 100644 index 00000000..ef14effb --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/delegate_v1.rs @@ -0,0 +1,775 @@ +use crate::prelude::*; +use anchor_lang::AnchorSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata, MetadataDelegateRecord, TokenRecord}, + instructions::{ + DelegateAuthorityItemV1InstructionArgs, DelegateCollectionItemV1InstructionArgs, + DelegateCollectionV1InstructionArgs, DelegateDataItemV1InstructionArgs, + DelegateDataV1InstructionArgs, DelegateLockedTransferV1InstructionArgs, + DelegateProgrammableConfigItemV1InstructionArgs, + DelegateProgrammableConfigV1InstructionArgs, DelegateSaleV1InstructionArgs, + DelegateStakingV1InstructionArgs, DelegateStandardV1InstructionArgs, + DelegateTransferV1InstructionArgs, DelegateUtilityV1InstructionArgs, + }, + types::{MetadataDelegateRole, TokenDelegateRole}, +}; +use solana_program::{system_program, sysvar}; + +use super::AuthorizationData; + +// Command Name +const NAME: &str = "delegate_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/delegate_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub delegate: Wallet, + pub update_authority: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub delegate_args: DelegateArgs, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +pub enum DelegateType { + Metadata(MetadataDelegateRole), + Token(TokenDelegateRole), +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + let (master_edition_account, _) = MasterEdition::find_pda(&input.mint_account); + + // get associated token account pda + let token_account = spl_associated_token_account::get_associated_token_address( + &input.fee_payer.pubkey(), + &input.mint_account, + ); + + let token_record = TokenRecord::find_pda(&input.mint_account, &token_account).0; + + let delegate_role: DelegateType = match input.delegate_args { + DelegateArgs::CollectionV1 { .. } => { + DelegateType::Metadata(MetadataDelegateRole::Collection) + } + DelegateArgs::SaleV1 { .. } => DelegateType::Token(TokenDelegateRole::Sale), + DelegateArgs::TransferV1 { .. } => DelegateType::Token(TokenDelegateRole::Transfer), + DelegateArgs::DataV1 { .. } => DelegateType::Metadata(MetadataDelegateRole::Data), + DelegateArgs::DataItemV1 { .. } => DelegateType::Metadata(MetadataDelegateRole::DataItem), + DelegateArgs::UtilityV1 { .. } => DelegateType::Token(TokenDelegateRole::Utility), + DelegateArgs::StakingV1 { .. } => DelegateType::Token(TokenDelegateRole::Staking), + DelegateArgs::StandardV1 { amount: _ } => DelegateType::Token(TokenDelegateRole::Standard), + DelegateArgs::LockedTransferV1 { .. } => { + DelegateType::Token(TokenDelegateRole::LockedTransfer) + } + DelegateArgs::ProgrammableConfigV1 { .. } => { + DelegateType::Metadata(MetadataDelegateRole::ProgrammableConfig) + } + DelegateArgs::AuthorityItemV1 { .. } => { + DelegateType::Metadata(MetadataDelegateRole::AuthorityItem) + } + DelegateArgs::CollectionItemV1 { .. } => { + DelegateType::Metadata(MetadataDelegateRole::CollectionItem) + } + DelegateArgs::ProgrammableConfigItemV1 { .. } => { + DelegateType::Metadata(MetadataDelegateRole::ProgrammableConfigItem) + } + }; + + let delegate_record = match delegate_role { + DelegateType::Metadata(role) => { + MetadataDelegateRecord::find_pda( + &input.mint_account, + role, + &input.update_authority.pubkey(), + &input.delegate.pubkey(), + ) + .0 + } + DelegateType::Token(..) => TokenRecord::find_pda(&input.mint_account, &token_account).0, + }; + + let delegate_v1 = DelegateV1 { + delegate_record: Some(delegate_record), + delegate: input.delegate.pubkey(), + metadata: metadata_account, + master_edition: Some(master_edition_account), + token_record: Some(token_record), + mint: input.mint_account, + // TODO: check if token account is correct + token: Some(token_account), + authority: input.update_authority.pubkey(), + payer: input.fee_payer.pubkey(), + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + spl_token_program: Some(spl_token::id()), + authorization_rules_program: None, + authorization_rules: None, + }; + + let create_ix = delegate_v1.instruction(input.delegate_args.into()); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.update_authority].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "delegate_record" => delegate_record, + "token_record" => token_record, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +/// Accounts. +pub struct DelegateV1 { + /// Delegate record account + pub delegate_record: Option, + /// Owner of the delegated account + pub delegate: solana_program::pubkey::Pubkey, + /// Metadata account + pub metadata: solana_program::pubkey::Pubkey, + /// Master Edition account + pub master_edition: Option, + /// Token record account + pub token_record: Option, + /// Mint of metadata + pub mint: solana_program::pubkey::Pubkey, + /// Token account of mint + pub token: Option, + /// Update authority or token owner + pub authority: solana_program::pubkey::Pubkey, + /// Payer + pub payer: solana_program::pubkey::Pubkey, + /// System Program + pub system_program: solana_program::pubkey::Pubkey, + /// Instructions sysvar account + pub sysvar_instructions: solana_program::pubkey::Pubkey, + /// SPL Token Program + pub spl_token_program: Option, + /// Token Authorization Rules Program + pub authorization_rules_program: Option, + /// Token Authorization Rules account + pub authorization_rules: Option, +} + +impl DelegateV1 { + pub fn instruction( + &self, + args: mpl_token_metadata::types::DelegateArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: mpl_token_metadata::types::DelegateArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(14 + remaining_accounts.len()); + if let Some(delegate_record) = self.delegate_record { + accounts.push(solana_program::instruction::AccountMeta::new( + delegate_record, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.delegate, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.metadata, + false, + )); + if let Some(master_edition) = self.master_edition { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + master_edition, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + if let Some(token_record) = self.token_record { + accounts.push(solana_program::instruction::AccountMeta::new( + token_record, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.mint, false, + )); + if let Some(token) = self.token { + accounts.push(solana_program::instruction::AccountMeta::new(token, false)); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.sysvar_instructions, + false, + )); + if let Some(spl_token_program) = self.spl_token_program { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + spl_token_program, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + if let Some(authorization_rules_program) = self.authorization_rules_program { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + authorization_rules_program, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + if let Some(authorization_rules) = self.authorization_rules { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + authorization_rules, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + remaining_accounts + .iter() + .for_each(|remaining_account| accounts.push(remaining_account.clone())); + + let (mut args, mut data) = match args { + mpl_token_metadata::types::DelegateArgs::AuthorityItemV1 { authorization_data } => ( + DelegateAuthorityItemV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateAuthorityItemV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::CollectionItemV1 { authorization_data } => ( + DelegateCollectionItemV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateCollectionItemV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::DataItemV1 { authorization_data } => ( + DelegateDataItemV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateDataItemV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::ProgrammableConfigItemV1 { + authorization_data, + } => ( + DelegateProgrammableConfigItemV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateProgrammableConfigItemV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::CollectionV1 { authorization_data } => ( + DelegateCollectionV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateCollectionV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::SaleV1 { + amount, + authorization_data, + } => ( + DelegateSaleV1InstructionArgs { + amount, + authorization_data, + } + .try_to_vec() + .unwrap(), + DelegateSaleV1InstructionData::new().try_to_vec().unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::TransferV1 { + amount, + authorization_data, + } => ( + DelegateTransferV1InstructionArgs { + amount, + authorization_data, + } + .try_to_vec() + .unwrap(), + DelegateTransferV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::DataV1 { authorization_data } => ( + DelegateDataV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateDataV1InstructionData::new().try_to_vec().unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::UtilityV1 { + amount, + authorization_data, + } => ( + DelegateUtilityV1InstructionArgs { + amount, + authorization_data, + } + .try_to_vec() + .unwrap(), + DelegateUtilityV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::StakingV1 { + amount, + authorization_data, + } => ( + DelegateStakingV1InstructionArgs { + amount, + authorization_data, + } + .try_to_vec() + .unwrap(), + DelegateStakingV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::StandardV1 { amount } => ( + DelegateStandardV1InstructionArgs { amount } + .try_to_vec() + .unwrap(), + DelegateStandardV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::LockedTransferV1 { + amount, + locked_address, + authorization_data, + } => ( + DelegateLockedTransferV1InstructionArgs { + amount, + locked_address, + authorization_data, + } + .try_to_vec() + .unwrap(), + DelegateLockedTransferV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::ProgrammableConfigV1 { + authorization_data, + } => ( + DelegateProgrammableConfigV1InstructionArgs { authorization_data } + .try_to_vec() + .unwrap(), + DelegateProgrammableConfigV1InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::DelegateArgs::PrintDelegateV1 { + authorization_data: _, + } => { + todo!() + } + }; + + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: mpl_token_metadata::ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateAuthorityItemV1InstructionData { + discriminator: u8, + delegate_authority_item_v1_discriminator: u8, +} + +impl DelegateAuthorityItemV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_authority_item_v1_discriminator: 9, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateCollectionItemV1InstructionData { + discriminator: u8, + delegate_collection_item_v1_discriminator: u8, +} + +impl DelegateCollectionItemV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_collection_item_v1_discriminator: 11, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateCollectionV1InstructionData { + discriminator: u8, + delegate_collection_v1_discriminator: u8, +} + +impl DelegateCollectionV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_collection_v1_discriminator: 0, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateDataItemV1InstructionData { + discriminator: u8, + delegate_data_item_v1_discriminator: u8, +} + +impl DelegateDataItemV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_data_item_v1_discriminator: 10, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateDataV1InstructionData { + discriminator: u8, + delegate_data_v1_discriminator: u8, +} + +impl DelegateDataV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_data_v1_discriminator: 3, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateLockedTransferV1InstructionData { + discriminator: u8, + delegate_locked_transfer_v1_discriminator: u8, +} + +impl DelegateLockedTransferV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_locked_transfer_v1_discriminator: 7, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateProgrammableConfigItemV1InstructionData { + discriminator: u8, + delegate_programmable_config_item_v1_discriminator: u8, +} + +impl DelegateProgrammableConfigItemV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_programmable_config_item_v1_discriminator: 12, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateProgrammableConfigV1InstructionData { + discriminator: u8, + delegate_programmable_config_v1_discriminator: u8, +} + +impl DelegateProgrammableConfigV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_programmable_config_v1_discriminator: 8, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateSaleV1InstructionData { + discriminator: u8, + delegate_sale_v1_discriminator: u8, +} + +impl DelegateSaleV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_sale_v1_discriminator: 1, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateStakingV1InstructionData { + discriminator: u8, + delegate_staking_v1_discriminator: u8, +} + +impl DelegateStakingV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_staking_v1_discriminator: 5, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateStandardV1InstructionData { + discriminator: u8, + delegate_standard_v1_discriminator: u8, +} + +impl DelegateStandardV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_standard_v1_discriminator: 6, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateTransferV1InstructionData { + discriminator: u8, + delegate_transfer_v1_discriminator: u8, +} + +impl DelegateTransferV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_transfer_v1_discriminator: 2, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct DelegateUtilityV1InstructionData { + discriminator: u8, + delegate_utility_v1_discriminator: u8, +} + +impl DelegateUtilityV1InstructionData { + fn new() -> Self { + Self { + discriminator: 44, + delegate_utility_v1_discriminator: 4, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum DelegateArgs { + CollectionV1 { + authorization_data: Option, + }, + SaleV1 { + amount: u64, + authorization_data: Option, + }, + TransferV1 { + amount: u64, + authorization_data: Option, + }, + DataV1 { + authorization_data: Option, + }, + UtilityV1 { + amount: u64, + authorization_data: Option, + }, + StakingV1 { + amount: u64, + authorization_data: Option, + }, + StandardV1 { + amount: u64, + }, + LockedTransferV1 { + amount: u64, + #[serde(with = "serde_with::As::")] + locked_address: Pubkey, + authorization_data: Option, + }, + ProgrammableConfigV1 { + authorization_data: Option, + }, + AuthorityItemV1 { + authorization_data: Option, + }, + DataItemV1 { + authorization_data: Option, + }, + CollectionItemV1 { + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + authorization_data: Option, + }, +} + +// implement from for DelegateArgs to mpl_token_metadata::types::DelegateArgs +impl From for mpl_token_metadata::types::DelegateArgs { + fn from(args: DelegateArgs) -> Self { + match args { + DelegateArgs::CollectionV1 { authorization_data } => Self::CollectionV1 { + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::SaleV1 { + amount, + authorization_data, + } => Self::SaleV1 { + amount, + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::TransferV1 { + amount, + authorization_data, + } => Self::TransferV1 { + amount, + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::DataV1 { authorization_data } => Self::DataV1 { + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::UtilityV1 { + amount, + authorization_data, + } => Self::UtilityV1 { + amount, + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::StakingV1 { + amount, + authorization_data, + } => Self::StakingV1 { + amount, + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::StandardV1 { amount } => Self::StandardV1 { amount }, + DelegateArgs::LockedTransferV1 { + amount, + locked_address, + authorization_data, + } => Self::LockedTransferV1 { + amount, + locked_address: locked_address.to_bytes().into(), + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::ProgrammableConfigV1 { authorization_data } => { + Self::ProgrammableConfigV1 { + authorization_data: authorization_data.map(Into::into), + } + } + DelegateArgs::AuthorityItemV1 { authorization_data } => Self::AuthorityItemV1 { + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::DataItemV1 { authorization_data } => Self::DataItemV1 { + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::CollectionItemV1 { authorization_data } => Self::CollectionItemV1 { + authorization_data: authorization_data.map(Into::into), + }, + DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => { + Self::ProgrammableConfigItemV1 { + authorization_data: authorization_data.map(Into::into), + } + } + } + } +} diff --git a/crates/cmds-solana/src/nft/v1/mint_v1.rs b/crates/cmds-solana/src/nft/v1/mint_v1.rs new file mode 100644 index 00000000..44706427 --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/mint_v1.rs @@ -0,0 +1,110 @@ +use crate::prelude::*; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata, TokenRecord}, + instructions::MintV1InstructionArgs, +}; +use solana_program::{system_program, sysvar}; + +use super::AuthorizationData; + +// Command Name +const NAME: &str = "mint_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/mint_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub authority: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(with = "value::pubkey")] + pub token_owner: Pubkey, + pub amount: u64, + #[serde(default, with = "value::pubkey::opt")] + pub delegate_record: Option, + #[serde(default, with = "value::pubkey::opt")] + pub authorization_rules_program: Option, + #[serde(default, with = "value::pubkey::opt")] + pub authorization_rules: Option, + pub authorization_data: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + let (master_edition_account, _) = MasterEdition::find_pda(&input.mint_account); + + // get associated token account pda + let token_account = spl_associated_token_account::get_associated_token_address( + &input.token_owner, + &input.mint_account, + ); + + let token_record = TokenRecord::find_pda(&input.mint_account, &token_account).0; + + let create_ix = mpl_token_metadata::instructions::MintV1 { + metadata: metadata_account, + master_edition: Some(master_edition_account), + mint: input.mint_account, + authority: input.authority.pubkey(), + payer: input.fee_payer.pubkey(), + token: token_account, + token_owner: Some(input.token_owner), + token_record: Some(token_record), + delegate_record: input.delegate_record, + spl_ata_program: spl_associated_token_account::id(), + authorization_rules_program: input.authorization_rules_program, + authorization_rules: input.authorization_rules, + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + spl_token_program: spl_token::id(), + }; + + let args = MintV1InstructionArgs { + amount: input.amount, + authorization_data: input.authorization_data.map(Into::into), + }; + + let create_ix = create_ix.instruction(args); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "token"=> token_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/v1/mod.rs b/crates/cmds-solana/src/nft/v1/mod.rs new file mode 100644 index 00000000..1fb118ca --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/mod.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use solana_program::pubkey::Pubkey; + +pub mod create_v1; +pub mod delegate_v1; +pub mod update_v1; +pub mod verify_collection_v1; +// pub mod transfer_v1; +pub mod burn_v1; +pub mod mint_v1; +pub mod verify_creator_v1; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct AuthorizationData { + pub payload: Payload, +} + +impl From for mpl_token_metadata::types::AuthorizationData { + fn from(authorization_data: AuthorizationData) -> Self { + Self { + payload: authorization_data.payload.into(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Payload { + pub map: HashMap, +} + +impl From for mpl_token_metadata::types::Payload { + fn from(payload: Payload) -> Self { + let mut map = std::collections::HashMap::new(); + for (key, value) in payload.map { + map.insert(key, value.into()); + } + Self { map } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum PayloadType { + Pubkey(Pubkey), + Seeds(SeedsVec), + MerkleProof(ProofInfo), + Number(u64), +} + +impl From for mpl_token_metadata::types::PayloadType { + fn from(payload_type: PayloadType) -> Self { + match payload_type { + PayloadType::Pubkey(pubkey) => Self::Pubkey(pubkey), + PayloadType::Seeds(seeds_vec) => Self::Seeds(seeds_vec.into()), + PayloadType::MerkleProof(proof_info) => Self::MerkleProof(proof_info.into()), + PayloadType::Number(number) => Self::Number(number), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct SeedsVec { + pub seeds: Vec>, +} + +impl From for mpl_token_metadata::types::SeedsVec { + fn from(seeds_vec: SeedsVec) -> Self { + Self { + seeds: seeds_vec.seeds, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct ProofInfo { + pub proof: Vec<[u8; 32]>, +} + +impl From for mpl_token_metadata::types::ProofInfo { + fn from(proof_info: ProofInfo) -> Self { + Self { + proof: proof_info.proof, + } + } +} diff --git a/crates/cmds-solana/src/nft/v1/update_v1.rs b/crates/cmds-solana/src/nft/v1/update_v1.rs new file mode 100644 index 00000000..2340b592 --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/update_v1.rs @@ -0,0 +1,830 @@ +use std::str::FromStr; + +use crate::{ + nft::{CollectionDetails, NftCreator, NftUses, TokenStandard}, + prelude::*, +}; +use anchor_lang::AnchorSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; +use mpl_token_metadata::{ + accounts::{MasterEdition, Metadata}, + instructions::{ + UpdateAsAuthorityItemDelegateV2InstructionArgs, + UpdateAsCollectionDelegateV2InstructionArgs, + UpdateAsCollectionItemDelegateV2InstructionArgs, UpdateAsDataDelegateV2InstructionArgs, + UpdateAsDataItemDelegateV2InstructionArgs, + UpdateAsProgrammableConfigDelegateV2InstructionArgs, + UpdateAsProgrammableConfigItemDelegateV2InstructionArgs, + UpdateAsUpdateAuthorityV2InstructionArgs, UpdateV1InstructionArgs, + }, +}; +use solana_program::{system_program, sysvar}; + +use super::AuthorizationData; + +// Command Name +const NAME: &str = "update_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/update_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub delegate: Option, + #[serde(default, with = "value::pubkey::opt")] + pub delegate_record: Option, + update_authority: Option, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub update_args: UpdateArgs, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + + let (_master_edition_account, _) = MasterEdition::find_pda(&input.mint_account); + + // get associated token account pda + let _token_account = spl_associated_token_account::get_associated_token_address( + &input.fee_payer.pubkey(), + &input.mint_account, + ); + + // let token_record = TokenRecord::find_pda(&input.mint_account, &token_account).0; + + let authority_or_delegate = input.delegate.unwrap_or_else(|| { + input + .update_authority + .expect("update_authority field must be set") + }); + + let delegate_v1 = UpdateAsDelegateV1 { + authority: authority_or_delegate.pubkey(), + delegate_record: input.delegate_record, + // TODO + token: None, + mint: input.mint_account, + metadata: metadata_account, + // TODO: edition + edition: None, + payer: input.fee_payer.pubkey(), + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + authorization_rules_program: None, + authorization_rules: None, + }; + + let create_ix = delegate_v1.instruction(input.update_args.into()); + + let authority_or_delegate_pubkey = authority_or_delegate.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, authority_or_delegate].into(), + instructions: [create_ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "authority or delegate" => authority_or_delegate_pubkey, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +/// Accounts. +pub struct UpdateAsDelegateV1 { + /// Update authority or delegate + pub authority: solana_program::pubkey::Pubkey, + /// Delegate record PDA + pub delegate_record: Option, + /// Token account + pub token: Option, + /// Mint account + pub mint: solana_program::pubkey::Pubkey, + /// Metadata account + pub metadata: solana_program::pubkey::Pubkey, + /// Edition account + pub edition: Option, + /// Payer + pub payer: solana_program::pubkey::Pubkey, + /// System program + pub system_program: solana_program::pubkey::Pubkey, + /// Instructions sysvar account + pub sysvar_instructions: solana_program::pubkey::Pubkey, + /// Token Authorization Rules Program + pub authorization_rules_program: Option, + /// Token Authorization Rules account + pub authorization_rules: Option, +} + +impl UpdateAsDelegateV1 { + pub fn instruction( + &self, + args: mpl_token_metadata::types::UpdateArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: mpl_token_metadata::types::UpdateArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(11 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.authority, + true, + )); + if let Some(delegate_record) = self.delegate_record { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + delegate_record, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + if let Some(token) = self.token { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + token, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.mint, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.metadata, + false, + )); + if let Some(edition) = self.edition { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + edition, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.sysvar_instructions, + false, + )); + if let Some(authorization_rules_program) = self.authorization_rules_program { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + authorization_rules_program, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + if let Some(authorization_rules) = self.authorization_rules { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + authorization_rules, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + mpl_token_metadata::ID, + false, + )); + } + remaining_accounts + .iter() + .for_each(|remaining_account| accounts.push(remaining_account.clone())); + + let (mut args, mut data) = match args { + mpl_token_metadata::types::UpdateArgs::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + authorization_data, + } => ( + UpdateAsAuthorityItemDelegateV2InstructionArgs { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsAuthorityItemDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsCollectionDelegateV2 { + collection, + authorization_data, + } => ( + UpdateAsCollectionDelegateV2InstructionArgs { + collection, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsCollectionDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsDataDelegateV2 { + data, + authorization_data, + } => ( + UpdateAsDataDelegateV2InstructionArgs { + data, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsDataDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsProgrammableConfigDelegateV2 { + rule_set, + authorization_data, + } => ( + UpdateAsProgrammableConfigDelegateV2InstructionArgs { + rule_set, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsProgrammableConfigDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsDataItemDelegateV2 { + data, + authorization_data, + } => ( + UpdateAsDataItemDelegateV2InstructionArgs { + data, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsDataItemDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsCollectionItemDelegateV2 { + collection, + authorization_data, + } => ( + UpdateAsCollectionItemDelegateV2InstructionArgs { + collection, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsCollectionItemDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsProgrammableConfigItemDelegateV2 { + rule_set, + authorization_data, + } => ( + UpdateAsProgrammableConfigItemDelegateV2InstructionArgs { + rule_set, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsProgrammableConfigItemDelegateV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + authorization_data, + } => ( + UpdateV1InstructionArgs { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateV1InstructionData::new().try_to_vec().unwrap(), + ), + mpl_token_metadata::types::UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + authorization_data, + } => ( + UpdateAsUpdateAuthorityV2InstructionArgs { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + authorization_data, + } + .try_to_vec() + .unwrap(), + UpdateAsUpdateAuthorityV2InstructionData::new() + .try_to_vec() + .unwrap(), + ), + }; + + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: mpl_token_metadata::ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsAuthorityItemDelegateV2InstructionData { + discriminator: u8, + update_as_authority_item_delegate_v2_discriminator: u8, +} + +impl UpdateAsAuthorityItemDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_authority_item_delegate_v2_discriminator: 2, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsCollectionDelegateV2InstructionData { + discriminator: u8, + update_as_collection_delegate_v2_discriminator: u8, +} + +impl UpdateAsCollectionDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_collection_delegate_v2_discriminator: 3, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsCollectionItemDelegateV2InstructionData { + discriminator: u8, + update_as_collection_item_delegate_v2_discriminator: u8, +} + +impl UpdateAsCollectionItemDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_collection_item_delegate_v2_discriminator: 7, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsDataDelegateV2InstructionData { + discriminator: u8, + update_as_data_delegate_v2_discriminator: u8, +} + +impl UpdateAsDataDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_data_delegate_v2_discriminator: 4, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsDataItemDelegateV2InstructionData { + discriminator: u8, + update_as_data_item_delegate_v2_discriminator: u8, +} + +impl UpdateAsDataItemDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_data_item_delegate_v2_discriminator: 6, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsProgrammableConfigDelegateV2InstructionData { + discriminator: u8, + update_as_programmable_config_delegate_v2_discriminator: u8, +} + +impl UpdateAsProgrammableConfigDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_programmable_config_delegate_v2_discriminator: 5, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsProgrammableConfigItemDelegateV2InstructionData { + discriminator: u8, + update_as_programmable_config_item_delegate_v2_discriminator: u8, +} + +impl UpdateAsProgrammableConfigItemDelegateV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_programmable_config_item_delegate_v2_discriminator: 8, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateAsUpdateAuthorityV2InstructionData { + discriminator: u8, + update_as_update_authority_v2_discriminator: u8, +} + +impl UpdateAsUpdateAuthorityV2InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_as_update_authority_v2_discriminator: 1, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateMetadataAccountV2InstructionData { + discriminator: u8, +} + +impl Default for UpdateMetadataAccountV2InstructionData { + fn default() -> Self { + Self { discriminator: 15 } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct UpdateV1InstructionData { + discriminator: u8, + update_v1_discriminator: u8, +} + +impl UpdateV1InstructionData { + fn new() -> Self { + Self { + discriminator: 50, + update_v1_discriminator: 0, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum UpdateArgs { + V1 { + new_update_authority: Option, + data: Option, + primary_sale_happened: Option, + is_mutable: Option, + collection: CollectionToggle, + collection_details: CollectionDetailsToggle, + uses: UsesToggle, + rule_set: RuleSetToggle, + authorization_data: Option, + }, + AsUpdateAuthorityV2 { + new_update_authority: Option, + data: Option, + primary_sale_happened: Option, + is_mutable: Option, + collection: CollectionToggle, + collection_details: CollectionDetailsToggle, + uses: UsesToggle, + rule_set: RuleSetToggle, + token_standard: Option, + authorization_data: Option, + }, + AsAuthorityItemDelegateV2 { + new_update_authority: Option, + primary_sale_happened: Option, + is_mutable: Option, + token_standard: Option, + authorization_data: Option, + }, + AsCollectionDelegateV2 { + collection: CollectionToggle, + authorization_data: Option, + }, + AsDataDelegateV2 { + data: Option, + authorization_data: Option, + }, + AsProgrammableConfigDelegateV2 { + rule_set: RuleSetToggle, + authorization_data: Option, + }, + AsDataItemDelegateV2 { + data: Option, + authorization_data: Option, + }, + AsCollectionItemDelegateV2 { + collection: CollectionToggle, + authorization_data: Option, + }, + AsProgrammableConfigItemDelegateV2 { + rule_set: RuleSetToggle, + authorization_data: Option, + }, +} + +impl From for mpl_token_metadata::types::UpdateArgs { + fn from(args: UpdateArgs) -> Self { + match args { + UpdateArgs::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard, + authorization_data, + } => Self::AsAuthorityItemDelegateV2 { + new_update_authority, + primary_sale_happened, + is_mutable, + token_standard: token_standard.map(Into::into), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsCollectionDelegateV2 { + collection, + authorization_data, + } => Self::AsCollectionDelegateV2 { + collection: collection.into(), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsDataDelegateV2 { + data, + authorization_data, + } => Self::AsDataDelegateV2 { + data: data.map(Into::into), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsProgrammableConfigDelegateV2 { + rule_set, + authorization_data, + } => Self::AsProgrammableConfigDelegateV2 { + rule_set: rule_set.into(), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsDataItemDelegateV2 { + data, + authorization_data, + } => Self::AsDataItemDelegateV2 { + data: data.map(Into::into), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsCollectionItemDelegateV2 { + collection, + authorization_data, + } => Self::AsCollectionItemDelegateV2 { + collection: collection.into(), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsProgrammableConfigItemDelegateV2 { + rule_set, + authorization_data, + } => Self::AsProgrammableConfigItemDelegateV2 { + rule_set: rule_set.into(), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + authorization_data, + } => Self::V1 { + new_update_authority, + data: data.map(Into::into), + primary_sale_happened, + is_mutable, + collection: collection.into(), + collection_details: collection_details.into(), + uses: uses.into(), + rule_set: rule_set.into(), + authorization_data: authorization_data.map(Into::into), + }, + UpdateArgs::AsUpdateAuthorityV2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + authorization_data, + } => Self::AsUpdateAuthorityV2 { + new_update_authority: new_update_authority + .map(|authority| Pubkey::from_str(&authority).unwrap()), + data: data.map(Into::into), + primary_sale_happened, + is_mutable, + collection: collection.into(), + collection_details: collection_details.into(), + uses: uses.into(), + rule_set: rule_set.into(), + token_standard: token_standard.map(Into::into), + authorization_data: authorization_data.map(Into::into), + }, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Data { + pub name: String, + pub symbol: String, + pub uri: String, + pub seller_fee_basis_points: u16, + pub creators: Option>, +} + +impl From for mpl_token_metadata::types::Data { + fn from(data: Data) -> Self { + Self { + name: data.name, + symbol: data.symbol, + uri: data.uri, + seller_fee_basis_points: data.seller_fee_basis_points, + creators: data + .creators + .map(|creators| creators.into_iter().map(Into::into).collect()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum CollectionToggle { + None, + Clear, + Set(Collection), +} + +impl From for mpl_token_metadata::types::CollectionToggle { + fn from(toggle: CollectionToggle) -> Self { + match toggle { + CollectionToggle::None => Self::None, + CollectionToggle::Clear => Self::Clear, + CollectionToggle::Set(collection) => Self::Set(collection.into()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct Collection { + pub verified: bool, + #[serde(with = "serde_with::As::")] + pub key: Pubkey, +} + +impl From for mpl_token_metadata::types::Collection { + fn from(collection: Collection) -> Self { + Self { + verified: collection.verified, + key: collection.key, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum CollectionDetailsToggle { + None, + Clear, + Set(CollectionDetails), +} + +impl From for mpl_token_metadata::types::CollectionDetailsToggle { + fn from(toggle: CollectionDetailsToggle) -> Self { + match toggle { + CollectionDetailsToggle::None => Self::None, + CollectionDetailsToggle::Clear => Self::Clear, + CollectionDetailsToggle::Set(details) => Self::Set(details.into()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum UsesToggle { + None, + Clear, + Set(NftUses), +} + +impl From for mpl_token_metadata::types::UsesToggle { + fn from(toggle: UsesToggle) -> Self { + match toggle { + UsesToggle::None => Self::None, + UsesToggle::Clear => Self::Clear, + UsesToggle::Set(uses) => Self::Set(uses.into()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum RuleSetToggle { + None, + Clear, + Set(Pubkey), +} + +impl From for mpl_token_metadata::types::RuleSetToggle { + fn from(toggle: RuleSetToggle) -> Self { + match toggle { + RuleSetToggle::None => Self::None, + RuleSetToggle::Clear => Self::Clear, + RuleSetToggle::Set(pubkey) => Self::Set(pubkey), + } + } +} diff --git a/crates/cmds-solana/src/nft/v1/verify_collection_v1.rs b/crates/cmds-solana/src/nft/v1/verify_collection_v1.rs new file mode 100644 index 00000000..7f5d0b5d --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/verify_collection_v1.rs @@ -0,0 +1,72 @@ +use crate::prelude::*; +use mpl_token_metadata::accounts::{MasterEdition, Metadata}; +use solana_program::{system_program, sysvar}; + +// Command Name +const NAME: &str = "verify_collection_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/verify_collection_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub fee_payer: Wallet, + pub collection_authority: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint_account: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub delegate_record: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (metadata_account, _) = Metadata::find_pda(&input.mint_account); + let (collection_metadata, _) = Metadata::find_pda(&input.collection_mint_account); + + let (collection_master_edition, _) = MasterEdition::find_pda(&input.collection_mint_account); + + let accounts = mpl_token_metadata::instructions::VerifyCollectionV1 { + authority: input.collection_authority.pubkey(), + delegate_record: input.delegate_record, + metadata: metadata_account, + collection_mint: input.collection_mint_account, + collection_metadata: Some(collection_metadata), + collection_master_edition: Some(collection_master_edition), + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + }; + + let ins = accounts.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.collection_authority].into(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/v1/verify_creator_v1.rs b/crates/cmds-solana/src/nft/v1/verify_creator_v1.rs new file mode 100644 index 00000000..a390826d --- /dev/null +++ b/crates/cmds-solana/src/nft/v1/verify_creator_v1.rs @@ -0,0 +1,78 @@ +use crate::prelude::*; +use mpl_token_metadata::accounts::{MasterEdition, Metadata}; +use solana_program::{system_program, sysvar}; + +// Command Name +const NAME: &str = "verify_creator_v1"; + +const DEFINITION: &str = flow_lib::node_definition!("nft/v1/verify_creator_v1.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + pub authority: Wallet, + #[serde(default, with = "value::pubkey::opt")] + pub delegate_record: Option, + #[serde(with = "value::pubkey")] + pub metadata: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub collection_mint_account: Option, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut collection_metadata: Option = None; + let mut collection_master_edition: Option = None; + + if let Some(collection_mint_account) = input.collection_mint_account { + let (metadata, _) = Metadata::find_pda(&collection_mint_account); + collection_metadata = Some(metadata); + + let (master_edition, _) = MasterEdition::find_pda(&collection_mint_account); + collection_master_edition = Some(master_edition); + } + + let accounts = mpl_token_metadata::instructions::VerifyCreatorV1 { + authority: input.authority.pubkey(), + delegate_record: input.delegate_record, + metadata: input.metadata, + collection_mint: input.collection_mint_account, + collection_metadata, + collection_master_edition, + system_program: system_program::id(), + sysvar_instructions: sysvar::instructions::id(), + }; + + let ins = accounts.instruction(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: [ins].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/nft/verify_collection.rs b/crates/cmds-solana/src/nft/verify_collection.rs new file mode 100644 index 00000000..fe5cca7f --- /dev/null +++ b/crates/cmds-solana/src/nft/verify_collection.rs @@ -0,0 +1,95 @@ +use crate::prelude::*; + +// Command Name +const NAME: &str = "verify_collection"; + +const DEFINITION: &str = flow_lib::node_definition!("NFT/verify_collection.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + pub fee_payer: Wallet, + pub collection_authority: Wallet, + #[serde(with = "value::pubkey")] + pub collection_mint_account: Pubkey, + pub collection_authority_is_delegated: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (collection_metadata_account, _) = + mpl_token_metadata::pda::find_metadata_account(&input.collection_mint_account); + + let (collection_master_edition_account, _) = + mpl_token_metadata::pda::find_master_edition_account(&input.collection_mint_account); + + let collection_authority_record = if input.collection_authority_is_delegated { + Some( + mpl_token_metadata::pda::find_collection_authority_account( + &input.mint_account, + &input.collection_authority.pubkey(), + ) + .0, + ) + } else { + None + }; + + let (metadata_account, _) = mpl_token_metadata::pda::find_metadata_account(&input.mint_account); + + let minimum_balance_for_rent_exemption = ctx + .solana_client + .get_minimum_balance_for_rent_exemption( + 100, // std::mem::size_of::< + // mpl_token_metadata::state::VerifyCollection, + // >(), + ) + .await?; + + let instructions = vec![ + mpl_token_metadata::instruction::verify_sized_collection_item( + mpl_token_metadata::id(), + metadata_account, + input.collection_authority.pubkey(), + input.fee_payer.pubkey(), + input.collection_mint_account, + collection_metadata_account, + collection_master_edition_account, + collection_authority_record, + ), + ]; + + let ins = Instructions { +lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [ + input.fee_payer, + input.collection_authority, + ] + .into(), + instructions, + minimum_balance_for_rent_exemption, + }; + + let signature: Option = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/pyth_price.rs b/crates/cmds-solana/src/pyth_price.rs new file mode 100644 index 00000000..aef25b7e --- /dev/null +++ b/crates/cmds-solana/src/pyth_price.rs @@ -0,0 +1,53 @@ +use chrono::Utc; +use flow_lib::{command::prelude::*, solana::Pubkey}; +use pyth_sdk_solana::state::SolanaPriceAccount; + +const NAME: &str = "pyth_price"; + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("pyth_price.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input { + #[serde(with = "value::pubkey")] + price_feed_id: Pubkey, +} + +#[derive(Serialize, Debug)] +struct Output { + price: i64, +} + +async fn run(ctx: Context, input: Input) -> Result { + let mut sol_price_account = ctx + .solana_client + .get_account(&input.price_feed_id) + .await + .map_err(|_| CommandError::msg("Failed to get price feed account"))?; + + let sol_price_feed = + SolanaPriceAccount::account_to_feed(&input.price_feed_id, &mut sol_price_account) + .map_err(|_| CommandError::msg("Invalid price feed account"))?; + + let price = sol_price_feed.get_price_unchecked(); + let age = Utc::now().timestamp() - price.publish_time; + tracing::info!("price: {:?}, age: {}s", price, age); + Ok(Output { price: price.price }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/record/initialize_record_with_seed.rs b/crates/cmds-solana/src/record/initialize_record_with_seed.rs new file mode 100644 index 00000000..655a02d8 --- /dev/null +++ b/crates/cmds-solana/src/record/initialize_record_with_seed.rs @@ -0,0 +1,107 @@ +use solana_sdk::{instruction::AccountMeta, rent::Rent, system_instruction}; +use spl_pod::bytemuck::pod_get_packed_len; + +use crate::prelude::*; + +use super::{record_program_id, RecordData, RecordInstruction}; + +const NAME: &str = "initialize_record_with_seed"; + +const DEFINITION: &str = flow_lib::node_definition!("/record/initialize_record_with_seed.json"); + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + authority: Wallet, + seed: String, + data: String, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + #[serde(with = "value::pubkey")] + account: Pubkey, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let record_program_id = record_program_id(ctx.cfg.solana_client.cluster); + + let account = + Pubkey::create_with_seed(&input.authority.pubkey(), &input.seed, &record_program_id) + .unwrap(); + + let data = input.data.as_bytes(); + + let account_length = pod_get_packed_len::() + .checked_add(data.len()) + .unwrap(); + + let create_account_instruction = system_instruction::create_account_with_seed( + &input.fee_payer.pubkey(), + &account, + &input.authority.pubkey(), + &input.seed, + 1.max(Rent::default().minimum_balance(account_length)), + account_length as u64, + &record_program_id, + ); + + let initialize_record_instruction = Instruction { + program_id: record_program_id, + accounts: vec![ + AccountMeta::new(account, false), + AccountMeta::new_readonly(input.authority.pubkey(), false), + ], + data: borsh::to_vec(&RecordInstruction::Initialize).unwrap(), + }; + + let data = RecordInstruction::Write { + offset: 0, + data: data.to_vec(), + }; + + let write_to_record_instruction = Instruction { + program_id: record_program_id, + accounts: vec![ + AccountMeta::new(account, false), + AccountMeta::new_readonly(input.authority.pubkey(), false), + ], + data: borsh::to_vec(&data).unwrap(), + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: [ + create_account_instruction, + initialize_record_instruction, + write_to_record_instruction, + ] + .into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute(ins, value::map! { "account" => account }) + .await? + .signature; + + Ok(Output { signature, account }) +} diff --git a/crates/cmds-solana/src/record/mod.rs b/crates/cmds-solana/src/record/mod.rs new file mode 100644 index 00000000..ccc4d74b --- /dev/null +++ b/crates/cmds-solana/src/record/mod.rs @@ -0,0 +1,101 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::Zeroable; +use flow_lib::SolanaNet; +use serde::Serialize; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::{program_pack::IsInitialized, pubkey}; +use {bytemuck::Pod, solana_sdk::program_error::ProgramError}; + +pub mod initialize_record_with_seed; +pub mod read_record; +pub mod write_to_record; + +// TODO need to find correct mainnet +pub const RECORD_MAINNET: Pubkey = pubkey!("recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5"); +// recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5 +pub const RECORD_DEVNET: Pubkey = pubkey!("6bCYkQ6pfLJMPivh17TV1Bqm3Q7GfkhU56iLtUiPXpK9"); + +pub const fn record_program_id(net: SolanaNet) -> Pubkey { + match net { + SolanaNet::Mainnet => crate::record::RECORD_MAINNET, + // TODO testnet not deployed yet + SolanaNet::Devnet => crate::record::RECORD_DEVNET, + SolanaNet::Testnet => crate::record::RECORD_DEVNET, + } +} + +/// Instructions supported by the program +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] +pub enum RecordInstruction { + /// Create a new record + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be uninitialized + /// 1. `[]` Record authority + Initialize, + + /// Write to the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + Write { + /// Offset to start writing record, expressed as `u64`. + offset: u64, + /// Data to replace the existing record data + data: Vec, + }, + + /// Update the authority of the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + /// 2. `[]` New record authority + SetAuthority, + + /// Close the provided record account, draining lamports to recipient account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Record authority + /// 2. `[]` Receiver of account lamports + CloseAccount, +} + +/// Struct wrapping data and providing metadata +/// #[repr(C)] + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize)] +pub struct RecordData { + /// Struct version, allows for upgrades to the program + pub version: u8, + + /// The account allowed to update the data + pub authority: Pubkey, +} + +impl RecordData { + /// Version to fill in on new created accounts + pub const CURRENT_VERSION: u8 = 1; + + /// Start of writable account data, after version and authority + pub const WRITABLE_START_INDEX: usize = 33; +} + +impl IsInitialized for RecordData { + /// Is initialized + fn is_initialized(&self) -> bool { + self.version == Self::CURRENT_VERSION + } +} + +/// Convert a slice of bytes into a `Pod` (zero copy) +pub fn pod_from_bytes(bytes: &[u8]) -> Result<&T, ProgramError> { + bytemuck::try_from_bytes(bytes).map_err(|_| ProgramError::InvalidArgument) +} diff --git a/crates/cmds-solana/src/record/read_record.rs b/crates/cmds-solana/src/record/read_record.rs new file mode 100644 index 00000000..f0dd484c --- /dev/null +++ b/crates/cmds-solana/src/record/read_record.rs @@ -0,0 +1,48 @@ +use crate::prelude::*; + +use super::{pod_from_bytes, RecordData}; + +pub const NAME: &str = "read_record"; + +const DEFINITION: &str = flow_lib::node_definition!("/record/read_record.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + #[serde(with = "value::pubkey")] + account: Pubkey, +} + +#[derive(Serialize)] +struct Output { + #[serde(with = "value::pubkey")] + authority: Pubkey, + version: u8, + data: String, +} + +async fn run(ctx: Context, input: Input) -> Result { + let resp = ctx.solana_client.get_account(&input.account).await?; + + let account_data = pod_from_bytes::(&resp.data[..RecordData::WRITABLE_START_INDEX]) + .map_err(|_| { + crate::error::Error::Any(anyhow::anyhow!( + "Error: Invalid account data: {}", + input.account + )) + })?; + + Ok(Output { + authority: account_data.authority, + version: account_data.version, + data: String::from_utf8(resp.data[RecordData::WRITABLE_START_INDEX..].to_vec())?, + }) +} diff --git a/crates/cmds-solana/src/record/write_to_record.rs b/crates/cmds-solana/src/record/write_to_record.rs new file mode 100644 index 00000000..50d91b5a --- /dev/null +++ b/crates/cmds-solana/src/record/write_to_record.rs @@ -0,0 +1,71 @@ +use solana_sdk::instruction::AccountMeta; + +use crate::prelude::*; + +use super::{record_program_id, RecordInstruction}; + +const NAME: &str = "write_to_record"; + +const DEFINITION: &str = flow_lib::node_definition!("/record/write_to_record.json"); + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + authority: Wallet, + seed: String, + offset: u64, + data: String, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let record_program_id = record_program_id(ctx.cfg.solana_client.cluster); + let record_account = + Pubkey::create_with_seed(&input.authority.pubkey(), &input.seed, &record_program_id) + .unwrap(); + + let data = RecordInstruction::Write { + offset: input.offset, + data: input.data.into(), + }; + + let write_to_record_instruction = Instruction { + program_id: record_program_id, + accounts: vec![ + AccountMeta::new(record_account, false), + AccountMeta::new_readonly(input.authority.pubkey(), false), + ], + data: borsh::to_vec(&data).unwrap(), + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: [write_to_record_instruction].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/request_airdrop.rs b/crates/cmds-solana/src/request_airdrop.rs new file mode 100644 index 00000000..54154013 --- /dev/null +++ b/crates/cmds-solana/src/request_airdrop.rs @@ -0,0 +1,57 @@ +use crate::prelude::*; + +const NAME: &str = "request_airdrop"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("request_airdrop.json"); + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +fn default_amount() -> u64 { + 1000000000 +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::pubkey")] + pub pubkey: Pubkey, + #[serde(default = "default_amount")] + pub amount: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::signature")] + pub signature: Signature, +} + +async fn run(ctx: Context, input: Input) -> Result { + let signature = ctx + .solana_client + .request_airdrop(&input.pubkey, input.amount) + .await?; + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + async fn test_valid() { + let pubkey = solana_sdk::pubkey!("DKsvmM9hfNm4R94yB3VdYMZJk2ETv5hpcjuRmiwgiztY"); + let amount: u64 = 1_500_000_000; + + let result = run(Context::default(), Input { amount, pubkey }).await; + dbg!(&result); + } +} diff --git a/crates/cmds-solana/src/spl/mod.rs b/crates/cmds-solana/src/spl/mod.rs new file mode 100644 index 00000000..5e279f2e --- /dev/null +++ b/crates/cmds-solana/src/spl/mod.rs @@ -0,0 +1 @@ +pub mod set_authority; diff --git a/crates/cmds-solana/src/spl/set_authority.rs b/crates/cmds-solana/src/spl/set_authority.rs new file mode 100644 index 00000000..d3b426fd --- /dev/null +++ b/crates/cmds-solana/src/spl/set_authority.rs @@ -0,0 +1,82 @@ +use crate::prelude::*; + +const NAME: &str = "set_authority"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/spl_token/set_authority.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub owned_pubkey: Pubkey, + #[serde(with = "value::pubkey::opt")] + pub new_authority: Option, + pub authority_type: AuthorityType, + #[serde(with = "value::pubkey")] + pub owner_pubkey: Pubkey, + pub signer_pubkeys: Vec, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let ix = spl_token::instruction::set_authority( + &spl_token::id(), + &input.owned_pubkey, + input.new_authority.as_ref(), + input.authority_type.into(), + &input.owner_pubkey, + &input.signer_pubkeys.iter().collect::>(), + )?; + + // TODO if signers not empty, add signers as signer + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum AuthorityType { + /// Authority to mint new tokens + MintTokens, + /// Authority to freeze any account associated with the Mint + FreezeAccount, + /// Owner of a given token account + AccountOwner, + /// Authority to close a token account + CloseAccount, +} + +impl From for spl_token::instruction::AuthorityType { + fn from(value: AuthorityType) -> Self { + match value { + AuthorityType::MintTokens => Self::MintTokens, + AuthorityType::FreezeAccount => Self::FreezeAccount, + AuthorityType::AccountOwner => Self::AccountOwner, + AuthorityType::CloseAccount => Self::CloseAccount, + } + } +} diff --git a/crates/cmds-solana/src/spl_token_2022/mod.rs b/crates/cmds-solana/src/spl_token_2022/mod.rs new file mode 100644 index 00000000..5e279f2e --- /dev/null +++ b/crates/cmds-solana/src/spl_token_2022/mod.rs @@ -0,0 +1 @@ +pub mod set_authority; diff --git a/crates/cmds-solana/src/spl_token_2022/set_authority.rs b/crates/cmds-solana/src/spl_token_2022/set_authority.rs new file mode 100644 index 00000000..60240012 --- /dev/null +++ b/crates/cmds-solana/src/spl_token_2022/set_authority.rs @@ -0,0 +1,61 @@ +use spl_token_2022::instruction::AuthorityType; + +use crate::prelude::*; + +const NAME: &str = "set_authority_2022"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/spl_token_2022/set_authority.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub owned_pubkey: Pubkey, + #[serde(with = "value::pubkey::opt")] + pub new_authority: Option, + pub authority_type: AuthorityType, + #[serde(with = "value::pubkey")] + pub owner_pubkey: Pubkey, + pub signer_pubkeys: Vec, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let ix = spl_token_2022::instruction::set_authority( + &spl_token_2022::id(), + &input.owned_pubkey, + input.new_authority.as_ref(), + input.authority_type, + &input.owner_pubkey, + &input.signer_pubkeys.iter().collect::>(), + )?; + + // TODO if signers not empty, add signers as signer + + let instructions = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer].into(), + instructions: [ix].into(), + }; + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/streamflow/create.rs b/crates/cmds-solana/src/streamflow/create.rs new file mode 100644 index 00000000..1c6d3f5a --- /dev/null +++ b/crates/cmds-solana/src/streamflow/create.rs @@ -0,0 +1,172 @@ +use std::str::FromStr; + +use crate::prelude::*; +use crate::utils::anchor_sighash; +use borsh::BorshSerialize; +use solana_program::instruction::AccountMeta; +use solana_program::{system_program, sysvar}; +use spl_associated_token_account::get_associated_token_address; + +use super::{CreateData, CreateDataInput, FEE_ORACLE_ADDRESS, STRM_TREASURY, WITHDRAWOR_ADDRESS}; + +const NAME: &str = "create_streamflow_timelock"; + +const DEFINITION: &str = flow_lib::node_definition!("streamflow/create.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + sender: Wallet, + #[serde(with = "value::pubkey")] + recipient: Pubkey, + metadata: Wallet, + #[serde(with = "value::pubkey")] + mint_account: Pubkey, + data: CreateDataInput, + #[serde(default, with = "value::pubkey::opt")] + partner: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +fn create_create_stream_instruction( + sender: &Pubkey, + sender_tokens: &Pubkey, + recipient: &Pubkey, + recipient_tokens: &Pubkey, + metadata: &Pubkey, + escrow_tokens: &Pubkey, + streamflow_treasury_tokens: &Pubkey, + partner: &Pubkey, + partner_tokens: &Pubkey, + mint: &Pubkey, + timelock_program: &Pubkey, + data: CreateData, +) -> Instruction { + let accounts = [ + AccountMeta::new(*sender, true), + AccountMeta::new(*sender_tokens, false), + AccountMeta::new(*recipient, false), + AccountMeta::new(*metadata, true), + AccountMeta::new(*escrow_tokens, false), + AccountMeta::new(*recipient_tokens, false), + AccountMeta::new(Pubkey::from_str(STRM_TREASURY).unwrap(), false), + AccountMeta::new(*streamflow_treasury_tokens, false), + AccountMeta::new(Pubkey::from_str(WITHDRAWOR_ADDRESS).unwrap(), false), + AccountMeta::new(*partner, false), + AccountMeta::new(*partner_tokens, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(Pubkey::from_str(FEE_ORACLE_ADDRESS).unwrap(), false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + AccountMeta::new_readonly(*timelock_program, false), + AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new_readonly(spl_associated_token_account::ID, false), + AccountMeta::new_readonly(system_program::ID, false), + ] + .to_vec(); + + let discriminator = anchor_sighash("create"); + + Instruction { + program_id: *timelock_program, + accounts, + data: (discriminator, data).try_to_vec().unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let timelock_program = crate::streamflow::streamflow_program_id(ctx.cfg.solana_client.cluster); + + let data: CreateData = input.data.into(); + + let escrow_tokens = Pubkey::find_program_address( + &[b"strm", input.metadata.pubkey().as_ref()], + &timelock_program, + ) + .0; + + let sender_tokens: Pubkey = + get_associated_token_address(&input.sender.pubkey(), &input.mint_account); + + let recipient_tokens = get_associated_token_address(&input.recipient, &input.mint_account); + + let streamflow_treasury_tokens = get_associated_token_address( + &Pubkey::from_str(STRM_TREASURY).unwrap(), + &input.mint_account, + ); + + let partner = match &input.partner { + Some(partner) => *partner, + None => Pubkey::from_str(STRM_TREASURY).unwrap(), + }; + + let partner_tokens = get_associated_token_address(&partner, &input.mint_account); + + let instruction = create_create_stream_instruction( + &input.fee_payer.pubkey(), + &sender_tokens, + &input.recipient, + &recipient_tokens, + &input.metadata.pubkey(), + &escrow_tokens, + &streamflow_treasury_tokens, + &partner, + &partner_tokens, + &input.mint_account, + &timelock_program, + data, + ); + + let metadata_pubkey = input.metadata.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.sender, input.metadata].into(), + instructions: vec![instruction], + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "escrow_tokens" => escrow_tokens, + "sender_tokens" => sender_tokens, + "recipient_tokens" => recipient_tokens, + "metadata" => metadata_pubkey, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/streamflow/mod.rs b/crates/cmds-solana/src/streamflow/mod.rs new file mode 100644 index 00000000..07098fe9 --- /dev/null +++ b/crates/cmds-solana/src/streamflow/mod.rs @@ -0,0 +1,232 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use flow_lib::SolanaNet; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; +use solana_sdk::pubkey; + +pub mod create; +pub mod withdraw; + +pub const STREAMFLOW_PROGRAM_ID: Pubkey = pubkey!("strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m"); +pub const STREAMFLOW_DEVNET_PROGRAM_ID: Pubkey = + pubkey!("HqDGZjaVRXJ9MGRQEw7qDc2rAr6iH1n1kAQdCZaCMfMZ"); + +pub const fn streamflow_program_id(net: SolanaNet) -> Pubkey { + match net { + SolanaNet::Mainnet => crate::streamflow::STREAMFLOW_PROGRAM_ID, + // TODO testnet not deployed yet + SolanaNet::Testnet => crate::streamflow::STREAMFLOW_DEVNET_PROGRAM_ID, + SolanaNet::Devnet => crate::streamflow::STREAMFLOW_DEVNET_PROGRAM_ID, + } +} + +// TODO: declare static Pubkeys instead of strings +/// Streamflow Treasury address, by default receives 0.25% of tokens deposited +pub const STRM_TREASURY: &str = "5SEpbdjFK5FxwTvfsGMXVQTD2v4M2c5tyRTxhdsPkgDw"; +/// Streamflow Withdrawor address, this account will process withdrawals +pub const WITHDRAWOR_ADDRESS: &str = "wdrwhnCv4pzW8beKsbPa4S2UDZrXenjg16KJdKSpb5u"; +/// Address of Fee Oracle that stores information about fees for speficic partners +pub const FEE_ORACLE_ADDRESS: &str = "B743wFVk2pCYhV91cn287e1xY7f1vt4gdY48hhNiuQmT"; + +/// Prefix used to derive Escrow account address +pub const ESCROW_SEED_PREFIX: &[u8] = b"strm"; + +/// Size of Stream metadata +pub const METADATA_LEN: usize = 1104; + +pub fn find_escrow_account(seed: &[u8], pid: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[ESCROW_SEED_PREFIX, seed], pid) +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +#[repr(C)] +pub struct StreamContract { + /// Magic bytes + pub magic: u64, + /// Version of the program + pub version: u8, + /// Timestamp when stream was created + pub created_at: u64, + /// Amount of funds withdrawn + pub amount_withdrawn: u64, + /// Timestamp when stream was canceled (if canceled) + pub canceled_at: u64, + /// Timestamp at which stream can be safely canceled by a 3rd party + /// (Stream is either fully vested or there isn't enough capital to + /// keep it active) + pub end_time: u64, + /// Timestamp of the last withdrawal + pub last_withdrawn_at: u64, + /// Pubkey of the stream initializer + pub sender: Pubkey, + /// Pubkey of the stream initializer's token account + pub sender_tokens: Pubkey, + /// Pubkey of the stream recipient + pub recipient: Pubkey, + /// Pubkey of the stream recipient's token account + pub recipient_tokens: Pubkey, + /// Pubkey of the token mint + pub mint: Pubkey, + /// Escrow account holding the locked tokens for recipient + pub escrow_tokens: Pubkey, + /// Streamflow treasury authority + pub streamflow_treasury: Pubkey, + /// Escrow account holding the locked tokens for Streamflow (fee account) + pub streamflow_treasury_tokens: Pubkey, + /// The total fee amount for streamflow + pub streamflow_fee_total: u64, + /// The withdrawn fee amount for streamflow + pub streamflow_fee_withdrawn: u64, + /// Fee percentage for Streamflow + pub streamflow_fee_percent: f32, + /// Streamflow partner authority + pub partner: Pubkey, + /// Escrow account holding the locked tokens for Streamflow partner (fee account) + pub partner_tokens: Pubkey, + /// The total fee amount for the partner + pub partner_fee_total: u64, + /// The withdrawn fee amount for the partner + pub partner_fee_withdrawn: u64, + /// Fee percentage for partner + pub partner_fee_percent: f32, + /// The stream instruction + pub ix: CreateParams, + /// Padding for `ix: CreateParams` to allow for future upgrades. + pub ix_padding: Vec, + // Stream is closed + pub closed: bool, + /// time of the current pause. 0 signifies unpaused state + pub current_pause_start: u64, + /// total time the contract was paused for + pub pause_cumulative: u64, + /// timestamp of last rate change for this stream. + /// Rate can be changed with `update` instruction + pub last_rate_change_time: u64, + /// Accumulated unlocked tokens before last rate change (excluding cliff_amount) + pub funds_unlocked_at_last_rate_change: u64, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug)] +#[repr(C)] +pub struct CreateParams { + /// Timestamp when the tokens start vesting + pub start_time: u64, + /// Deposited amount of tokens + pub net_amount_deposited: u64, + /// Time step (period) in seconds per which the vesting/release occurs + pub period: u64, + /// Amount released per period. Combined with `period`, we get a release rate. + pub amount_per_period: u64, + /// Vesting contract "cliff" timestamp + pub cliff: u64, + /// Amount unlocked at the "cliff" timestamp + pub cliff_amount: u64, + /// Whether or not a stream can be canceled by a sender + pub cancelable_by_sender: bool, + /// Whether or not a stream can be canceled by a recipient + pub cancelable_by_recipient: bool, + /// Whether or not a 3rd party can initiate withdraw in the name of recipient + pub automatic_withdrawal: bool, + /// Whether or not the sender can transfer the stream + pub transferable_by_sender: bool, + /// Whether or not the recipient can transfer the stream + pub transferable_by_recipient: bool, + /// Whether topup is enabled + pub can_topup: bool, + /// The name of this stream + pub stream_name: [u8; 64], + /// Withdraw frequency + pub withdraw_frequency: u64, + /// used as padding len in serialization in old streams, added for backwards compatibility + pub ghost: u32, + /// Whether or not the contract can be paused + pub pausable: bool, + /// Whether or not the contract can update release amount + pub can_update_rate: bool, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateData { + start_time: u64, + net_amount_deposited: u64, + period: u64, + amount_per_period: u64, + cliff: u64, + cliff_amount: u64, + cancelable_by_sender: bool, + cancelable_by_recipient: bool, + automatic_withdrawal: bool, + transferable_by_sender: bool, + transferable_by_recipient: bool, + can_topup: bool, + stream_name: [u8; 64], + withdraw_frequency: u64, + pausable: Option, + can_update_rate: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CreateDataInput { + start_time: u64, + net_amount_deposited: u64, + period: u64, + amount_per_period: u64, + cliff: u64, + cliff_amount: u64, + cancelable_by_sender: bool, + cancelable_by_recipient: bool, + automatic_withdrawal: bool, + transferable_by_sender: bool, + transferable_by_recipient: bool, + can_topup: bool, + stream_name: String, + withdraw_frequency: u64, + pausable: Option, + can_update_rate: Option, +} + +impl From for CreateData { + fn from(input: CreateDataInput) -> Self { + let mut array = [0; 64]; + let bytes = input.stream_name.as_bytes(); + + array[..bytes.len()].copy_from_slice(bytes); + + CreateData { + start_time: input.start_time, + net_amount_deposited: input.net_amount_deposited, + period: input.period, + amount_per_period: input.amount_per_period, + cliff: input.cliff, + cliff_amount: input.cliff_amount, + cancelable_by_sender: input.cancelable_by_sender, + cancelable_by_recipient: input.cancelable_by_recipient, + automatic_withdrawal: input.automatic_withdrawal, + transferable_by_sender: input.transferable_by_sender, + transferable_by_recipient: input.transferable_by_recipient, + can_topup: input.can_topup, + stream_name: array, + withdraw_frequency: input.withdraw_frequency, + pausable: input.pausable, + can_update_rate: input.can_update_rate, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WithdrawDataInput { + amount: u64, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct WithdrawData { + amount: u64, +} + +impl From for WithdrawData { + fn from(input: WithdrawDataInput) -> Self { + WithdrawData { + amount: input.amount, + } + } +} diff --git a/crates/cmds-solana/src/streamflow/withdraw.rs b/crates/cmds-solana/src/streamflow/withdraw.rs new file mode 100644 index 00000000..7de57707 --- /dev/null +++ b/crates/cmds-solana/src/streamflow/withdraw.rs @@ -0,0 +1,176 @@ +use std::str::FromStr; + +use crate::prelude::*; +use crate::streamflow::StreamContract; +use crate::utils::anchor_sighash; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_account_decoder::UiAccountEncoding; +use solana_client::rpc_config::RpcAccountInfoConfig; +use solana_program::instruction::AccountMeta; +use solana_sdk::commitment_config::CommitmentConfig; +use spl_associated_token_account::get_associated_token_address; +use tracing::info; + +use super::{WithdrawData, WithdrawDataInput, STRM_TREASURY}; + +const NAME: &str = "withdraw_streamflow_timelock"; + +const DEFINITION: &str = flow_lib::node_definition!("streamflow/withdraw.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + fee_payer: Wallet, + authority: Wallet, + #[serde(with = "value::pubkey")] + recipient: Pubkey, + #[serde(with = "value::pubkey")] + metadata: Pubkey, + data: WithdrawDataInput, + #[serde(default, with = "value::pubkey::opt")] + partner: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +fn create_withdraw_stream_instruction( + authority: &Pubkey, + recipient: &Pubkey, + recipient_tokens: &Pubkey, + metadata: &Pubkey, + escrow_tokens: &Pubkey, + streamflow_treasury_tokens: &Pubkey, + partner: &Pubkey, + partner_tokens: &Pubkey, + mint: &Pubkey, + timelock_program: &Pubkey, + data: WithdrawData, +) -> Instruction { + let accounts = [ + AccountMeta::new(*authority, true), + AccountMeta::new(*recipient, false), + AccountMeta::new(*recipient_tokens, false), + AccountMeta::new(*metadata, false), + AccountMeta::new(*escrow_tokens, false), + AccountMeta::new(Pubkey::from_str(STRM_TREASURY).unwrap(), false), + AccountMeta::new(*streamflow_treasury_tokens, false), + AccountMeta::new(*partner, false), + AccountMeta::new(*partner_tokens, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(spl_token::ID, false), + ] + .to_vec(); + + let discriminator = anchor_sighash("withdraw"); + + Instruction { + program_id: *timelock_program, + accounts, + data: (discriminator, data).try_to_vec().unwrap(), + } +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let timelock_program = crate::streamflow::streamflow_program_id(ctx.cfg.solana_client.cluster); + + let data: WithdrawData = input.data.into(); + + let commitment = CommitmentConfig::confirmed(); + + let config = RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + commitment: Some(commitment), + data_slice: None, + min_context_slot: None, + }; + + let response = ctx + .solana_client + .get_account_with_config(&input.metadata, config) + .await + .map_err(|e| { + tracing::error!("Error: {:?}", e); + crate::Error::AccountNotFound(input.metadata) + })?; + + let escrow = match response.value { + Some(account) => account, + None => return Err(crate::Error::AccountNotFound(input.metadata).into()), + }; + + let mut escrow_data: &[u8] = &escrow.data; + let escrow_data = StreamContract::deserialize(&mut escrow_data).map_err(|_| { + tracing::error!( + "Invalid data for: {:?}", + crate::Error::InvalidAccountData(input.metadata) + ); + crate::Error::InvalidAccountData(input.metadata) + })?; + + info!("Escrow account: {:?}", escrow_data); + + let recipient_tokens = get_associated_token_address(&escrow_data.recipient, &escrow_data.mint); + + let streamflow_treasury_tokens = + get_associated_token_address(&Pubkey::from_str(STRM_TREASURY).unwrap(), &escrow_data.mint); + + let partner = match &input.partner { + Some(partner) => *partner, + None => Pubkey::from_str(STRM_TREASURY).unwrap(), + }; + + let partner_tokens = get_associated_token_address(&partner, &escrow_data.mint); + + let instruction = create_withdraw_stream_instruction( + &input.fee_payer.pubkey(), + &escrow_data.recipient, + &recipient_tokens, + &input.metadata, + &escrow_data.escrow_tokens, + &streamflow_treasury_tokens, + &partner, + &partner_tokens, + &escrow_data.mint, + &timelock_program, + data, + ); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.authority].into(), + instructions: vec![instruction], + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/transfer_sol.rs b/crates/cmds-solana/src/transfer_sol.rs new file mode 100644 index 00000000..b59255cd --- /dev/null +++ b/crates/cmds-solana/src/transfer_sol.rs @@ -0,0 +1,114 @@ +use crate::{prelude::*, utils::sol_to_lamports}; + +const NAME: &str = "transfer_sol"; + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("transfer_sol.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +pub struct Input { + pub fee_payer: Option, + pub sender: Wallet, + #[serde_as(as = "AsPubkey")] + pub recipient: Pubkey, + #[serde_as(as = "AsDecimal")] + pub amount: Decimal, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +pub struct Output { + #[serde_as(as = "Option")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let amount = sol_to_lamports(input.amount)?; + + let instruction = + solana_sdk::system_instruction::transfer(&input.sender.pubkey(), &input.recipient, amount); + + let sender_pubkey = input.sender.pubkey(); + let mut signers = vec![input.sender]; + if let Some(fee_payer) = input.fee_payer { + if fee_payer.pubkey() != sender_pubkey { + signers.insert(0, fee_payer); + } + } + let fee_payer = signers[0].pubkey(); + let instructions = if input.submit { + Instructions { + lookup_tables: None, + fee_payer, + signers, + instructions: [instruction].into(), + } + } else { + Instructions::default() + }; + + let signature = ctx.execute(instructions, <_>::default()).await?.signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_valid() { + tracing_subscriber::fmt::try_init().ok(); + let ctx = Context::default(); + + let sender: Wallet = Keypair::from_base58_string("4rQanLxTFvdgtLsGirizXejgYXACawB5ShoZgvz4wwXi4jnii7XHSyUFJbvAk4ojRiEAHvzK6Qnjq7UyJFNbydeQ").into(); + let recipient = solana_sdk::pubkey!("GQZRKDqVzM4DXGGMEUNdnBD3CC4TTywh3PwgjYPBm8W9"); + + let balance = ctx + .solana_client + .get_balance(&sender.pubkey()) + .await + .unwrap() as f64 + / 1_000_000_000.0; + + if balance < 0.1 { + let _ = ctx + .solana_client + .request_airdrop(&sender.pubkey(), 1_000_000_000) + .await; + } + + // Transfer + let output = run( + ctx, + Input { + fee_payer: None, + sender, + recipient, + amount: rust_decimal_macros::dec!(0.1), + submit: true, + }, + ) + .await + .unwrap(); + dbg!(output.signature.unwrap()); + } +} diff --git a/crates/cmds-solana/src/transfer_token.rs b/crates/cmds-solana/src/transfer_token.rs new file mode 100644 index 00000000..7e440e67 --- /dev/null +++ b/crates/cmds-solana/src/transfer_token.rs @@ -0,0 +1,234 @@ +use crate::get_decimals; +use crate::{prelude::*, utils::ui_amount_to_amount}; +use solana_program::system_program; +use solana_sdk::commitment_config::CommitmentConfig; +use solana_sdk::instruction::Instruction; +use solana_sdk::program_pack::Pack; +use spl_associated_token_account::instruction; +use spl_token::instruction::transfer_checked; +use tracing::info; + +const SOLANA_TRANSFER_TOKEN: &str = "transfer_token"; + +const DEFINITION: &str = flow_lib::node_definition!("transfer_token.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(SOLANA_TRANSFER_TOKEN)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(SOLANA_TRANSFER_TOKEN, |_| build())); + +#[allow(clippy::too_many_arguments)] +async fn command_transfer_token( + client: &RpcClient, + fee_payer: &Pubkey, + token_mint: Pubkey, + ui_amount: Decimal, + decimals: Option, + recipient: Pubkey, + sender: Option, + sender_owner: Pubkey, + allow_unfunded_recipient: bool, + fund_recipient: bool, + memo: String, +) -> crate::Result<(Vec, Pubkey)> { + let sender_token_acc = if let Some(sender) = sender { + sender + } else { + spl_associated_token_account::get_associated_token_address(&sender_owner, &token_mint) + }; + + let decimals = if let Some(d) = decimals { + d + } else { + get_decimals(client, token_mint).await? + }; + + let commitment = CommitmentConfig::confirmed(); + + let transfer_balance = { + // TODO error handling + let sender_token_amount = client + .get_token_account_balance_with_commitment(&sender_token_acc, commitment) + .await? + .value; + + info!("sender_token_amount: {:?}", sender_token_amount); + + // TODO error handling + let sender_balance = sender_token_amount + .amount + .parse::() + .map_err(crate::Error::custom)?; + + let transfer_balance = ui_amount_to_amount(ui_amount, decimals)?; + if transfer_balance > sender_balance { + // TODO: discuss if this error appropriate for token semantically? + return Err(crate::Error::InsufficientSolanaBalance { + needed: transfer_balance, + balance: sender_balance, + }); + } + transfer_balance + }; + + let mut recipient_token_account = recipient; + + let recipient_is_token_account = { + let recipient_account_info = client + .get_account_with_commitment(&recipient, commitment) + .await? + .value + .map(|account| { + account.owner == spl_token::id() + && account.data.len() == spl_token::state::Account::LEN + }); + + if recipient_account_info.is_none() && !allow_unfunded_recipient { + return Err(crate::Error::RecipientAddressNotFunded); + } + recipient_account_info.unwrap_or(false) + }; + + info!( + "recipient_is_token_account: {:?}", + recipient_is_token_account + ); + + let mut instructions = vec![]; + if !recipient_is_token_account { + recipient_token_account = + spl_associated_token_account::get_associated_token_address(&recipient, &token_mint); + + let needs_funding = { + if let Some(recipient_token_account_data) = client + .get_account_with_commitment(&recipient_token_account, commitment) + .await? + .value + { + match recipient_token_account_data.owner { + x if x == system_program::ID => true, + y if y == spl_token::ID => false, + _ => { + return Err(crate::Error::UnsupportedRecipientAddress( + recipient.to_string(), + )) + } + } + } else { + true + } + }; + + if needs_funding { + if fund_recipient { + instructions.push(instruction::create_associated_token_account( + fee_payer, + &recipient, + &token_mint, + &spl_token::ID, + )); + } else { + // TODO: discuss the logic of this error + return Err(crate::Error::AssociatedTokenAccountDoesntExist); + } + } + } + + instructions.push(transfer_checked( + &spl_token::ID, + &sender_token_acc, + &token_mint, + &recipient_token_account, + &sender_owner, + &[&sender_owner, fee_payer], + transfer_balance, + decimals, + )?); + + instructions.push(spl_memo::build_memo(memo.as_bytes(), &[fee_payer])); + + Ok((instructions, recipient_token_account)) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde(with = "value::pubkey")] + pub mint_account: Pubkey, + #[serde(default)] + pub memo: String, + #[serde(with = "value::decimal")] + pub amount: Decimal, + pub decimals: Option, + #[serde(with = "value::pubkey")] + pub recipient: Pubkey, + #[serde(default, with = "value::pubkey::opt")] + pub sender_token_account: Option, + pub sender_owner: Wallet, + #[serde(default = "value::default::bool_true")] + pub allow_unfunded: bool, + #[serde(default = "value::default::bool_true")] + pub fund_recipient: bool, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + pub signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let (instructions, recipient_token_account) = command_transfer_token( + &ctx.solana_client, + &input.fee_payer.pubkey(), + input.mint_account, + input.amount, + input.decimals, + input.recipient, + input.sender_token_account, + input.sender_owner.pubkey(), + input.allow_unfunded, + input.fund_recipient, + input.memo, + ) + .await?; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.fee_payer.pubkey(), + signers: [input.fee_payer, input.sender_owner].into(), + instructions, + }; + + let instructions = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + instructions, + value::map! { + "recipient_token_account" => recipient_token_account, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-solana/src/utils.rs b/crates/cmds-solana/src/utils.rs new file mode 100644 index 00000000..24293301 --- /dev/null +++ b/crates/cmds-solana/src/utils.rs @@ -0,0 +1,92 @@ +use crate::prelude::*; +use bytes::Bytes; +use rust_decimal::{ + prelude::{MathematicalOps, ToPrimitive}, + Decimal, +}; +use solana_program::{ + hash::Hash, instruction::Instruction, message::Message, native_token::LAMPORTS_PER_SOL, +}; +use solana_sdk::{signature::Presigner, transaction::Transaction}; +use std::time::Duration; +use value::Error as ValueError; + +pub const SIGNATURE_TIMEOUT: Duration = Duration::from_secs(60 * 5); + +pub async fn execute( + client: &RpcClient, + fee_payer: &Pubkey, + instructions: &[Instruction], +) -> crate::Result<(Transaction, Hash)> { + let recent_blockhash = client.get_latest_blockhash().await?; + + let message = Message::new_with_blockhash(instructions, Some(fee_payer), &recent_blockhash); + + let transaction = Transaction::new_unsigned(message); + + Ok((transaction, recent_blockhash)) +} + +pub async fn submit_transaction(client: &RpcClient, tx: Transaction) -> crate::Result { + Ok(client.send_and_confirm_transaction(&tx).await?) +} + +pub fn sol_to_lamports(amount: Decimal) -> crate::Result { + if amount < Decimal::ZERO { + return Err(ValueError::Custom("amount is negative".into()).into()); + } + amount + .checked_mul(Decimal::from(LAMPORTS_PER_SOL)) + .and_then(|d| d.floor().to_u64()) + .ok_or_else(|| ValueError::Custom("value overflow".into()).into()) +} + +/// Convert the UI representation of a token amount (using the decimals field defined in its mint) +/// to the raw amount. +pub fn ui_amount_to_amount(ui_amount: Decimal, decimals: u8) -> crate::Result { + if ui_amount < Decimal::ZERO { + return Err(ValueError::Custom("amount is negative".to_owned()).into()); + } + ui_amount + .checked_mul(Decimal::TEN.powu(decimals as u64)) + .and_then(|d| d.floor().to_u64()) + .ok_or_else(|| ValueError::Custom("amount overflow".to_owned()).into()) +} + +pub fn tx_to_string(tx: &Transaction) -> Result { + Ok(base64::encode(bincode::serialize(tx)?)) +} + +pub async fn try_sign_wallet( + ctx: &Context, + tx: &mut Transaction, + wallet: &Wallet, + recent_blockhash: Hash, +) -> Result<(), crate::Error> { + if let Some(keypair) = wallet.keypair() { + tx.try_sign(&[keypair], recent_blockhash)?; + } else { + let msg: Bytes = tx.message_data().into(); + let sig = tokio::time::timeout( + SIGNATURE_TIMEOUT, + ctx.request_signature(wallet.pubkey(), msg.clone(), SIGNATURE_TIMEOUT), + ) + .await + .map_err(|_| crate::Error::SignatureTimeout)??; + let presigner = Presigner::new(&wallet.pubkey(), &sig.signature); + tx.try_sign(&[&presigner], recent_blockhash)?; + } + + Ok(()) +} + +// +pub fn anchor_sighash(name: &str) -> [u8; 8] { + let namespace = "global"; + let preimage = format!("{}:{}", namespace, name); + let mut sighash = [0u8; 8]; + sighash.copy_from_slice( + &anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8], + ); + sighash +} diff --git a/crates/cmds-solana/src/utils/bundlr_signer.rs b/crates/cmds-solana/src/utils/bundlr_signer.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/cmds-solana/src/utils/bundlr_signer.rs @@ -0,0 +1 @@ + diff --git a/crates/cmds-solana/src/wallet.rs b/crates/cmds-solana/src/wallet.rs new file mode 100644 index 00000000..43fdd6c7 --- /dev/null +++ b/crates/cmds-solana/src/wallet.rs @@ -0,0 +1,123 @@ +use crate::prelude::*; +use flow_lib::config::client::NodeData; +use thiserror::Error as ThisError; + +#[derive(Debug)] +pub struct WalletCmd { + form: Result, +} + +#[derive(Deserialize)] +struct FormData { + public_key: String, +} + +#[derive(ThisError, Debug)] +enum WalletError { + #[error("failed to decode wallet as base58")] + InvalidBase58, + #[error(transparent)] + Form(serde_json::Error), +} + +fn adapter_wallet(pubkey: Pubkey) -> Output { + Output { + pubkey, + keypair: Wallet::Adapter { public_key: pubkey }, + } +} + +impl FormData { + fn into_output(self) -> Result { + let pubkey = self + .public_key + .parse::() + .map_err(|_| WalletError::InvalidBase58)?; + Ok(adapter_wallet(pubkey)) + } +} + +impl WalletCmd { + fn new(nd: &NodeData) -> Self { + let form = serde_json::from_value::(nd.targets_form.form_data.clone()) + .map_err(WalletError::Form) + .and_then(FormData::into_output); + Self { form } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(with = "value::pubkey")] + pub pubkey: Pubkey, + pub keypair: Wallet, +} + +const WALLET: &str = "wallet"; + +#[async_trait] +impl CommandTrait for WalletCmd { + fn name(&self) -> Name { + WALLET.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + [ + CmdOutput { + name: "pubkey".into(), + r#type: ValueType::Pubkey, + optional: false, + }, + CmdOutput { + name: "keypair".into(), + r#type: ValueType::Keypair, + optional: false, + }, + ] + .to_vec() + } + + async fn run(&self, _: Context, _: ValueSet) -> Result { + match &self.form { + Ok(output) => Ok(value::to_map(output)?), + Err(e) => Err(CommandError::msg(e.to_string())), + } + } +} + +flow_lib::submit!(CommandDescription::new(WALLET, |nd| { + Ok(Box::new(WalletCmd::new(nd))) +})); + +#[cfg(test)] +mod tests { + use super::*; + use flow_lib::config::client::{Extra, TargetsForm}; + use serde_json::json; + + const PUBKEY: Pubkey = solana_sdk::pubkey!("DKsvmM9hfNm4R94yB3VdYMZJk2ETv5hpcjuRmiwgiztY"); + const PUBKEY_STR: &str = "DKsvmM9hfNm4R94yB3VdYMZJk2ETv5hpcjuRmiwgiztY"; + + #[test] + fn adapter() { + let nd = NodeData { + r#type: flow_lib::CommandType::Native, + node_id: WALLET.into(), + sources: Vec::new(), + targets: Vec::new(), + targets_form: TargetsForm { + form_data: json!({ + "public_key": PUBKEY_STR, + }), + extra: Extra::default(), + wasm_bytes: None, + }, + instruction_info: None, + }; + assert_eq!(WalletCmd::new(&nd).form.unwrap().pubkey, PUBKEY); + } +} diff --git a/crates/cmds-solana/src/wormhole/get_vaa.rs b/crates/cmds-solana/src/wormhole/get_vaa.rs new file mode 100644 index 00000000..5d47a05c --- /dev/null +++ b/crates/cmds-solana/src/wormhole/get_vaa.rs @@ -0,0 +1,96 @@ +use crate::{prelude::*, wormhole::WormholeResponse}; + +use std::time::Duration; +use tokio::time::sleep; + +// Command Name +const NAME: &str = "get_vaa"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/get_vaa.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub emitter: String, + pub chain_id: String, + pub sequence: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + response: Option, + vaa: Option, +} + +async fn run(ctx: Context, input: Input) -> Result { + let wormhole_endpoint = match ctx.cfg.solana_client.cluster { + SolanaNet::Mainnet => "", + SolanaNet::Testnet => "", + SolanaNet::Devnet => "https://api.testnet.wormscan.io", + } + .to_owned(); + + let wormhole_path: &str = "api/v1/vaas"; + + let wormhole_url = format!( + "{}/{}/{}/{}/{}", + wormhole_endpoint, wormhole_path, input.chain_id, input.emitter, input.sequence + ); + + async fn send_wormhole_request( + client: &reqwest::Client, + wormhole_url: &str, + timeout: Duration, + ) -> Result { + let response = client.get(wormhole_url).timeout(timeout).send().await?; + Ok(response) + } + + let timeout = Duration::from_secs(60); + + let mut response = send_wormhole_request(&ctx.http, &wormhole_url, timeout).await?; + + while response.status() != 200 { + // Solana + if input.chain_id == "1" { + sleep(Duration::from_secs(5)).await; + } + // Eth Sepolia about 20m + if input.chain_id == "10002" { + sleep(Duration::from_secs(45)).await; + } + response = send_wormhole_request(&ctx.http, &wormhole_url, timeout).await?; + } + + let response_text = response.text().await?; + let response: WormholeResponse = serde_json::from_str(&response_text)?; + + let vaa = &response.data.vaa; + + Ok(Output { + response: Some(response.clone()), + vaa: Some(vaa.to_owned()), + }) +} + +/* +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test() { + // const res:&str = "{\"data\":{\"sequence\":420,\"id\":\"10002/000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9/420\",\"version\":1,\"emitterChain\":10002,\"emitterAddr\":\"000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9\",\"guardianSetIndex\":0,\"vaa\":\"AQAAAAABAIGVMaxqz2cou11lb1AVxzNNzPAV9ooflmTPSmcQmChxEfwlzHd+osaDIilfFlxNW7g5IMQPqQDhkgTyU/46qDwAZMBlwLQtAQAnEgAAAAAAAAAAAAAAANtUkiZfYDiDHon0lWcP+Qmt6UvZAAAAAAAAAaQBAgAAAAAAAAAAAAAAAEEKixUC8B8oh/CwWyLMk01FpiinJxISRVJDX1NZTUJPTAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNeUVSQzIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\",\"timestamp\":\"2023-07-26T00:16:00Z\",\"updatedAt\":\"2023-07-26T00:33:35.942Z\",\"indexedAt\":\"2023-07-26T00:33:35.942Z\",\"txHash\":\"0eacb8738102df585cb5dbbd7664f8e2fd9e04c02bcb7080cdc62b9bfcf09d9d\"},\"pagination\":{\"next\":\"\"}}"; + // let response: WormholeResponse = ron::de::from_str(&res).unwrap(); + + // dbg!(response.clone()); + } +} +*/ diff --git a/crates/cmds-solana/src/wormhole/mod.rs b/crates/cmds-solana/src/wormhole/mod.rs new file mode 100644 index 00000000..5ce593bf --- /dev/null +++ b/crates/cmds-solana/src/wormhole/mod.rs @@ -0,0 +1,319 @@ +use anchor_lang::AnchorSerialize; +use borsh::{BorshDeserialize, BorshSerialize}; +use byteorder::{BigEndian, ReadBytesExt}; +use flow_lib::SolanaNet; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; +use solana_sdk::pubkey; +use std::io::{Cursor, Read}; +use wormhole_sdk::{nft::Message as NftMessage, token::Message}; + +pub mod utils; + +pub const WORMHOLE_CORE_MAINNET: Pubkey = pubkey!("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"); +pub const WORMHOLE_CORE_TESTNET: Pubkey = pubkey!("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"); +pub const WORMHOLE_CORE_DEVNET: Pubkey = pubkey!("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"); + +pub const fn wormhole_core_program_id(net: SolanaNet) -> Pubkey { + match net { + SolanaNet::Mainnet => crate::wormhole::WORMHOLE_CORE_MAINNET, + // TODO testnet not deployed yet + SolanaNet::Testnet => crate::wormhole::WORMHOLE_CORE_TESTNET, + SolanaNet::Devnet => crate::wormhole::WORMHOLE_CORE_DEVNET, + } +} + +pub const TOKEN_BRIDGE_MAINNET: Pubkey = pubkey!("wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"); +pub const TOKEN_BRIDGE_TESTNET: Pubkey = pubkey!("DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe"); +pub const TOKEN_BRIDGE_DEVNET: Pubkey = pubkey!("DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe"); + +pub const fn token_bridge_program_id(net: SolanaNet) -> Pubkey { + match net { + SolanaNet::Mainnet => TOKEN_BRIDGE_MAINNET, + // TODO testnet not deployed yet + SolanaNet::Testnet => TOKEN_BRIDGE_TESTNET, + SolanaNet::Devnet => TOKEN_BRIDGE_DEVNET, + } +} + +pub const NFT_BRIDGE_MAINNET: Pubkey = pubkey!("WnFt12ZrnzZrFZkt2xsNsaNWoQribnuQ5B5FrDbwDhD"); +pub const NFT_BRIDGE_TESTNET: Pubkey = pubkey!("2rHhojZ7hpu1zA91nvZmT8TqWWvMcKmmNBCr2mKTtMq4"); +pub const NFT_BRIDGE_DEVNET: Pubkey = pubkey!("2rHhojZ7hpu1zA91nvZmT8TqWWvMcKmmNBCr2mKTtMq4"); + +pub const fn nft_bridge_program_id(net: SolanaNet) -> Pubkey { + match net { + SolanaNet::Mainnet => NFT_BRIDGE_MAINNET, + // TODO testnet not deployed yet + SolanaNet::Testnet => NFT_BRIDGE_TESTNET, + SolanaNet::Devnet => NFT_BRIDGE_DEVNET, + } +} + +pub mod nft_bridge; +pub mod token_bridge; + +pub mod get_vaa; +pub mod parse_vaa; +pub mod post_message; +pub mod post_vaa; +pub mod verify_signatures; + +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize)] +pub enum WormholeInstructions { + Initialize, + PostMessage, + PostVAA, + SetFees, + TransferFees, + UpgradeContract, + UpgradeGuardianSet, + VerifySignatures, + PostMessageUnreliable, +} + +#[derive(AnchorSerialize, Deserialize, Serialize)] +pub struct PostMessageData { + /// Unique nonce for this message + pub nonce: u32, + + /// Message payload + pub payload: Vec, + + /// Commitment Level required for an attestation to be produced + pub consistency_level: ConsistencyLevel, +} + +#[repr(u8)] +#[derive(AnchorSerialize, Clone, Serialize, Deserialize)] +pub enum ConsistencyLevel { + Confirmed, + Finalized, +} + +#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +pub struct BridgeData { + /// The current guardian set index, used to decide which signature sets to accept. + pub guardian_set_index: u32, + + /// Lamports in the collection account + pub last_lamports: u64, + + /// Bridge configuration, which is set once upon initialization. + pub config: BridgeConfig, +} + +#[derive(Clone, Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +pub struct BridgeConfig { + /// Period for how long a guardian set is valid after it has been replaced by a new one. This + /// guarantees that VAAs issued by that set can still be submitted for a certain period. In + /// this period we still trust the old guardian set. + pub guardian_set_expiration_time: u32, + + /// Amount of lamports that needs to be paid to the protocol to post a message + pub fee: u64, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct WormholeResponse { + data: WormholeData, + pagination: WormholePagination, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct WormholeData { + sequence: u64, + id: String, + version: u64, + emitter_chain: u64, + emitter_addr: String, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::unwrap_or_skip" + )] + emitter_native_addr: Option, + guardian_set_index: u64, + vaa: String, + timestamp: String, + updated_at: String, + indexed_at: String, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::unwrap_or_skip" + )] + tx_hash: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct WormholePagination { + next: String, +} + +// // Structs for API VAA parsing +// #[derive(Serialize, Deserialize, Debug)] +// struct GuardianSignature { +// index: u8, +// signature: Vec, +// } + +// #[derive(Serialize, Deserialize, Debug)] +// struct ParsedVaa { +// version: u8, +// guardian_set_index: u32, +// guardian_signatures: Vec, +// timestamp: u32, +// nonce: u32, +// emitter_chain: u16, +// emitter_address: [u8; 32], +// sequence: u64, +// consistency_level: u8, +// payload: Vec, +// } + +/// Type representing an Ethereum style public key for Guardians. +pub type GuardianPublicKey = [u8; 20]; + +#[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +pub struct GuardianSetData { + /// Index representing an incrementing version number for this guardian set. + pub index: u32, + + /// ETH style public keys + pub keys: Vec, + + /// Timestamp representing the time this guardian became active. + pub creation_time: u32, + + /// Expiration time when VAAs issued by this set are no longer valid. + pub expiration_time: u32, +} + +pub struct SignatureItem { + pub signature: Vec, + pub key: [u8; 20], + pub index: u8, +} + +const MAX_LEN_GUARDIAN_KEYS: usize = 19; + +#[derive(Default, BorshSerialize, BorshDeserialize)] +pub struct VerifySignaturesData { + /// instruction indices of signers (-1 for missing) + pub signers: [i8; MAX_LEN_GUARDIAN_KEYS], +} + +pub type ForeignAddress = [u8; 32]; + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct VAASignature { + pub signature: Vec, + pub guardian_index: u8, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct VAA { + // Header part + pub version: u8, + pub guardian_set_index: u32, + pub signatures: Vec, + // Body part + pub timestamp: u32, + pub nonce: u32, + pub emitter_chain: u16, + pub emitter_address: ForeignAddress, + pub sequence: u64, + pub consistency_level: u8, + pub payload: Vec, +} + +impl VAA { + pub const HEADER_LEN: usize = 6; + pub const SIGNATURE_LEN: usize = 66; + + pub fn deserialize(data: &[u8]) -> std::result::Result { + let mut rdr = Cursor::new(data); + + let version = rdr.read_u8()?; + let guardian_set_index = rdr.read_u32::()?; + + let len_sig = rdr.read_u8()?; + let mut signatures: Vec = Vec::with_capacity(len_sig as usize); + for _i in 0..len_sig { + let guardian_index = rdr.read_u8()?; + let mut signature_data = [0u8; 65]; + rdr.read_exact(&mut signature_data)?; + let signature = signature_data.to_vec(); + + signatures.push(VAASignature { + guardian_index, + signature, + }); + } + + let timestamp = rdr.read_u32::()?; + let nonce = rdr.read_u32::()?; + let emitter_chain = rdr.read_u16::()?; + + let mut emitter_address = [0u8; 32]; + rdr.read_exact(&mut emitter_address)?; + + let sequence = rdr.read_u64::()?; + let consistency_level = rdr.read_u8()?; + + let mut payload = Vec::new(); + rdr.read_to_end(&mut payload)?; + + Ok(VAA { + version, + guardian_set_index, + signatures, + timestamp, + nonce, + emitter_chain, + emitter_address, + sequence, + consistency_level, + payload, + }) + } +} + +#[derive(Default, BorshSerialize, BorshDeserialize, Clone, Serialize, Deserialize, Debug)] +pub struct PostVAAData { + // Header part + pub version: u8, + pub guardian_set_index: u32, + + // Body part + pub timestamp: u32, + pub nonce: u32, + pub emitter_chain: u16, + pub emitter_address: ForeignAddress, + pub sequence: u64, + pub consistency_level: u8, + pub payload: Vec, +} +impl From for PostVAAData { + fn from(vaa: VAA) -> Self { + PostVAAData { + version: vaa.version, + guardian_set_index: vaa.guardian_set_index, + timestamp: vaa.timestamp, + nonce: vaa.nonce, + emitter_chain: vaa.emitter_chain, + emitter_address: vaa.emitter_address, + sequence: vaa.sequence, + consistency_level: vaa.consistency_level, + payload: vaa.payload, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum MessageAlias { + Transfer(Message), + NftTransfer(NftMessage), +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/complete_native.rs b/crates/cmds-solana/src/wormhole/nft_bridge/complete_native.rs new file mode 100644 index 00000000..82b77d56 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/complete_native.rs @@ -0,0 +1,159 @@ +use crate::wormhole::{PostVAAData, VAA}; + +use crate::prelude::*; + +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; +use wormhole_sdk::nft::Message; + +use super::{CompleteNativeData, NFTBridgeInstructions, PayloadTransfer}; + +// Command Name +const NAME: &str = "nft_complete_native"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/nft_bridge/nft_complete_native.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + #[serde(with = "value::pubkey")] + pub to_authority: Pubkey, + pub vaa: bytes::Bytes, + pub payload: wormhole_sdk::nft::Message, + pub vaa_hash: bytes::Bytes, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let nft_bridge_program_id = + crate::wormhole::nft_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &nft_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + let vaa: PostVAAData = vaa.into(); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let claim_key = Pubkey::find_program_address( + &[ + vaa.emitter_address.as_ref(), + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.sequence.to_be_bytes().as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + + let payload: PayloadTransfer = match input.payload { + Message::Transfer { + nft_address, + nft_chain, + symbol, + name, + token_id, + uri, + to, + to_chain, + } => PayloadTransfer { + token_address: nft_address.0, + token_chain: nft_chain.into(), + to: to.into(), + to_chain: to_chain.into(), + symbol: symbol.to_string(), + name: name.to_string(), + token_id: primitive_types::U256::from(token_id.0), + uri: uri.to_string(), + }, + }; + // https://github.com/wormhole-foundation/wormhole/blob/faa397ca4f5cca067a7cfff375ab193463aabe39/sdk/js/src/solana/nftBridge/program.ts#L37 + let mut mint = vec![0u8; 32]; + payload.token_id.to_big_endian(&mut mint); + + let mint = Pubkey::try_from(mint).map_err(|_| anyhow::anyhow!("Invalid mint"))?; + + let custody_key = Pubkey::find_program_address(&[mint.as_ref()], &nft_bridge_program_id).0; + let custody_signer = + Pubkey::find_program_address(&[b"custody_signer"], &nft_bridge_program_id).0; + + let associated_token = + spl_associated_token_account::get_associated_token_address(&input.to_authority, &mint); + + let ix = solana_program::instruction::Instruction { + program_id: nft_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new(claim_key, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new(associated_token, false), + AccountMeta::new_readonly(input.to_authority, false), + AccountMeta::new(custody_key, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(custody_signer, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + ], + data: (NFTBridgeInstructions::CompleteNative, CompleteNativeData {}).try_to_vec()?, + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "token" => associated_token, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped.rs b/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped.rs new file mode 100644 index 00000000..94ead970 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped.rs @@ -0,0 +1,265 @@ +use crate::prelude::*; +use crate::wormhole::nft_bridge::Address; +use crate::wormhole::{PostVAAData, VAA}; +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; +use tracing::info; +use wormhole_sdk::nft::Message; + +use super::{CompleteWrappedData, NFTBridgeInstructions, PayloadTransfer}; + +// Command Name +const NAME: &str = "nft_complete_wrapped"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/nft_bridge/nft_complete_wrapped.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub vaa: bytes::Bytes, + // pub vaa: String, + pub payload: wormhole_sdk::nft::Message, + // pub payload: serde_json::Value, + pub vaa_hash: bytes::Bytes, + #[serde(with = "value::pubkey")] + pub to_authority: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let nft_bridge_program_id = + crate::wormhole::nft_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &nft_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + + let vaa: PostVAAData = vaa.into(); + + let payload: PayloadTransfer = match input.payload { + Message::Transfer { + nft_address, + nft_chain, + symbol, + name, + token_id, + uri, + to, + to_chain, + } => PayloadTransfer { + token_address: nft_address.0, + token_chain: nft_chain.into(), + to: Address(to.0), + to_chain: to_chain.into(), + symbol: symbol.to_string(), + name: name.to_string(), + token_id: primitive_types::U256::from_big_endian(&token_id.0), + uri: uri.to_string(), + }, + }; + + // Convert token id + let mut token_id = vec![0u8; 32]; + payload.token_id.to_big_endian(&mut token_id); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let claim_key = Pubkey::find_program_address( + &[ + vaa.emitter_address.as_ref(), + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.sequence.to_be_bytes().as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + + let mint = Pubkey::find_program_address( + &[ + b"wrapped", + payload.token_chain.to_be_bytes().as_ref(), + payload.token_address.as_ref(), + token_id.as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + info!("mint: {:?}", mint); + + let mint_meta = + Pubkey::find_program_address(&[b"meta", mint.as_ref()], &nft_bridge_program_id).0; + + let mint_authority = Pubkey::find_program_address(&[b"mint_signer"], &nft_bridge_program_id).0; + + let to = Pubkey::from(payload.to.0); + // let to = spl_associated_token_account::get_associated_token_address(&input.to_authority, &mint); + info!("to: {:?}", to); + + let ix = solana_program::instruction::Instruction { + program_id: nft_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new(claim_key, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new(to, false), + AccountMeta::new_readonly(input.to_authority, false), + AccountMeta::new(mint, false), + AccountMeta::new(mint_meta, false), + AccountMeta::new_readonly(mint_authority, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), + ], + data: ( + NFTBridgeInstructions::CompleteWrapped, + CompleteWrappedData {}, + ) + .try_to_vec()?, + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "mint_metadata" => mint_meta, + "mint" => mint, + "token_account"=> to + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} + +// #[cfg(test)] +// mod tests { +// use crate::wormhole::token_bridge::eth::Receipt; + +// use super::*; + +// #[derive(Serialize, Deserialize, Debug)] +// struct Payload { +// #[serde(rename = "networkName")] +// network_name: String, +// token: String, +// keypair: String, +// recipient: String, +// #[serde(rename = "tokenId")] +// token_id: String, +// } + +// #[tokio::test] +// async fn need_key_test_local() { +// let _json_input = r#"{ +// "output": { +// "receipt": { +// "to": "0xD8E4C2DbDd2e2bd8F1336EA691dBFF6952B1a6eB", +// "from": "0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6", +// "contractAddress": null, +// "transactionIndex": 8, +// "gasUsed": { +// "type": "BigNumber", +// "hex": "0x578c" +// }, +// "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", +// "blockHash": "0x4eb1e80788dfed4d50a5bf72d5ece34f023e796ebb522d0102997cc8b066c49f", +// "transactionHash": "0x0b911086660107e379011b76a5841626db0b67df80f4734ed12ddceef8f41799", +// "logs": [], +// "blockNumber": 4330148, +// "confirmations": 1, +// "cumulativeGasUsed": { +// "type": "BigNumber", +// "hex": "0x23ebec" +// }, +// "effectiveGasPrice": { +// "type": "BigNumber", +// "hex": "0x59682f08" +// }, +// "status": 1, +// "type": 2, +// "byzantium": true, +// "events": [] +// } +// } +// }"#; + +// async fn test(payload: Payload) -> Result { +// let client = reqwest::Client::new(); +// let response = client +// .post( +// "https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/transfer_nft_from_eth", +// ) +// .json(&payload) +// .send() +// .await? +// .json::() +// .await?; + +// let receipt = response.output.receipt; + +// Ok(receipt) +// } + +// let payload = Payload { +// network_name: "devnet".into(), +// token: "0xDB5492265f6038831E89f495670FF909aDe94bd9".into(), +// keypair: "".into(), +// recipient: "0x00000000".into(), +// token_id: "0".into(), +// }; + +// let res = test(payload).await.unwrap(); +// dbg!(res); +// } +// } diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped_meta.rs b/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped_meta.rs new file mode 100644 index 00000000..765d3615 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/complete_wrapped_meta.rs @@ -0,0 +1,178 @@ +use crate::wormhole::{PostVAAData, VAA}; + +use crate::prelude::*; + +use borsh::BorshSerialize; + +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; +use tracing::info; +use wormhole_sdk::nft::Message; + +use super::{Address, CompleteWrappedMetaData, NFTBridgeInstructions, PayloadTransfer}; + +// Command Name +const NAME: &str = "nft_complete_wrapped_meta"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/nft_bridge/nft_complete_wrapped_meta.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub vaa: bytes::Bytes, + pub payload: wormhole_sdk::nft::Message, + pub vaa_hash: bytes::Bytes, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let nft_bridge_program_id = + crate::wormhole::nft_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &nft_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + let vaa: PostVAAData = vaa.into(); + + let payload: PayloadTransfer = match input.payload { + Message::Transfer { + nft_address, + nft_chain, + symbol, + name, + token_id, + uri, + to, + to_chain, + } => PayloadTransfer { + token_address: nft_address.0, + token_chain: nft_chain.into(), + to: Address(to.0), + to_chain: to_chain.into(), + symbol: symbol.to_string(), + name: name.to_string(), + token_id: primitive_types::U256::from(token_id.0), + uri: uri.to_string(), + }, + }; + + info!("payload: {:?}", payload); + + // Convert token id + let mut token_id = vec![0u8; 32]; + payload.token_id.to_big_endian(&mut token_id); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &nft_bridge_program_id, + ) + .0; + + let mint = Pubkey::find_program_address( + &[ + b"wrapped", + payload.token_chain.to_be_bytes().as_ref(), + payload.token_address.as_ref(), + &token_id, + ], + &nft_bridge_program_id, + ) + .0; + + let mint_meta = + Pubkey::find_program_address(&[b"meta", mint.as_ref()], &nft_bridge_program_id).0; + + let mint_authority = Pubkey::find_program_address(&[b"mint_signer"], &nft_bridge_program_id).0; + + // SPL Metadata + let spl_metadata = Pubkey::find_program_address( + &[ + b"metadata".as_ref(), + mpl_token_metadata::ID.as_ref(), + mint.as_ref(), + ], + &mpl_token_metadata::ID, + ) + .0; + + let ix = solana_program::instruction::Instruction { + program_id: nft_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(mint_meta, false), + AccountMeta::new(spl_metadata, false), + AccountMeta::new_readonly(mint_authority, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(spl_associated_token_account::id(), false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), + ], + data: ( + NFTBridgeInstructions::CompleteWrappedMeta, + CompleteWrappedMetaData {}, + ) + .try_to_vec()?, + }; + + info!("ix: {:?}", ix); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "mint_metadata" => mint_meta, + "mint" => mint, + "spl_metadata"=> spl_metadata + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/eth/mod.rs b/crates/cmds-solana/src/wormhole/nft_bridge/eth/mod.rs new file mode 100644 index 00000000..abae4a35 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/eth/mod.rs @@ -0,0 +1,2 @@ +pub mod redeem_nft_on_eth; +pub mod transfer_nft_from_eth; diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/eth/redeem_nft_on_eth.rs b/crates/cmds-solana/src/wormhole/nft_bridge/eth/redeem_nft_on_eth.rs new file mode 100644 index 00000000..2edf87be --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/eth/redeem_nft_on_eth.rs @@ -0,0 +1,173 @@ +use crate::{ + prelude::*, + wormhole::token_bridge::eth::{Receipt, RedeemOnEthResponse}, +}; + +// Command Name +const NAME: &str = "redeem_nft_on_eth"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/nft_bridge/eth/redeem_nft_on_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub network_name: String, + pub signed_vaa: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + receipt: Receipt, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + } + + let payload = Payload { + network_name: input.network_name, + keypair: input.keypair, + signed_vaa: input.signed_vaa, + }; + + let response: RedeemOnEthResponse = ctx + .http + .post("https://space-operator.deno.dev/api/redeem_nft_on_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let receipt: Receipt = response.output.receipt; + + // to is the wormhole token bridge contract + // from is the recipient + // logs/address is the transferred token contract address + + Ok(Output { receipt }) +} + +#[cfg(test)] +mod tests { + use crate::wormhole::token_bridge::eth::RedeemOnEthResponse; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + } + + #[tokio::test] + async fn need_key_test_local() { + let _json_response = r#"{ + "output": Object { + "receipt": Object { + "to": String("0xDB5492265f6038831E89f495670FF909aDe94bd9"), + "from": String("0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6"), + "contractAddress": Null, + "transactionIndex": Number(24), + "gasUsed": Object { + "type": String("BigNumber"), + "hex": String("0x02916e"), + }, + "logsBloom": String("0x00000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000040000008000400000000000000000000000000000000000000000000020000000000000000000800000000000002000000000010000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000"), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "logs": Array [ + Object { + "transactionIndex": Number(24), + "blockNumber": Number(4205532), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "address": String("0x44C80265b027b4Fed63C177f3Ed9C174a0f417d1"), + "topics": Array [ + String("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + String("0x0000000000000000000000000000000000000000000000000000000000000000"), + String("0x000000000000000000000000dd6c5b9ea3ac0fb5387e5e6b482788d5f70772a6"), + ], + "data": String("0x00000000000000000000000000000000000000000000000000000002540be400"), + "logIndex": Number(45), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + }, + ], + "blockNumber": Number(4205532), + "confirmations": Number(1), + "cumulativeGasUsed": Object { + "type": String("BigNumber"), + "hex": String("0x763888"), + }, + "effectiveGasPrice": Object { + "type": String("BigNumber"), + "hex": String("0x68f8dff3"), + }, + "status": Number(1), + "type": Number(2), + "byzantium": Bool(true), + "events": Array [ + Object { + "transactionIndex": Number(24), + "blockNumber": Number(4205532), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "address": String("0x44C80265b027b4Fed63C177f3Ed9C174a0f417d1"), + "topics": Array [ + String("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + String("0x0000000000000000000000000000000000000000000000000000000000000000"), + String("0x000000000000000000000000dd6c5b9ea3ac0fb5387e5e6b482788d5f70772a6"), + ], + "data": String("0x00000000000000000000000000000000000000000000000000000002540be400"), + "logIndex": Number(45), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + }, + ], + }, + }, + }"#; + + async fn test(payload: Payload) -> Result { + let client = reqwest::Client::new(); + let json = client + .post("https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/redeem_nft_on_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + dbg!(&json); + + let response = serde_json::from_value(json).unwrap(); + + Ok(response) + } + + let payload = Payload { + network_name: "devnet".into(), + keypair: "0x1bb0ed141673d3228d6dc10806f0de5ee6522695160aed8fb99e487a9abc622c".into(), + signed_vaa: "AQAAAAABANOioLxunWtMG55i8Sbn9l2UMNrf50Vh9XDEb1vn9ZhRY5KuhjiXRVeM4aZ/xCUR+Oem5bZRRLZBIRg+Xa6WcTsAZQ0MVNHVy/YAAXUqSYFOQLlrCXIH5LU/3TMFROHmYWU/utS8FZzCioOeAAAAAAAAAKUgAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAFTUE9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNPICMxMTExMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4q3NjFNInmCE+RA3bMIlNx5NQRkCcSdoXMxHOFc/8wjIaHR0cHM6Ly9hcndlYXZlLm5ldC8zRnhwSUlicHlTbmZUVFhJcnBvamhGMktISGpldkk4TXJ0M3BBQ21FYlNZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADdbFueo6wPtTh+XmtIJ4jV9wdypicm".into(), + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/eth/transfer_nft_from_eth.rs b/crates/cmds-solana/src/wormhole/nft_bridge/eth/transfer_nft_from_eth.rs new file mode 100644 index 00000000..b3f3aaec --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/eth/transfer_nft_from_eth.rs @@ -0,0 +1,165 @@ +use crate::{prelude::*, wormhole::token_bridge::eth::TransferFromEthResponse}; +use tracing::info; + +// Command Name +const NAME: &str = "transfer_nft_from_eth"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/nft_bridge/eth/transfer_nft_from_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub token: String, + pub network_name: String, + pub recipient: String, + pub token_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + response: TransferFromEthResponse, + emitter: String, + sequence: String, + recipient_ata: Pubkey, + mint: Pubkey, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + recipient: String, + #[serde(rename = "tokenId")] + token_id: String, + } + + let payload = Payload { + network_name: input.network_name, + token: input.token, + keypair: input.keypair, + recipient: input.recipient, + token_id: input.token_id, + }; + + let response: TransferFromEthResponse = ctx + .http + .post("https://space-operator.deno.dev/api/transfer_nft_from_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let emitter = response.output.emitter_address.clone(); + let sequence = response.output.sequence.clone(); + + let recipient_ata = response.output.recipient_ata.parse()?; + let mint = response.output.mint.parse()?; + + info!("recipient_ata: {:?}", recipient_ata); + info!("mint: {:?}", mint); + + Ok(Output { + response, + emitter, + sequence, + recipient_ata, + mint, + }) +} + +#[cfg(test)] +mod tests { + use crate::wormhole::token_bridge::eth::{Receipt, Response as ServerlessOutput}; + + use super::*; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + recipient: String, + #[serde(rename = "tokenId")] + token_id: String, + } + + #[tokio::test] + async fn need_key_test_local() { + let _json_input = r#"{ + "output": { + "receipt": { + "to": "0xD8E4C2DbDd2e2bd8F1336EA691dBFF6952B1a6eB", + "from": "0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6", + "contractAddress": null, + "transactionIndex": 8, + "gasUsed": { + "type": "BigNumber", + "hex": "0x578c" + }, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x4eb1e80788dfed4d50a5bf72d5ece34f023e796ebb522d0102997cc8b066c49f", + "transactionHash": "0x0b911086660107e379011b76a5841626db0b67df80f4734ed12ddceef8f41799", + "logs": [], + "blockNumber": 4330148, + "confirmations": 1, + "cumulativeGasUsed": { + "type": "BigNumber", + "hex": "0x23ebec" + }, + "effectiveGasPrice": { + "type": "BigNumber", + "hex": "0x59682f08" + }, + "status": 1, + "type": 2, + "byzantium": true, + "events": [] + } + } + }"#; + + async fn test(payload: Payload) -> Result { + let client = reqwest::Client::new(); + let response = client + .post( + "https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/transfer_nft_from_eth", + ) + .json(&payload) + .send() + .await? + .json::() + .await?; + + let receipt = response.output.receipt; + + Ok(receipt) + } + + let payload = Payload { + network_name: "devnet".into(), + token: "0xDB5492265f6038831E89f495670FF909aDe94bd9".into(), + keypair: "".into(), + recipient: "0x00000000".into(), + token_id: "0".into(), + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/mod.rs b/crates/cmds-solana/src/wormhole/nft_bridge/mod.rs new file mode 100644 index 00000000..17ac1872 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/mod.rs @@ -0,0 +1,69 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use primitive_types::U256; + +use super::{token_bridge::Address, ForeignAddress}; + +pub mod complete_native; +pub mod complete_wrapped; +pub mod complete_wrapped_meta; +pub mod eth; +pub mod transfer_native; +pub mod transfer_wrapped; + +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize)] +enum NFTBridgeInstructions { + Initialize, + CompleteNative, + CompleteWrapped, + CompleteWrappedMeta, + TransferWrapped, + TransferNative, + RegisterChain, + UpgradeContract, +} + +pub type ChainID = u16; + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct TransferWrappedData { + pub nonce: u32, + pub target_address: Address, + pub target_chain: ChainID, +} + +#[derive(PartialEq, Debug, Clone)] +pub struct PayloadTransfer { + // Address of the token. Left-zero-padded if shorter than 32 bytes + pub token_address: ForeignAddress, + // Chain ID of the token + pub token_chain: ChainID, + // Symbol of the token + pub symbol: String, + // Name of the token + pub name: String, + // TokenID of the token (big-endian uint256) + pub token_id: U256, + // URI of the token metadata + pub uri: String, + // Address of the recipient. Left-zero-padded if shorter than 32 bytes + pub to: Address, + // Chain ID of the recipient + pub to_chain: ChainID, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteWrappedData {} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteWrappedMetaData {} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct TransferNativeData { + pub nonce: u32, + pub target_address: Address, + pub target_chain: ChainID, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteNativeData {} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/transfer_native.rs b/crates/cmds-solana/src/wormhole/nft_bridge/transfer_native.rs new file mode 100644 index 00000000..46b5d0eb --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/transfer_native.rs @@ -0,0 +1,172 @@ +use crate::{ + prelude::*, + wormhole::token_bridge::{eth::hex_to_address, get_sequence_number_from_message}, +}; + +use borsh::BorshSerialize; + +use rand::Rng; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; + +use super::{NFTBridgeInstructions, TransferNativeData}; + +// Command Name +const NAME: &str = "nft_transfer_native"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/nft_bridge/nft_transfer_native.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub target_address: String, + pub target_chain: u16, + pub message: Wallet, + #[serde(with = "value::pubkey")] + pub from: Pubkey, + #[serde(with = "value::pubkey")] + pub mint: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let nft_bridge_program_id = + crate::wormhole::nft_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &nft_bridge_program_id).0; + let custody_key = + Pubkey::find_program_address(&[input.mint.as_ref()], &nft_bridge_program_id).0; + + let authority_signer = + Pubkey::find_program_address(&[b"authority_signer"], &nft_bridge_program_id).0; + + let custody_signer = + Pubkey::find_program_address(&[b"custody_signer"], &nft_bridge_program_id).0; + + // SPL Metadata + let metadata = Pubkey::find_program_address( + &[ + b"metadata".as_ref(), + mpl_token_metadata::ID.as_ref(), + input.mint.as_ref(), + ], + &mpl_token_metadata::ID, + ) + .0; + + let emitter = Pubkey::find_program_address(&[b"emitter"], &nft_bridge_program_id).0; + + let bridge_config = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + let address = hex_to_address(&input.target_address).map_err(anyhow::Error::msg)?; + + let data = TransferNativeData { + nonce, + target_address: address, + target_chain: input.target_chain, + }; + + let ix = solana_program::instruction::Instruction { + program_id: nft_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new(input.from, false), + AccountMeta::new(input.mint, false), + AccountMeta::new_readonly(metadata, false), + AccountMeta::new(custody_key, false), + AccountMeta::new_readonly(authority_signer, false), + AccountMeta::new_readonly(custody_signer, false), + AccountMeta::new(bridge_config, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, false), + AccountMeta::new(sequence, false), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), + // Dependencies + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: (NFTBridgeInstructions::TransferNative, data).try_to_vec()?, + }; + + let message_pubkey = input.message.pubkey(); + + let instructions = [ + spl_token::instruction::approve( + &spl_token::id(), + &input.from, + &authority_signer, + &input.payer.pubkey(), + &[], + 1, + ) + .unwrap(), + ix, + ] + .into(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.message].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + // let sequence_data: SequenceTracker = get_sequence_number(&ctx, sequence).await?; + + let signature = ctx + .execute( + ins, + value::map! { + "metadata" => metadata, + // "sequence" => sequence_data.sequence.to_string(), + "emitter" => emitter.to_string(), + }, + ) + .await? + .signature; + + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + + Ok(Output { + signature, + sequence, + }) +} diff --git a/crates/cmds-solana/src/wormhole/nft_bridge/transfer_wrapped.rs b/crates/cmds-solana/src/wormhole/nft_bridge/transfer_wrapped.rs new file mode 100644 index 00000000..8c987713 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/nft_bridge/transfer_wrapped.rs @@ -0,0 +1,164 @@ +use crate::wormhole::token_bridge::{eth::hex_to_address, get_sequence_number_from_message}; + +use crate::prelude::*; + +use borsh::BorshSerialize; + +use rand::Rng; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; + +use super::{NFTBridgeInstructions, TransferWrappedData}; + +// Command Name +const NAME: &str = "nft_transfer_wrapped"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/nft_bridge/nft_transfer_wrapped.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + #[serde(with = "value::pubkey")] + pub mint: Pubkey, + pub target_address: String, + pub target_chain: u16, + pub message: Wallet, + pub from_owner: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let nft_bridge_program_id = + crate::wormhole::nft_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &nft_bridge_program_id).0; + + let wrapped_meta_key = + Pubkey::find_program_address(&[b"meta", input.mint.as_ref()], &nft_bridge_program_id).0; + + // SPL Metadata + let spl_metadata = Pubkey::find_program_address( + &[ + b"metadata".as_ref(), + mpl_token_metadata::ID.as_ref(), + input.mint.as_ref(), + ], + &mpl_token_metadata::ID, + ) + .0; + + let authority_signer = + Pubkey::find_program_address(&[b"authority_signer"], &nft_bridge_program_id).0; + + let emitter = Pubkey::find_program_address(&[b"emitter"], &nft_bridge_program_id).0; + + let bridge_config = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + let wrapped_data = TransferWrappedData { + nonce, + target_address: hex_to_address(&input.target_address)?, + target_chain: input.target_chain, + }; + + let from_ata = spl_associated_token_account::get_associated_token_address( + &input.from_owner.pubkey(), + &input.mint, + ); + + let ix = solana_program::instruction::Instruction { + program_id: nft_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new(from_ata, false), + AccountMeta::new_readonly(input.from_owner.pubkey(), true), + AccountMeta::new(input.mint, false), + AccountMeta::new_readonly(wrapped_meta_key, false), + AccountMeta::new_readonly(spl_metadata, false), + AccountMeta::new_readonly(authority_signer, false), + AccountMeta::new(bridge_config, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, false), + AccountMeta::new(sequence, false), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), + // Dependencies + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: (NFTBridgeInstructions::TransferWrapped, wrapped_data).try_to_vec()?, + }; + + let approve_ix = spl_token::instruction::approve( + &spl_token::id(), + &from_ata, + &authority_signer, + &input.from_owner.pubkey(), + &[], + 1, + )?; + + let message_pubkey = input.message.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.from_owner, input.message].into(), + instructions: [approve_ix, ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "wrapped_meta_key" => wrapped_meta_key, + "emitter" => emitter, + }, + ) + .await? + .signature; + + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + + Ok(Output { + signature, + sequence, + }) +} diff --git a/crates/cmds-solana/src/wormhole/parse_vaa.rs b/crates/cmds-solana/src/wormhole/parse_vaa.rs new file mode 100644 index 00000000..715f2b43 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/parse_vaa.rs @@ -0,0 +1,274 @@ +use crate::prelude::*; +use base64::decode; +use primitive_types::U256; +use wormhole_sdk::{nft::Message as NftMessage, vaa::Digest, Address, Chain, Vaa}; + +use super::MessageAlias; + +// Command Name +const NAME: &str = "parse_vaa"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/parse_vaa.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub vaa: String, + // pub vaa_payload_type: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + parsed_vaa: Vaa>, + vaa_bytes: bytes::Bytes, + signatures: Vec, + body: bytes::Bytes, + vaa_hash: bytes::Bytes, + vaa_secp256k_hash: bytes::Bytes, + guardian_set_index: u32, + payload: serde_json::Value, + nft_token_id: Option, +} + +async fn run(_ctx: Context, input: Input) -> Result { + let vaa_string = &input.vaa; + + let vaa_bytes = decode(vaa_string) + .map_err(|err| anyhow::anyhow!("Failed to decode VAA string: {}", err))?; + + let sig_start = 6; + let num_signers = vaa_bytes[5] as usize; + let sig_length = 66; + + let mut guardian_signatures = Vec::new(); + for i in 0..num_signers { + let start = sig_start + i * sig_length; + let mut signature = [0u8; 65]; + signature.copy_from_slice(&vaa_bytes[start + 1..start + 66]); + guardian_signatures.push(wormhole_sdk::vaa::Signature { + index: vaa_bytes[start], + signature, + }); + } + + let body = &vaa_bytes[sig_start + sig_length * num_signers..]; + // Check this https://github.com/wormhole-foundation/wormhole/blob/14a1251c06b3d837dcbd2b7bed5b1abae6eb7d02/solana/bridge/program/src/vaa.rs#L176 + let parsed_vaa: Vaa> = Vaa { + version: vaa_bytes[0], + guardian_set_index: u32::from_be_bytes( + vaa_bytes[1..5] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert guardian_set_index"))?, + ), + signatures: guardian_signatures.clone(), + timestamp: u32::from_be_bytes( + body[0..4] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert timestamp"))?, + ), + nonce: u32::from_be_bytes( + body[4..8] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert nonce"))?, + ), + emitter_chain: Chain::from(u16::from_be_bytes( + body[8..10] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert emitter_chain"))?, + )), + emitter_address: Address( + body[10..42] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert emitter_address"))?, + ), + sequence: u64::from_be_bytes( + body[42..50] + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert sequence"))?, + ), + consistency_level: body[50], + // gets converted to base64 string? + payload: body[51..].to_vec(), + }; + + // let (_, body): (Header, Body>) = parsed_vaa.into(); + + let Digest { + hash: vaa_hash, + secp256k_hash: vaa_secp256k_hash, + } = wormhole_sdk::vaa::digest(body).map_err(|_| anyhow::anyhow!("Failed to digest VAA"))?; + + let payload = match serde_wormhole::from_slice(&parsed_vaa.payload) { + Ok(message) => MessageAlias::Transfer(message), + Err(_) => match serde_wormhole::from_slice(&parsed_vaa.payload) { + Ok(nft_message) => MessageAlias::NftTransfer(nft_message), + Err(_) => return Err(anyhow::anyhow!("Payload content not supported")), + }, + }; + + let output_payload: serde_json::Value = + serde_json::from_str(&serde_json::to_string(&payload)?)?; + + let output_payload = output_payload + .get("NftTransfer") + .or(output_payload.get("Transfer")) + .ok_or_else(|| anyhow::anyhow!("Invalid payload"))?; + + let token_id = match &payload { + MessageAlias::NftTransfer(message) => match message { + NftMessage::Transfer { + token_id, + nft_address: _, + nft_chain: _, + symbol: _, + name: _, + uri: _, + to: _, + to_chain: _, + } => Some(token_id), + }, + _ => None, + }; + + let nft_token_id = token_id.map(|token_id| U256::from_big_endian(&token_id.0).to_string()); + + Ok(Output { + parsed_vaa: parsed_vaa.clone(), + vaa_bytes: bytes::Bytes::copy_from_slice(&vaa_bytes), + signatures: guardian_signatures, + body: bytes::Bytes::copy_from_slice(body), + vaa_hash: bytes::Bytes::copy_from_slice(&vaa_hash), + vaa_secp256k_hash: bytes::Bytes::copy_from_slice(&vaa_secp256k_hash), + guardian_set_index: parsed_vaa.guardian_set_index, + payload: output_payload.clone(), + nft_token_id, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use wormhole_sdk::token::Message; + + #[test] + fn test() -> Result<(), anyhow::Error> { + //sol vaa, not supported payload + let _vaa_string:String = "AQAAAAABAE9eT/T0B917C5+ZQEHdlDUD/b7PNfTkyy/mXX7LPSJzVS6VTJx1gigK7xCic3UywM5/ehtUnZ/HCdoLQtOLX1IBZLYUVg1YsBoAAcARZHHBCI3jyzPKm9l0vBFJ3DJ4Yh+vmP6ZmTrfVHxrAAAAAAAAAAABSGVsbG8gV29ybGQh".to_string(); + let vaa_string:String ="AQAAAAABAMy+FBjMJafK1Xt4cCSbJ03jxJs3f3UW647HrdpT34XWE/7CBbQjo+0xMQXDTlh5IymI6wissEo8TkxTwY/ufCwBZMMBLO/WHgoAATsmQJ+Kre0/XdyhhGlapqD6gpsMhcr4SFYySJbSFMqYAAAAAAAAX3cgAv+98jdq256Gu41IuSzwRBryKQ5Ku3e8LsfhFUYdQ2pkAAEJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".to_string(); + // //eth vaa + // let vaa_string:String="AQAAAAABAHZle4NbI4+ItAFCCwtKYDthhzq61u1az/gZIbW+hQ8MRskKSDEvutVy7pjuRwRq7EsKhB/lMz4XDDxoeyVm6YkBZMASCPZ6AAAnEgAAAAAAAAAAAAAAANtUkiZfYDiDHon0lWcP+Qmt6UvZAAAAAAAAAZgBAgAAAAAAAAAAAAAAAEEKixUC8B8oh/CwWyLMk01FpiinJxISRVJDX1NZTUJPTAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNeUVSQzIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".to_string(); + + // //eth transfer vaa + // let vaa_string:String="AQAAAAABAIDirkZb0u0i33P55FM8+ErUor6LbHELePcpfMyC3JRHPFQJ7ztwLOI9XlwvK1cqgSQC8Q+4hh/gyV5W8/rKt2cBZMFSePdTAQAnEgAAAAAAAAAAAAAAANtUkiZfYDiDHon0lWcP+Qmt6UvZAAAAAAAAAacBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF9eEAAAAAAAAAAAAAAAAAQQqLFQLwHyiH8LBbIsyTTUWmKKcnEi26xJVia/fd3KTtEQn+ZwcAonBDCzA1vRw+oHhAWKEJAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".to_string(); + + // NFT vaa from ETH + // let vaa_string ="AQAAAAABAK2WRJ3P2kYYzQTI1P9QS7PK19hPBic2XYQviPBIzGqOISa+/M6aSwm/2VyKfVEPvAfDXbhKqpOpeHuifzlSluwBZQ3JzfuW1JYAAXUqSYFOQLlrCXIH5LU/3TMFROHmYWU/utS8FZzCioOeAAAAAAAAAKsgAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAFTUE9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNPICMxMTExMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0YcsTJvXJtMAUWOeXRBneS7i9QqAN/hIBs4rqasbOrnIaHR0cHM6Ly9hcndlYXZlLm5ldC8zRnhwSUlicHlTbmZUVFhJcnBvamhGMktISGpldkk4TXJ0M3BBQ21FYlNZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADdbFueo6wPtTh+XmtIJ4jV9wdypicS".to_string(); + + let vaa_bytes = decode(vaa_string).unwrap(); + + let sig_start = 6; + let num_signers = vaa_bytes[5] as usize; + let sig_length = 66; + + let mut guardian_signatures = Vec::new(); + for i in 0..num_signers { + let start = sig_start + i * sig_length; + let mut signature = [0u8; 65]; + signature.copy_from_slice(&vaa_bytes[start + 1..start + 66]); + guardian_signatures.push(wormhole_sdk::vaa::Signature { + index: vaa_bytes[start], + signature, + }); + } + + let body = &vaa_bytes[sig_start + sig_length * num_signers..]; + + let parsed_vaa = Vaa { + version: vaa_bytes[0], + guardian_set_index: u32::from_be_bytes(vaa_bytes[1..5].try_into().unwrap()), + signatures: guardian_signatures, + timestamp: u32::from_be_bytes(body[0..4].try_into().unwrap()), + nonce: u32::from_be_bytes(body[4..8].try_into().unwrap()), + emitter_chain: Chain::from(u16::from_be_bytes(body[8..10].try_into().unwrap())), + emitter_address: Address(body[10..42].try_into().unwrap()), + sequence: u64::from_be_bytes(body[42..50].try_into().unwrap()), + consistency_level: body[50], + payload: body[51..].to_vec(), + }; + + #[derive(Serialize, Deserialize, Debug)] + enum MessageAlias { + Transfer(Message), + NftTransfer(NftMessage), + } + + let payload = match serde_wormhole::from_slice(&parsed_vaa.payload) { + Ok(message) => MessageAlias::Transfer(message), + Err(_) => match serde_wormhole::from_slice(&parsed_vaa.payload) { + Ok(nft_message) => MessageAlias::NftTransfer(nft_message), + Err(_) => return Err(anyhow::anyhow!("Payload content not supported")), + }, + }; + + let token_id = match &payload { + MessageAlias::NftTransfer(message) => match message { + NftMessage::Transfer { + token_id, + nft_address: _, + nft_chain: _, + symbol: _, + name: _, + uri: _, + to: _, + to_chain: _, + } => Some(token_id), + }, + _ => None, + }; + + let _token_id = token_id.map(|token_id| U256::from_big_endian(&token_id.0).to_string()); + // dbg!(token_id); + // panic!("test"); + + // Convert token id + + // let payload_value: serde_json::Value = serde_json::from_str(&serde_json::to_string(&payload)?)?; + + // let inner_json = payload_value + // .get("NftTransfer") + // .or(payload_value.get("Transfer")) + // .ok_or_else(|| anyhow::anyhow!("Invalid payload"))?; + + // dbg!(&parsed_vaa); + // dbg!(&inner_json.to_string()); + + // let string = String::from_utf8(parsed_vaa.payload).unwrap(); + // println!("{}", string); + // dbg!(&vaa_bytes); + + // let token_id_str = inner_json["1"]["token_id"].as_str().ok_or_else(|| anyhow::anyhow!("Token ID not found"))?; + // let token_id = U256::from_dec_str(token_id_str).map_err(|_| anyhow::anyhow!("Invalid token ID"))?; + // let mut token_id_bytes = vec![0u8; 32]; + // token_id.to_big_endian(&mut token_id_bytes); + // dbg!(token_id); + + // let token_id_bytes = U256::from_dec_str(token_id) + // .map_err(|_| anyhow::anyhow!("Invalid token ID"))? + // .to_big_endian(); + // dbg!(token_id_bytes); + + // let token_id_input = + // U256::from_str(token_id_str).map_err(|_| anyhow::anyhow!("Invalid token id"))?; + // let mut token_id = vec![0u8; 32]; + // token_id_input.to_big_endian(&mut token_id); + Ok(()) + } +} diff --git a/crates/cmds-solana/src/wormhole/post_message.rs b/crates/cmds-solana/src/wormhole/post_message.rs new file mode 100644 index 00000000..64413fa8 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/post_message.rs @@ -0,0 +1,118 @@ +use crate::{prelude::*, wormhole::WormholeInstructions}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use rand::Rng; +use solana_program::{instruction::AccountMeta, system_instruction, sysvar}; +use solana_sdk::pubkey::Pubkey; + +use super::{token_bridge::get_sequence_number_from_message, BridgeData, PostMessageData}; + +// Command Name +const NAME: &str = "post_message"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/post_message.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub emitter: Wallet, + pub message: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, + emitter: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + let emitter = input.emitter.pubkey(); + + let bridge = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + // TODO test payload + let _payload = [0u8; 32].to_vec(); + let payload = "Hello World!".as_bytes().to_vec(); + + let ix = solana_program::instruction::Instruction { + program_id: wormhole_core_program_id, + accounts: vec![ + AccountMeta::new(bridge, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, true), + AccountMeta::new(sequence, false), + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ], + data: ( + WormholeInstructions::PostMessage, + PostMessageData { + nonce, + payload: payload.to_vec(), + consistency_level: super::ConsistencyLevel::Confirmed, + }, + ) + .try_to_vec()?, + }; + + // Get message fee + let bridge_config_account = ctx.solana_client.get_account(&bridge).await?; + let bridge_config = BridgeData::try_from_slice(bridge_config_account.data.as_slice())?; + let fee = bridge_config.config.fee; + + let message_pubkey = input.message.pubkey(); + + let instructions = [ + system_instruction::transfer(&input.payer.pubkey(), &fee_collector, fee), + ix, + ] + .into(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.emitter, input.message].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + + Ok(Output { + signature, + sequence, + emitter: emitter.to_string(), + }) +} diff --git a/crates/cmds-solana/src/wormhole/post_vaa.rs b/crates/cmds-solana/src/wormhole/post_vaa.rs new file mode 100644 index 00000000..dae79e8b --- /dev/null +++ b/crates/cmds-solana/src/wormhole/post_vaa.rs @@ -0,0 +1,95 @@ +use super::{PostVAAData, VAA}; +use crate::{prelude::*, wormhole::WormholeInstructions}; +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const NAME: &str = "post_vaa"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/post_vaa.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub guardian_set_index: u32, + pub vaa_hash: bytes::Bytes, + pub vaa: bytes::Bytes, + // TODO: not in signers list + pub signature_set: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + let bridge = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let guardian_set = Pubkey::find_program_address( + &[b"GuardianSet", &input.guardian_set_index.to_le_bytes()], + &wormhole_core_program_id, + ) + .0; + + let vaa_address = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + + let vaa: PostVAAData = vaa.into(); + + let ix = solana_program::instruction::Instruction { + program_id: wormhole_core_program_id, + accounts: vec![ + AccountMeta::new_readonly(guardian_set, false), + AccountMeta::new_readonly(bridge, false), + AccountMeta::new_readonly(input.signature_set.pubkey(), false), + AccountMeta::new(vaa_address, false), + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: (WormholeInstructions::PostVAA, vaa).try_to_vec()?, + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "vaa_address" => vaa_address, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/attest.rs b/crates/cmds-solana/src/wormhole/token_bridge/attest.rs new file mode 100644 index 00000000..b02f7134 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/attest.rs @@ -0,0 +1,138 @@ +use crate::prelude::*; + +use borsh::BorshSerialize; +use rand::Rng; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; + +use super::{get_sequence_number_from_message, AttestTokenData, TokenBridgeInstructions}; + +// Command Name +const NAME: &str = "attest_token"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/attest.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub message: Wallet, + #[serde(with = "value::pubkey")] + pub mint: Pubkey, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let emitter = Pubkey::find_program_address(&[b"emitter"], &token_bridge_program_id).0; + + // SPL Metadata + + let spl_metadata = Pubkey::find_program_address( + &[ + b"metadata".as_ref(), + mpl_token_metadata::ID.as_ref(), + input.mint.as_ref(), + ], + &mpl_token_metadata::ID, + ) + .0; + + // Mint Metadata + let seeds = &[b"meta".as_ref(), input.mint.as_ref()]; + let mint_meta = Pubkey::find_program_address(seeds, &token_bridge_program_id).0; + + let bridge = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new(config_key, false), + AccountMeta::new_readonly(input.mint, false), + AccountMeta::new_readonly(mint_meta, false), + AccountMeta::new_readonly(spl_metadata, false), + // Bridge accounts + AccountMeta::new(bridge, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, false), + AccountMeta::new(sequence, false), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + // Dependencies + AccountMeta::new(sysvar::rent::id(), false), + AccountMeta::new(system_program::id(), false), + // Program + AccountMeta::new_readonly(wormhole_core_program_id, false), + ], + data: ( + TokenBridgeInstructions::AttestToken, + AttestTokenData { nonce }, + ) + .try_to_vec()?, + }; + + let message_pubkey = input.message.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.message].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "spl_metadata" => spl_metadata, + "mint_metadata" => mint_meta, + "emitter"=>emitter.to_string(), + // "sequence"=>sequence_data.sequence.to_string(), + }, + ) + .await? + .signature; + + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + Ok(Output { + signature, + sequence, + }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/complete_native.rs b/crates/cmds-solana/src/wormhole/token_bridge/complete_native.rs new file mode 100644 index 00000000..4c586865 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/complete_native.rs @@ -0,0 +1,160 @@ +use crate::wormhole::{PostVAAData, VAA}; + +use crate::prelude::*; + +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; +use wormhole_sdk::token::Message; + +use super::{CompleteNativeData, PayloadTransfer, TokenBridgeInstructions}; + +// Command Name +const NAME: &str = "complete_native"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/complete_native.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub vaa: bytes::Bytes, + pub payload: wormhole_sdk::token::Message, + pub vaa_hash: bytes::Bytes, + #[serde(default, with = "value::pubkey::opt")] + pub fee_recipient: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let payload: PayloadTransfer = match input.payload { + Message::Transfer { + amount, + token_address, + token_chain, + recipient, + recipient_chain, + fee, + } => PayloadTransfer { + amount, + token_address: token_address.0, + token_chain: token_chain.into(), + to: recipient.into(), + to_chain: recipient_chain.into(), + fee, + }, + // ignore other arms + _ => { + return Err(anyhow::anyhow!("Payload content not supported")); + } + }; + + let to: Pubkey = Pubkey::from(payload.to.0); + let mint = Pubkey::from(payload.token_address); + + let custody_key = Pubkey::find_program_address(&[mint.as_ref()], &token_bridge_program_id).0; + let custody_signer = + Pubkey::find_program_address(&[b"custody_signer"], &token_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + let vaa: PostVAAData = vaa.into(); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let claim_key = Pubkey::find_program_address( + &[ + vaa.emitter_address.as_ref(), + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.sequence.to_be_bytes().as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new(claim_key, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new(to, false), + if let Some(fee_r) = input.fee_recipient { + AccountMeta::new(fee_r, false) + } else { + AccountMeta::new(to, false) + }, + AccountMeta::new(custody_key, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(custody_signer, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + // Program + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: ( + TokenBridgeInstructions::CompleteNative, + CompleteNativeData {}, + ) + .try_to_vec()?, + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "mint" => mint, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/complete_transfer_wrapped.rs b/crates/cmds-solana/src/wormhole/token_bridge/complete_transfer_wrapped.rs new file mode 100644 index 00000000..235bfd1d --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/complete_transfer_wrapped.rs @@ -0,0 +1,207 @@ +use super::{Address, CompleteWrappedData, PayloadTransfer, TokenBridgeInstructions}; +use crate::prelude::*; +use crate::wormhole::{PostVAAData, VAA}; +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::commitment_config::CommitmentConfig; +use solana_sdk::pubkey::Pubkey; +use tracing::info; +use wormhole_sdk::token::Message; + +// Command Name +const NAME: &str = "complete_transfer_wrapped"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/token_bridge/complete_transfer_wrapped.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub vaa: bytes::Bytes, + pub payload: wormhole_sdk::token::Message, + pub vaa_hash: bytes::Bytes, + #[serde(default, with = "value::pubkey::opt")] + pub fee_recipient: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + let vaa: PostVAAData = vaa.into(); + + let payload: PayloadTransfer = match input.payload { + Message::Transfer { + amount, + token_address, + token_chain, + recipient, + recipient_chain, + fee, + } => PayloadTransfer { + amount, + token_address: token_address.0, + token_chain: token_chain.into(), + to: Address(recipient.0), + to_chain: recipient_chain.into(), + fee, + }, + // ignore other arms + _ => { + return Err(anyhow::anyhow!("Payload content not supported")); + } + }; + + let to = Pubkey::from(payload.to.0); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let claim_key = Pubkey::find_program_address( + &[ + vaa.emitter_address.as_ref(), + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.sequence.to_be_bytes().as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let mint = Pubkey::find_program_address( + &[ + b"wrapped", + payload.token_chain.to_be_bytes().as_ref(), + payload.token_address.as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let mint_meta = + Pubkey::find_program_address(&[b"meta", mint.as_ref()], &token_bridge_program_id).0; + + let mint_authority = + Pubkey::find_program_address(&[b"mint_signer"], &token_bridge_program_id).0; + + // Check if the associated token account exists + let associated_token = + spl_associated_token_account::get_associated_token_address(&input.payer.pubkey(), &mint); + + let associated_token_exists = match ctx + .solana_client + .get_account_with_commitment(&associated_token, CommitmentConfig::confirmed()) + .await + { + Ok(response) => match response.value { + Some(_) => Ok(true), + None => Ok(false), + }, + Err(_) => Err(crate::Error::AccountNotFound(associated_token)), + }?; + + // add associated token account instruction if it doesn't exist + let associated_token_ix = + spl_associated_token_account::instruction::create_associated_token_account( + &input.payer.pubkey(), + &input.payer.pubkey(), + &mint, + &spl_token::id(), + ); + + info!("associated_token_exists: {:?}", associated_token_exists); + + info!("to: {:?}", to); + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new(claim_key, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new(to, false), + if let Some(fee_r) = input.fee_recipient { + AccountMeta::new(fee_r, false) + } else { + AccountMeta::new(to, false) + }, + AccountMeta::new(mint, false), + AccountMeta::new_readonly(mint_meta, false), + AccountMeta::new_readonly(mint_authority, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(wormhole_core_program_id, false), + // Program + ], + data: ( + TokenBridgeInstructions::CompleteWrapped, + CompleteWrappedData {}, + ) + .try_to_vec()?, + }; + + let instructions = if associated_token_exists { + vec![ix] + } else { + vec![associated_token_ix, ix] + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "mint_metadata" => mint_meta, + "mint" => mint, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/create_wrapped.rs b/crates/cmds-solana/src/wormhole/token_bridge/create_wrapped.rs new file mode 100644 index 00000000..6ac71d10 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/create_wrapped.rs @@ -0,0 +1,174 @@ +use super::{CreateWrappedData, PayloadAssetMeta, TokenBridgeInstructions}; +use crate::prelude::*; +use crate::wormhole::{PostVAAData, VAA}; +use borsh::BorshSerialize; +use solana_program::{instruction::AccountMeta, system_program, sysvar}; +use solana_sdk::pubkey::Pubkey; +use tracing::info; +use wormhole_sdk::token::Message; + +// Command Name +const NAME: &str = "create_wrapped"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/create_wrapped.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub vaa: bytes::Bytes, + pub payload: wormhole_sdk::token::Message, + pub vaa_hash: bytes::Bytes, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let vaa = + VAA::deserialize(&input.vaa).map_err(|_| anyhow::anyhow!("Failed to deserialize VAA"))?; + let vaa: PostVAAData = vaa.into(); + + let payload: PayloadAssetMeta = match input.payload { + Message::AssetMeta { + token_address, + token_chain, + decimals, + symbol, + name, + } => PayloadAssetMeta { + token_address: token_address.0, + token_chain: token_chain.into(), + decimals, + symbol: symbol.to_string(), + name: name.to_string(), + }, + // ignore other arms + _ => { + return Err(anyhow::anyhow!("Payload content not supported")); + } + }; + + info!("payload: {:?}", payload); + + let message = + Pubkey::find_program_address(&[b"PostedVAA", &input.vaa_hash], &wormhole_core_program_id).0; + + let claim_key = Pubkey::find_program_address( + &[ + vaa.emitter_address.as_ref(), + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.sequence.to_be_bytes().as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let endpoint = Pubkey::find_program_address( + &[ + vaa.emitter_chain.to_be_bytes().as_ref(), + vaa.emitter_address.as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + let mint = Pubkey::find_program_address( + &[ + b"wrapped", + payload.token_chain.to_be_bytes().as_ref(), + payload.token_address.as_ref(), + ], + &token_bridge_program_id, + ) + .0; + + info!("payload token address: {:?}", payload.token_address); + + let mint_meta = + Pubkey::find_program_address(&[b"meta", mint.as_ref()], &token_bridge_program_id).0; + + let mint_authority = + Pubkey::find_program_address(&[b"mint_signer"], &token_bridge_program_id).0; + + // SPL Metadata + let spl_metadata = Pubkey::find_program_address( + &[ + b"metadata".as_ref(), + mpl_token_metadata::ID.as_ref(), + mint.as_ref(), + ], + &mpl_token_metadata::ID, + ) + .0; + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(endpoint, false), + AccountMeta::new_readonly(message, false), + AccountMeta::new(claim_key, false), + AccountMeta::new(mint, false), + AccountMeta::new(mint_meta, false), + AccountMeta::new(spl_metadata, false), + AccountMeta::new_readonly(mint_authority, false), + // Dependencies + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + // Program + AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), + ], + data: (TokenBridgeInstructions::CreateWrapped, CreateWrappedData {}).try_to_vec()?, + }; + + info!("ix: {:?}", ix); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "spl_metadata" => spl_metadata, + "mint_metadata" => mint_meta, + "mint" => mint, + }, + ) + .await? + .signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/eth/attest_from_eth.rs b/crates/cmds-solana/src/wormhole/token_bridge/eth/attest_from_eth.rs new file mode 100644 index 00000000..f9fecf2c --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/eth/attest_from_eth.rs @@ -0,0 +1,172 @@ +use tracing::info; + +use crate::{prelude::*, wormhole::token_bridge::eth::Response as ServerlessOutput}; + +// Command Name +const NAME: &str = "attest_from_eth"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/token_bridge/eth/attest_from_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub token: String, + pub network_name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + response: ServerlessOutput, + emitter: String, + sequence: String, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + } + + let payload = Payload { + network_name: input.network_name, + token: input.token, + keypair: input.keypair, + }; + info!("payload: {:?}", payload); + + let response: ServerlessOutput = ctx + .http + .post("https://space-operator.deno.dev/api/attest_from_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let emitter = response.output.emitter_address.clone(); + let sequence = response.output.sequence.clone(); + + Ok(Output { + response, + emitter, + sequence, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + } + + #[tokio::test] + async fn need_key_test_local() { + let _json_input = r#"{ + "output": { + "receipt": { + "to": "0xDB5492265f6038831E89f495670FF909aDe94bd9", + "from": "0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6", + "contractAddress": null, + "transactionIndex": 20, + "gasUsed": { + "type": "BigNumber", + "hex": "0x010a74" + }, + "logsBloom": "0x00000000000100000000000000000010000000000000000000000000000000010010000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3", + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "logs": [ + { + "transactionIndex": 20, + "blockNumber": 3957578, + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "address": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", + "topics": [ + "0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2", + "0x000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000018c00000000000000000000000000000000000000000000000000000000fc50010000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006402000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a72712124552435f53594d424f4c000000000000000000000000000000000000000000004d7945524332300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 20, + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3" + } + ], + "blockNumber": 3957578, + "confirmations": 1, + "cumulativeGasUsed": { + "type": "BigNumber", + "hex": "0x2e6cd8" + }, + "effectiveGasPrice": { + "type": "BigNumber", + "hex": "0x59689a64" + }, + "status": 1, + "type": 2, + "byzantium": true, + "events": [ + { + "transactionIndex": 20, + "blockNumber": 3957578, + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "address": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", + "topics": [ + "0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2", + "0x000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000018c00000000000000000000000000000000000000000000000000000000fc50010000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006402000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a72712124552435f53594d424f4c000000000000000000000000000000000000000000004d7945524332300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 20, + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3" + } + ] + }, + "emitterAddress": "000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a7", + "sequence": "396" + } + }"#; + + async fn test(payload: Payload) -> Result<(String, String), reqwest::Error> { + let client = reqwest::Client::new(); + let response = client + .post("https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/attest_from_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let emitter = response.output.emitter_address; + let sequence = response.output.sequence; + + Ok((emitter, sequence)) + } + + let payload = Payload { + network_name: "devnet".into(), + token: "0xDB5492265f6038831E89f495670FF909aDe94bd9".into(), + keypair: "".into(), + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/eth/create_wrapped_on_eth.rs b/crates/cmds-solana/src/wormhole/token_bridge/eth/create_wrapped_on_eth.rs new file mode 100644 index 00000000..d1893091 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/eth/create_wrapped_on_eth.rs @@ -0,0 +1,164 @@ +use tracing::info; + +use crate::{ + prelude::*, + wormhole::token_bridge::{ + eth::{hex_to_address, CreateWrappedResponse}, + Address, + }, +}; + +use super::Receipt; + +// Command Name +const NAME: &str = "create_wrapped_on_eth"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/token_bridge/eth/create_wrapped_on_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub network_name: String, + pub signed_vaa: String, + #[serde(with = "value::pubkey")] + pub token: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + receipt: Receipt, + address: Address, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + token: String, + } + + let payload = Payload { + network_name: input.network_name, + keypair: input.keypair, + signed_vaa: input.signed_vaa, + token: input.token.to_string(), + }; + + let response: CreateWrappedResponse = ctx + .http + .post("https://space-operator.deno.dev/api/create_wrapped_on_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + info!("response: {:?}", response); + let receipt = response.output.receipt; + + // token contract address on ETH + let address = hex_to_address(&response.output.address).map_err(anyhow::Error::msg)?; + + Ok(Output { receipt, address }) +} + +#[cfg(test)] +mod tests { + use crate::wormhole::token_bridge::eth::CreateWrappedResponse; + use serde::{Deserialize, Serialize}; + use std::{fmt::Write, num::ParseIntError}; + use wormhole_sdk::Address; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + token: String, + } + + #[tokio::test] + async fn need_key_test_local() { + async fn test(payload: Payload) -> Result { + let client = reqwest::Client::new(); + let json = client + .post( + "https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/create_wrapped_on_eth", + ) + .json(&payload) + .send() + .await? + .json::() + .await?; + + dbg!(&json); + + let response = serde_json::from_value(json).unwrap(); + + Ok(response) + } + + let payload = Payload { + network_name: "devnet".into(), + keypair: "".into(), + signed_vaa: "AQAAAAABAG9er/MmJMZA+TXKhvruR6h07pgDs4jvGEX32tA/X+fPJoLN5GdryI2AnnKLeN/y2DG1XVfqQIjSwVmJrdFQ1JUAZNvkF/RT7/MAATsmQJ+Kre0/XdyhhGlapqD6gpsMhcr4SFYySJbSFMqYAAAAAAAAYqYgAmc+E+tQG8MVnhfmdvaOmyILEFx3DYlI+fuqLuFPMiDtAAEJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(), + token:"7x1tu6xjxhCNnnwTNytmGYL6w4Cwe3PDMo7gmfc89GHa".into() + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } + + pub fn decode_hex(s: &str) -> Result, ParseIntError> { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect() + } + + #[test] + fn hex_to_address() -> Result<(), anyhow::Error> { + let address = "0xc15B6515aC32a91ACe0b8fABEBBB924a6CD4A539"; + + if !address.starts_with("0x") { + return Err(anyhow::anyhow!("invalid address {}", address)); + }; + + let stripped_address = address.split_at(2).1; + + let bytes = decode_hex(stripped_address).unwrap(); + let mut array = [0u8; 32]; + array[32 - bytes.len()..].copy_from_slice(&bytes); + let address: Address = Address(array); + // dbg!(address.to_string()); + + // back to string + // remove left zero padding + let mut s = String::new(); + s.push_str("0x"); + for b in address.0.iter() { + if *b == 0 { + continue; + } + write!(&mut s, "{:02x}", b).unwrap(); + } + // dbg!(s); + Ok(()) + } +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/eth/mod.rs b/crates/cmds-solana/src/wormhole/token_bridge/eth/mod.rs new file mode 100644 index 00000000..0037db42 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/eth/mod.rs @@ -0,0 +1,165 @@ +use std::num::ParseIntError; + +use serde::{Deserialize, Serialize}; + +use super::Address; + +pub mod attest_from_eth; +pub mod create_wrapped_on_eth; +pub mod redeem_on_eth; +pub mod transfer_from_eth; + +#[derive(Serialize, Deserialize, Debug)] +struct GasUsed { + #[serde(rename = "type")] + gas_type: String, + hex: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct EffectiveGasPrice { + #[serde(rename = "type")] + gas_type: String, + hex: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Receipt { + to: String, + from: String, + contract_address: Option, + #[serde(rename = "transactionIndex")] + transaction_index: u32, + #[serde(rename = "gasUsed")] + gas_used: GasUsed, + #[serde(rename = "logsBloom")] + logs_bloom: String, + #[serde(rename = "blockHash")] + block_hash: String, + #[serde(rename = "transactionHash")] + transaction_hash: String, + logs: Vec, + #[serde(rename = "blockNumber")] + block_number: u32, + confirmations: u32, + #[serde(rename = "cumulativeGasUsed")] + cumulative_gas_used: GasUsed, + #[serde(rename = "effectiveGasPrice")] + effective_gas_price: EffectiveGasPrice, + status: u32, + r#type: u32, + byzantium: bool, + events: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Log { + #[serde(rename = "transactionIndex")] + transaction_index: u32, + #[serde(rename = "blockNumber")] + block_number: u32, + #[serde(rename = "transactionHash")] + transaction_hash: String, + address: String, + topics: Vec, + data: String, + #[serde(rename = "logIndex")] + log_index: u32, + #[serde(rename = "blockHash")] + block_hash: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub receipt: Receipt, + #[serde(rename = "emitterAddress")] + pub emitter_address: String, + pub sequence: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TransferFromEth { + pub receipt: Receipt, + #[serde(rename = "emitterAddress")] + pub emitter_address: String, + pub sequence: String, + pub recipient_ata: String, + pub mint: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TransferFromEthResponse { + pub output: TransferFromEth, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Response { + pub output: Output, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetForeignAddress { + pub output: AddressOnEth, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct AddressOnEth { + pub address: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct CreateWrappedOutput { + receipt: Receipt, + address: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct CreateWrappedResponse { + output: CreateWrappedOutput, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RedeemOnEthOutput { + pub receipt: Receipt, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RedeemOnEthResponse { + pub output: RedeemOnEthOutput, +} + +// Function to Decode Hex String to Bytes +pub fn decode_hex(s: &str) -> Result, ParseIntError> { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect() +} + +// Function to Convert Hex String to Address +pub fn hex_to_address(hex: &str) -> Result { + if !hex.starts_with("0x") { + return Err(anyhow::anyhow!("invalid address {}", hex)); + }; + + let stripped_address = hex.split_at(2).1; + + let bytes = decode_hex(stripped_address).unwrap(); + let mut array = [0u8; 32]; + array[32 - bytes.len()..].copy_from_slice(&bytes); + let address: Address = Address(array); + Ok(address) +} + +// let to: String = format!("{}", Pubkey::new_from_array(payload.to.0)); +// info!("to {:?}", to); + +// let to: String = format!("{}", Address(payload.token_address)); +// info!("token: {:?}", to); + +// let hex = hex::encode(&payload.token_address); +// info!("hex_token: {:?}", hex); + +// format!("0x{}", hex); + +// info!("payload: {:?}", payload); diff --git a/crates/cmds-solana/src/wormhole/token_bridge/eth/redeem_on_eth.rs b/crates/cmds-solana/src/wormhole/token_bridge/eth/redeem_on_eth.rs new file mode 100644 index 00000000..bcd78b3e --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/eth/redeem_on_eth.rs @@ -0,0 +1,172 @@ +use crate::{ + prelude::*, + wormhole::token_bridge::eth::{Receipt, RedeemOnEthResponse}, +}; + +// Command Name +const NAME: &str = "redeem_on_eth"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/eth/redeem_on_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub network_name: String, + pub signed_vaa: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + receipt: Receipt, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + } + + let payload = Payload { + network_name: input.network_name, + keypair: input.keypair, + signed_vaa: input.signed_vaa, + }; + + let response: RedeemOnEthResponse = ctx + .http + .post("https://space-operator.deno.dev/api/redeem_on_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let receipt: Receipt = response.output.receipt; + + // to is the wormhole token bridge contract + // from is the recipient + // logs/address is the transferred token contract address + + Ok(Output { receipt }) +} + +#[cfg(test)] +mod tests { + use crate::wormhole::token_bridge::eth::RedeemOnEthResponse; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + keypair: String, + #[serde(rename = "signedVAA")] + signed_vaa: String, + } + + #[tokio::test] + async fn need_key_test_local() { + let _json_response = r#"{ + "output": Object { + "receipt": Object { + "to": String("0xDB5492265f6038831E89f495670FF909aDe94bd9"), + "from": String("0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6"), + "contractAddress": Null, + "transactionIndex": Number(24), + "gasUsed": Object { + "type": String("BigNumber"), + "hex": String("0x02916e"), + }, + "logsBloom": String("0x00000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000040000008000400000000000000000000000000000000000000000000020000000000000000000800000000000002000000000010000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000"), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "logs": Array [ + Object { + "transactionIndex": Number(24), + "blockNumber": Number(4205532), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "address": String("0x44C80265b027b4Fed63C177f3Ed9C174a0f417d1"), + "topics": Array [ + String("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + String("0x0000000000000000000000000000000000000000000000000000000000000000"), + String("0x000000000000000000000000dd6c5b9ea3ac0fb5387e5e6b482788d5f70772a6"), + ], + "data": String("0x00000000000000000000000000000000000000000000000000000002540be400"), + "logIndex": Number(45), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + }, + ], + "blockNumber": Number(4205532), + "confirmations": Number(1), + "cumulativeGasUsed": Object { + "type": String("BigNumber"), + "hex": String("0x763888"), + }, + "effectiveGasPrice": Object { + "type": String("BigNumber"), + "hex": String("0x68f8dff3"), + }, + "status": Number(1), + "type": Number(2), + "byzantium": Bool(true), + "events": Array [ + Object { + "transactionIndex": Number(24), + "blockNumber": Number(4205532), + "transactionHash": String("0xc3da8759b01f0f04ff0d0aad5594d69888bd5d2cde0e0236248fcdb50b51dcab"), + "address": String("0x44C80265b027b4Fed63C177f3Ed9C174a0f417d1"), + "topics": Array [ + String("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + String("0x0000000000000000000000000000000000000000000000000000000000000000"), + String("0x000000000000000000000000dd6c5b9ea3ac0fb5387e5e6b482788d5f70772a6"), + ], + "data": String("0x00000000000000000000000000000000000000000000000000000002540be400"), + "logIndex": Number(45), + "blockHash": String("0x910302d187cea8989abf11f08994b49508b6e9bec8f15c1e837370af722c70c0"), + }, + ], + }, + }, + }"#; + + async fn test(payload: Payload) -> Result { + let client = reqwest::Client::new(); + let json = client + .post("https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/redeem_on_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + dbg!(&json); + + let response = serde_json::from_value(json).unwrap(); + + Ok(response) + } + + let payload = Payload { + network_name: "devnet".into(), + keypair: "0x1bb0ed141673d3228d6dc10806f0de5ee6522695160aed8fb99e487a9abc622c".into(), + signed_vaa: "AQAAAAABACoXm1NP//WChroqx/awXt8eWpsWcsdVEBjRRNflDXM+bk6OLrXSCTlF5taxaDv6r9juiBz2r0gQHGcJUoqUmxABZPIikc4LdPUAATsmQJ+Kre0/XdyhhGlapqD6gpsMhcr4SFYySJbSFMqYAAAAAAAAYwsgAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7msoA0h+0dT4vg35B/rPGEAri/K6uW549WIGBY3kkF9qrQIIAAQAAAAAAAAAAAAAAAN1sW56jrA+1OH5ea0gniNX3B3KmJxIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==".into(), + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/eth/transfer_from_eth.rs b/crates/cmds-solana/src/wormhole/token_bridge/eth/transfer_from_eth.rs new file mode 100644 index 00000000..300c798f --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/eth/transfer_from_eth.rs @@ -0,0 +1,186 @@ +use crate::{prelude::*, wormhole::token_bridge::eth::TransferFromEthResponse}; +use tracing::info; + +// Command Name +const NAME: &str = "transfer_from_eth"; + +const DEFINITION: &str = + flow_lib::node_definition!("wormhole/token_bridge/eth/transfer_from_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub keypair: String, + pub token: String, + pub network_name: String, + pub recipient: String, + pub amount: f64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + response: TransferFromEthResponse, + emitter: String, + sequence: String, + recipient_ata: Pubkey, + mint: Pubkey, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + recipient: String, + amount: String, + } + + let payload = Payload { + network_name: input.network_name, + token: input.token, + keypair: input.keypair, + recipient: input.recipient, + amount: input.amount.to_string(), + }; + + let response: TransferFromEthResponse = ctx + .http + .post("https://space-operator.deno.dev/api/transfer_from_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let emitter = response.output.emitter_address.clone(); + let sequence = response.output.sequence.clone(); + + let recipient_ata = response.output.recipient_ata.parse()?; + let mint = response.output.mint.parse()?; + + info!("recipient_ata: {:?}", recipient_ata); + info!("mint: {:?}", mint); + Ok(Output { + response, + emitter, + sequence, + recipient_ata, + mint, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wormhole::token_bridge::eth::Response as ServerlessOutput; + + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network_name: String, + token: String, + keypair: String, + } + + #[tokio::test] + async fn need_key_test_local() { + let _json_input = r#"{ + "output": { + "receipt": { + "to": "0xDB5492265f6038831E89f495670FF909aDe94bd9", + "from": "0xdD6c5B9eA3Ac0FB5387E5e6B482788d5F70772A6", + "contractAddress": null, + "transactionIndex": 20, + "gasUsed": { + "type": "BigNumber", + "hex": "0x010a74" + }, + "logsBloom": "0x00000000000100000000000000000010000000000000000000000000000000010010000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3", + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "logs": [ + { + "transactionIndex": 20, + "blockNumber": 3957578, + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "address": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", + "topics": [ + "0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2", + "0x000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000018c00000000000000000000000000000000000000000000000000000000fc50010000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006402000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a72712124552435f53594d424f4c000000000000000000000000000000000000000000004d7945524332300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 20, + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3" + } + ], + "blockNumber": 3957578, + "confirmations": 1, + "cumulativeGasUsed": { + "type": "BigNumber", + "hex": "0x2e6cd8" + }, + "effectiveGasPrice": { + "type": "BigNumber", + "hex": "0x59689a64" + }, + "status": 1, + "type": 2, + "byzantium": true, + "events": [ + { + "transactionIndex": 20, + "blockNumber": 3957578, + "transactionHash": "0x62f2b7d16c483b3ec76962fb5337b6a442458af0027941e717770afbb3769b08", + "address": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", + "topics": [ + "0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2", + "0x000000000000000000000000db5492265f6038831e89f495670ff909ade94bd9" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000018c00000000000000000000000000000000000000000000000000000000fc50010000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006402000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a72712124552435f53594d424f4c000000000000000000000000000000000000000000004d7945524332300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 20, + "blockHash": "0x974ba2485dbb37cf9253cec90bab96f96b806b1ee1db8f8d4833f69258f635d3" + } + ] + }, + "emitterAddress": "000000000000000000000000410a8b1502f01f2887f0b05b22cc934d45a628a7", + "sequence": "396" + } + }"#; + + async fn test(payload: Payload) -> Result<(String, String), reqwest::Error> { + let client = reqwest::Client::new(); + let response = client + .post("https://gygvoikm3c.execute-api.us-east-1.amazonaws.com/attest_from_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + let emitter = response.output.emitter_address; + let sequence = response.output.sequence; + + Ok((emitter, sequence)) + } + + let payload = Payload { + network_name: "devnet".into(), + token: "0xDB5492265f6038831E89f495670FF909aDe94bd9".into(), + keypair: "".into(), + }; + + let res = test(payload).await.unwrap(); + dbg!(res); + } +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/initialize.rs b/crates/cmds-solana/src/wormhole/token_bridge/initialize.rs new file mode 100644 index 00000000..550a9f72 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/initialize.rs @@ -0,0 +1,73 @@ +use super::TokenBridgeInstructions; +use crate::prelude::*; +use borsh::BorshSerialize; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; + +// Command Name +const NAME: &str = "initialize_token_bridge"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/initialize.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new(config_key, false), + // Dependencies + AccountMeta::new(solana_program::sysvar::rent::id(), false), + AccountMeta::new(solana_program::system_program::id(), false), + ], + data: ( + TokenBridgeInstructions::Initialize, + wormhole_core_program_id, + ) + .try_to_vec()?, + }; + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer].into(), + instructions: [ix].into(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/mod.rs b/crates/cmds-solana/src/wormhole/token_bridge/mod.rs new file mode 100644 index 00000000..94b77c02 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/mod.rs @@ -0,0 +1,226 @@ +use std::fmt; + +use crate::wormhole::ForeignAddress; +use borsh::{BorshDeserialize, BorshSerialize}; + +use byteorder::{ByteOrder, LittleEndian}; +use flow_lib::Context; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; +use solana_sdk::commitment_config::CommitmentConfig; +use tracing::info; +use wormhole_sdk::Amount; + +pub mod attest; +pub mod complete_native; +pub mod complete_transfer_wrapped; +pub mod create_wrapped; +pub mod initialize; +pub mod transfer_native; +pub mod transfer_wrapped; + +pub mod eth; + +#[repr(u8)] +#[derive(BorshSerialize, BorshDeserialize)] +enum TokenBridgeInstructions { + Initialize, + AttestToken, + CompleteNative, + CompleteWrapped, + TransferWrapped, + TransferNative, + RegisterChain, + CreateWrapped, + UpgradeContract, + CompleteNativeWithPayload, + CompleteWrappedWithPayload, + TransferWrappedWithPayload, + TransferNativeWithPayload, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct AttestTokenData { + pub nonce: u32, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CreateWrappedData {} + +#[derive(PartialEq, Debug)] +pub struct PayloadAssetMeta { + /// Address of the token. Left-zero-padded if shorter than 32 bytes + pub token_address: ForeignAddress, + /// Chain ID of the token + pub token_chain: ChainID, + /// Number of decimals of the token + pub decimals: u8, + /// Symbol of the token + pub symbol: String, + /// Name of the token + pub name: String, +} + +#[derive( + Serialize, Deserialize, BorshDeserialize, BorshSerialize, Default, PartialEq, Debug, Clone, +)] +pub struct Address(pub [u8; 32]); + +// implement from wormhole_sdk::Address to Address +impl From for Address { + fn from(address: wormhole_sdk::Address) -> Self { + let mut addr = [0u8; 32]; + addr.copy_from_slice(&address.0); + Address(addr) + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in self.0 { + write!(f, "{b:02x}")?; + } + + Ok(()) + } +} + +pub type ChainID = u16; + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteWrappedData {} + +#[derive(PartialEq, Debug, Clone)] +pub struct PayloadTransfer { + /// Amount being transferred (big-endian uint256) + pub amount: Amount, + /// Address of the token. Left-zero-padded if shorter than 32 bytes + pub token_address: ForeignAddress, + /// Chain ID of the token + pub token_chain: ChainID, + /// Address of the recipient. Left-zero-padded if shorter than 32 bytes + pub to: Address, + /// Chain ID of the recipient + pub to_chain: ChainID, + /// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount. + pub fee: Amount, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteWrappedWithPayloadData {} + +#[derive(PartialEq, Debug, Clone)] +pub struct PayloadTransferWithPayload { + /// Amount being transferred (big-endian uint256) + pub amount: Amount, + /// Address of the token. Left-zero-padded if shorter than 32 bytes + pub token_address: ForeignAddress, + /// Chain ID of the token + pub token_chain: ChainID, + /// Address of the recipient. Left-zero-padded if shorter than 32 bytes + pub to: Address, + /// Chain ID of the recipient + pub to_chain: ChainID, + /// Sender of the transaction + pub from_address: Address, + /// Arbitrary payload + pub payload: Vec, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct TransferWrappedData { + pub nonce: u32, + pub amount: u64, + pub fee: u64, + pub target_address: Address, + pub target_chain: ChainID, +} + +#[derive(Debug, BorshDeserialize, BorshSerialize, Clone)] +pub struct TransferTokensArgs { + pub nonce: u32, + pub amount: u64, + pub relayer_fee: u64, + pub recipient: [u8; 32], + pub recipient_chain: u16, +} + +#[derive(Default, BorshSerialize, BorshDeserialize, Serialize)] +pub struct SequenceTracker { + pub sequence: u64, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct TransferNativeData { + pub nonce: u32, + pub amount: u64, + pub fee: u64, + pub target_address: Address, + pub target_chain: ChainID, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CompleteNativeData {} + +pub async fn get_sequence_number( + ctx: &Context, + sequence: Pubkey, +) -> Result { + let commitment = CommitmentConfig::confirmed(); + + let response = ctx + .solana_client + .get_account_with_commitment(&sequence, commitment) + .await + .map_err(|e| { + tracing::error!("Error: {:?}", e); + crate::Error::AccountNotFound(sequence) + })?; + + info!("response: {:?}", response); + let sequence_account = match response.value { + Some(account) => account, + None => return Err(crate::Error::AccountNotFound(sequence)), + }; + + let mut sequence_data: &[u8] = &sequence_account.data; + let sequence_data: SequenceTracker = + SequenceTracker::deserialize(&mut sequence_data).map_err(|_| { + tracing::error!( + "Invalid data for sequence: {:?}", + crate::Error::InvalidAccountData(sequence) + ); + crate::Error::InvalidAccountData(sequence) + })?; + info!("sequence_data: {:?}", sequence_data.sequence); + Ok(sequence_data) +} + +// https://github.com/wormhole-foundation/connect-sdk/blob/dc90598ecadea0319a83a983ae87667f44a3b5f2/platforms/solana/protocols/core/src/core.ts#L294C17-L294C17 +pub async fn get_sequence_number_from_message( + ctx: &Context, + message: Pubkey, +) -> Result { + let commitment = CommitmentConfig::confirmed(); + + let response = ctx + .solana_client + .get_account_with_commitment(&message, commitment) + .await + .map_err(|e| { + tracing::error!("Error: {:?}", e); + crate::Error::AccountNotFound(message) + })?; + + info!("response: {:?}", response); + let sequence_account = match response.value { + Some(account) => account, + None => return Err(crate::Error::AccountNotFound(message)), + }; + + let sequence_data: &[u8] = &sequence_account.data; + let sequence: u64 = LittleEndian::read_u64(&sequence_data[49..57]); + + info!("sequence_data: {:?}", sequence); + Ok(sequence.to_string()) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/transfer_native.rs b/crates/cmds-solana/src/wormhole/token_bridge/transfer_native.rs new file mode 100644 index 00000000..7ab18fd3 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/transfer_native.rs @@ -0,0 +1,166 @@ +use crate::prelude::*; + +use borsh::BorshSerialize; +use rand::Rng; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; + +use super::{ + eth::hex_to_address, get_sequence_number_from_message, TokenBridgeInstructions, + TransferNativeData, +}; + +// Command Name +const NAME: &str = "transfer_native"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/transfer_native.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub message: Wallet, + #[serde(with = "value::pubkey")] + pub from: Pubkey, + #[serde(with = "value::pubkey")] + pub mint: Pubkey, + // 1 = 1,000,000,000 + pub amount: u64, + pub fee: u64, + pub target_address: String, + pub target_chain: u16, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let mint = input.mint; + + let custody_key = Pubkey::find_program_address(&[mint.as_ref()], &token_bridge_program_id).0; + + let authority_signer = + Pubkey::find_program_address(&[b"authority_signer"], &token_bridge_program_id).0; + + let custody_signer = + Pubkey::find_program_address(&[b"custody_signer"], &token_bridge_program_id).0; + + let emitter = Pubkey::find_program_address(&[b"emitter"], &token_bridge_program_id).0; + + let bridge_config = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + let address = hex_to_address(&input.target_address).map_err(anyhow::Error::msg)?; + + let wrapped_data = TransferNativeData { + nonce, + amount: input.amount, + fee: input.fee, + target_address: address, + target_chain: input.target_chain, + }; + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new(input.from, false), + AccountMeta::new(mint, false), + AccountMeta::new(custody_key, false), + AccountMeta::new_readonly(authority_signer, false), + AccountMeta::new_readonly(custody_signer, false), + AccountMeta::new(bridge_config, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, false), + AccountMeta::new(sequence, false), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), + // Dependencies + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + // Program + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(wormhole_core_program_id, false), + ], + data: (TokenBridgeInstructions::TransferNative, wrapped_data).try_to_vec()?, + }; + + let instructions = [ + spl_token::instruction::approve( + &spl_token::id(), + &input.from, + &authority_signer, + &input.payer.pubkey(), + &[], + input.amount, + ) + .unwrap(), + ix, + ] + .into(); + + let message_pubkey = input.message.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.message].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + // let sequence_data: SequenceTracker = get_sequence_number(&ctx, sequence).await?; + + let signature = ctx + .execute( + ins, + value::map! { + "custody" => custody_key, + "custody_signer" => custody_signer, + // "sequence" => sequence_data.sequence.to_string(), + "emitter" => emitter.to_string(), + }, + ) + .await? + .signature; + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + + Ok(Output { + signature, + sequence, + }) +} diff --git a/crates/cmds-solana/src/wormhole/token_bridge/transfer_wrapped.rs b/crates/cmds-solana/src/wormhole/token_bridge/transfer_wrapped.rs new file mode 100644 index 00000000..8e0728d8 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/token_bridge/transfer_wrapped.rs @@ -0,0 +1,167 @@ +use super::TokenBridgeInstructions; +use crate::prelude::*; +use crate::wormhole::token_bridge::TransferTokensArgs; +use crate::wormhole::token_bridge::{eth::hex_to_address, get_sequence_number_from_message}; +use borsh::BorshSerialize; +use rand::Rng; +use solana_program::instruction::AccountMeta; +use solana_sdk::pubkey::Pubkey; +use tracing::info; + +// Command Name +const NAME: &str = "transfer_wrapped"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/token_bridge/transfer_wrapped.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub mint: Pubkey, + pub amount: u64, + pub fee: u64, + pub target_address: String, + pub target_chain: u16, + pub message: Wallet, + pub from_owner: Wallet, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, + sequence: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let token_bridge_program_id = + crate::wormhole::token_bridge_program_id(ctx.cfg.solana_client.cluster); + + let config_key = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id).0; + + let target_address = hex_to_address(&input.target_address).map_err(anyhow::Error::msg)?; + + let wrapped_meta_key = + Pubkey::find_program_address(&[b"meta", input.mint.as_ref()], &token_bridge_program_id).0; + + let authority_signer = + Pubkey::find_program_address(&[b"authority_signer"], &token_bridge_program_id).0; + info!("authority_signer: {}", authority_signer); + + let emitter = Pubkey::find_program_address(&[b"emitter"], &token_bridge_program_id).0; + let bridge_config = Pubkey::find_program_address(&[b"Bridge"], &wormhole_core_program_id).0; + + let sequence = + Pubkey::find_program_address(&[b"Sequence", emitter.as_ref()], &wormhole_core_program_id).0; + + let fee_collector = + Pubkey::find_program_address(&[b"fee_collector"], &wormhole_core_program_id).0; + + // TODO: use a real nonce + let nonce = rand::thread_rng().gen(); + + // let wrapped_data = TransferWrappedData { + // nonce, + // amount: input.amount, + // fee: input.fee, + // target_address, + // target_chain: input.target_chain, + // }; + + let wrapped_data = TransferTokensArgs { + nonce, + amount: input.amount, + relayer_fee: input.fee, + recipient: target_address.0, + recipient_chain: input.target_chain, + }; + + let from_ata = spl_associated_token_account::get_associated_token_address( + &input.from_owner.pubkey(), + &input.mint, + ); + info!("amount: {}", input.amount); + + let ix = solana_program::instruction::Instruction { + program_id: token_bridge_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new(from_ata, false), + AccountMeta::new_readonly(input.from_owner.pubkey(), true), + AccountMeta::new(input.mint, false), + AccountMeta::new_readonly(wrapped_meta_key, false), + AccountMeta::new_readonly(authority_signer, false), + AccountMeta::new(bridge_config, false), + AccountMeta::new(input.message.pubkey(), true), + AccountMeta::new_readonly(emitter, false), + AccountMeta::new(sequence, false), + AccountMeta::new(fee_collector, false), + AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), + // Dependencies + AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + // Program + AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly(wormhole_core_program_id, false), + ], + data: (TokenBridgeInstructions::TransferWrapped, wrapped_data).try_to_vec()?, + }; + + let instructions = [ + spl_token::instruction::approve( + &spl_token::id(), + &from_ata, + &authority_signer, + &input.from_owner.pubkey(), + &[], + input.amount, + )?, + ix, + ] + .into(); + + let message_pubkey = input.message.pubkey(); + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.from_owner, input.message].into(), + instructions, + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx + .execute( + ins, + value::map! { + "wrapped_meta_key" => wrapped_meta_key, + "emitter" => emitter, + }, + ) + .await? + .signature; + + let sequence = get_sequence_number_from_message(&ctx, message_pubkey).await?; + + Ok(Output { + signature, + sequence, + }) +} diff --git a/crates/cmds-solana/src/wormhole/utils/get_foreign_asset_eth.rs b/crates/cmds-solana/src/wormhole/utils/get_foreign_asset_eth.rs new file mode 100644 index 00000000..30e24565 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/utils/get_foreign_asset_eth.rs @@ -0,0 +1,62 @@ +use crate::{prelude::*, wormhole::token_bridge::eth::GetForeignAddress}; + +// Command Name +const NAME: &str = "get_foreign_asset_eth"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/utils/get_foreign_asset_eth.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub network: String, + pub token: String, + pub is_nft: bool, + pub chain_id: u16, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + address: String, +} + +async fn run(ctx: Context, input: Input) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + #[serde(rename = "networkName")] + network: String, + token: String, + #[serde(rename = "isNFT")] + is_nft: bool, + #[serde(rename = "chainId")] + chain_id: u16, + } + + let payload = Payload { + network: input.network, + token: input.token, + is_nft: input.is_nft, + chain_id: input.chain_id, + }; + + let response: GetForeignAddress = ctx + .http + .post("https://space-operator.deno.dev/api/get_foreign_asset_eth") + .json(&payload) + .send() + .await? + .json::() + .await?; + + Ok(Output { + address: response.output.address, + }) +} diff --git a/crates/cmds-solana/src/wormhole/utils/mod.rs b/crates/cmds-solana/src/wormhole/utils/mod.rs new file mode 100644 index 00000000..b0a46ae4 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/utils/mod.rs @@ -0,0 +1 @@ +pub mod get_foreign_asset_eth; diff --git a/crates/cmds-solana/src/wormhole/verify_signatures.rs b/crates/cmds-solana/src/wormhole/verify_signatures.rs new file mode 100644 index 00000000..175879b7 --- /dev/null +++ b/crates/cmds-solana/src/wormhole/verify_signatures.rs @@ -0,0 +1,142 @@ +use super::{GuardianSetData, SignatureItem, VerifySignaturesData}; +use crate::{prelude::*, wormhole::WormholeInstructions}; +use borsh::{BorshDeserialize, BorshSerialize}; +use byteorder::{LittleEndian, WriteBytesExt}; +use solana_program::{instruction::AccountMeta, sysvar}; +use solana_sdk::pubkey::Pubkey; +use std::io::Write; + +// Command Name +const NAME: &str = "verify_signatures"; + +const DEFINITION: &str = flow_lib::node_definition!("wormhole/verify_signatures.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .simple_instruction_info("signature") + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub payer: Wallet, + pub signature_set: Wallet, + pub guardian_set_index: u32, + pub signatures: Vec, + pub vaa_body: bytes::Bytes, + pub vaa_hash: bytes::Bytes, + #[serde(default = "value::default::bool_true")] + submit: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + #[serde(default, with = "value::signature::opt")] + signature: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let wormhole_core_program_id = + crate::wormhole::wormhole_core_program_id(ctx.cfg.solana_client.cluster); + + let guardian_set = Pubkey::find_program_address( + &[b"GuardianSet", &input.guardian_set_index.to_le_bytes()], + &wormhole_core_program_id, + ) + .0; + + let account: solana_sdk::account::Account = + ctx.solana_client.get_account(&guardian_set).await.unwrap(); + let guardian_set_data: GuardianSetData = + GuardianSetData::try_from_slice(&account.data).unwrap(); + + let mut signature_items: Vec = Vec::new(); + for s in input.signatures.iter() { + let mut item = SignatureItem { + signature: s.signature.to_vec(), + key: [0; 20], + index: s.index, + }; + item.key = guardian_set_data.keys[s.index as usize]; + + signature_items.push(item); + } + + let mut verify_txs: Vec> = Vec::new(); + + for chunk in signature_items.chunks(7) { + let mut secp_payload = Vec::new(); + let mut signature_status = [-1i8; 19]; + + let data_offset = 1 + chunk.len() * 11; + let message_offset = data_offset + chunk.len() * 85; + + // 1 number of signatures + secp_payload.write_u8(chunk.len() as u8)?; + + // Secp signature info description (11 bytes * n) + for (i, s) in chunk.iter().enumerate() { + secp_payload.write_u16::((data_offset + 85 * i) as u16)?; + secp_payload.write_u8(0)?; + secp_payload.write_u16::((data_offset + 85 * i + 65) as u16)?; + secp_payload.write_u8(0)?; + secp_payload.write_u16::(message_offset as u16)?; + secp_payload.write_u16::(input.vaa_hash.len() as u16)?; + secp_payload.write_u8(0)?; + signature_status[s.index as usize] = i as i8; + } + + // Write signatures and addresses + for s in chunk.iter() { + secp_payload.write_all(&s.signature)?; + secp_payload.write_all(&s.key)?; + } + + // Write body + secp_payload.write_all(&input.vaa_hash)?; + + let secp_ix = Instruction { + program_id: solana_program::secp256k1_program::id(), + data: secp_payload, + accounts: vec![], + }; + + let payload = VerifySignaturesData { + signers: signature_status, + }; + + let verify_ix = Instruction { + program_id: wormhole_core_program_id, + accounts: vec![ + AccountMeta::new(input.payer.pubkey(), true), + AccountMeta::new_readonly(guardian_set, false), + AccountMeta::new(input.signature_set.pubkey(), true), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(solana_program::system_program::id(), false), + ], + + data: (WormholeInstructions::VerifySignatures, payload).try_to_vec()?, + }; + + verify_txs.push(vec![secp_ix, verify_ix]) + } + + let ins = Instructions { + lookup_tables: None, + fee_payer: input.payer.pubkey(), + signers: [input.payer, input.signature_set].into(), + instructions: verify_txs.concat(), + }; + + let ins = input.submit.then_some(ins).unwrap_or_default(); + + let signature = ctx.execute(ins, <_>::default()).await?.signature; + + Ok(Output { signature }) +} diff --git a/crates/cmds-std/Cargo.toml b/crates/cmds-std/Cargo.toml new file mode 100644 index 00000000..e18f13a5 --- /dev/null +++ b/crates/cmds-std/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "cmds-std" +version = "0.0.0" +edition = "2021" + +[[bench]] +name = "postgrest" +harness = false + +[dependencies] +value = { workspace = true } +flow-lib = { workspace = true } + +async-trait = "0.1" +serde = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } +anyhow = { workspace = true } +bs58 = { workspace = true } +thiserror = "1" +reqwest = { version = "0.12", features = ["multipart"] } +futures-util = "0.3.29" +rust_decimal = { version = "1.32.0", features = ["serde-with-float"] } +tracing = "0.1.40" +bytes = "1.5.0" +mime_guess = "2.0.4" +postgrest = { workspace = true } +tokio = "1.33.0" +once_cell = "1.17" +url = { version = "2.5.0", features = ["serde"] } +hyper = { version = "0.14.26", default-features = false, features = ["client"] } + +[dev-dependencies] +tokio = { version = "1", features = ["rt", "macros"] } +criterion = "0.5" +futures-executor = "0.3" diff --git a/crates/cmds-std/benches/postgrest.rs b/crates/cmds-std/benches/postgrest.rs new file mode 100644 index 00000000..66a4762c --- /dev/null +++ b/crates/cmds-std/benches/postgrest.rs @@ -0,0 +1,69 @@ +use cmds_std::postgrest::builder_select; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use flow_lib::{ + value::{self, array, map}, + Context, Value, +}; +use reqwest::header::{HeaderMap, HeaderValue}; + +fn build_header() -> HeaderMap { + let mut map = HeaderMap::new(); + map.insert("accept", HeaderValue::from_str("application/json").unwrap()); + map +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let json = serde_json::from_str::( + r#" +{ + "url": "https://base.spaceoperator.com/rest/v1/table", + "body": null, + "is_rpc": false, + "method": "GET", + "schema": null, + "headers": [ + [ + "accept", + "application/json" + ] + ], + "queries": [] +}"#, + ) + .unwrap(); + let params = map! { + "query" => json, + "columns" => "*", + }; + let cmd = builder_select::build().unwrap(); + let ctx = Context::default(); + c.bench_function("run_command", |b| { + b.iter(|| { + let fut = cmd.run(black_box(ctx.clone()), black_box(params.clone())); + futures_executor::block_on(fut) + }) + }); + c.bench_function("deserialize", |b| { + b.iter(|| value::from_map::(black_box(params.clone())).unwrap()) + }); + let query = value::from_map::(params) + .unwrap() + .query; + c.bench_function("serialize", |b| { + b.iter(|| { + value::to_map(&black_box(builder_select::Output { + query: query.clone(), + })) + .unwrap() + }) + }); + c.bench_function("build_header", |b| b.iter(build_header)); + let value = Value::Array(array![array!["accept", "application/json"]]); + c.bench_function("deser_vec_tuple", |b| { + b.iter(|| value::from_value::>(black_box(value.clone())).unwrap()) + }); + c.bench_function("new_reqwest_client", |b| b.iter(reqwest::Client::new)); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/cmds-std/node-definitions/collect.json b/crates/cmds-std/node-definitions/collect.json new file mode 100644 index 00000000..6860e0bc --- /dev/null +++ b/crates/cmds-std/node-definitions/collect.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "collect", + "version": "0.1", + "display_name": "Collect", + "description": "Collect inputs into an array", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "array", + "type": "free", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "element", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/const.json b/crates/cmds-std/node-definitions/const.json new file mode 100644 index 00000000..0b64cd94 --- /dev/null +++ b/crates/cmds-std/node-definitions/const.json @@ -0,0 +1,61 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "const", + "version": "0.1", + "description": "", + "display_name": "Const", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/display/chart.json b/crates/cmds-std/node-definitions/display/chart.json new file mode 100644 index 00000000..70feea9e --- /dev/null +++ b/crates/cmds-std/node-definitions/display/chart.json @@ -0,0 +1,71 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "chart", + "version": "0.1", + "display_name": "Chart", + "description": "Chart, bar chart to start", + "tags": ["display", "chart"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "data", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "columns", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "['column_A', 'column_B', etc.]", + "passthrough": false + } + ], + "sources": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/display/json_viewer.json b/crates/cmds-std/node-definitions/display/json_viewer.json new file mode 100644 index 00000000..2cf5bdcf --- /dev/null +++ b/crates/cmds-std/node-definitions/display/json_viewer.json @@ -0,0 +1,63 @@ +{ + "type": "mock", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "json_viewer", + "version": "0.1", + "display_name": "Json Viewer", + "description": "Display JSON", + "tags": ["display", "json"], + "related_to": [ + { + "id": "json_extract", + "type": "node", + "relationship": "accompany" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "input", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "sources": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/display/table.json b/crates/cmds-std/node-definitions/display/table.json new file mode 100644 index 00000000..de39cc0c --- /dev/null +++ b/crates/cmds-std/node-definitions/display/table.json @@ -0,0 +1,71 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "table", + "version": "0.1", + "display_name": "Table", + "description": "Display data in a table", + "tags": ["display", "table"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "data", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "flatten", + "type_bounds": ["bool"], + "required": false, + "defaultValue": "false", + "tooltip": "Flattens the JSON to remove the nesting", + "passthrough": false + } + ], + "sources": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/flow_input.json b/crates/cmds-std/node-definitions/flow_input.json new file mode 100644 index 00000000..1448747b --- /dev/null +++ b/crates/cmds-std/node-definitions/flow_input.json @@ -0,0 +1,65 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "flow_input", + "version": "0.1", + "display_name": "Flow Input", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 150, + "height": 150, + "icon_url": "", + "backgroundColorDark": "#0491d6", + "backgroundColor": "#f2fcff" + }, + "options": {} + }, + "sources": [ + { + "name": "", + "type": "free", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": { + "label": { + "ui:emptyValue": "" + } + } +} diff --git a/crates/cmds-std/node-definitions/flow_output.json b/crates/cmds-std/node-definitions/flow_output.json new file mode 100644 index 00000000..62620ab9 --- /dev/null +++ b/crates/cmds-std/node-definitions/flow_output.json @@ -0,0 +1,76 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "flow_output", + "version": "0.1", + "display_name": "Flow Output", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 150, + "height": 150, + "icon_url": "", + "backgroundColorDark": "#dea10a", + "backgroundColor": "#f2fcff" + }, + "options": {} + }, + "sources": [], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "targets_form.ui_schema": { + "label": { + "ui:emptyValue": "" + } + } +} diff --git a/crates/cmds-std/node-definitions/flow_run_info.json b/crates/cmds-std/node-definitions/flow_run_info.json new file mode 100644 index 00000000..4dd2a843 --- /dev/null +++ b/crates/cmds-std/node-definitions/flow_run_info.json @@ -0,0 +1,73 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "flow_run_info", + "version": "0.1", + "description": "", + "display_name": "Flow Run Info", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "flow_owner", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "started_by", + "type": "string", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "solana_net", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/foreach.json b/crates/cmds-std/node-definitions/foreach.json new file mode 100644 index 00000000..bfe17682 --- /dev/null +++ b/crates/cmds-std/node-definitions/foreach.json @@ -0,0 +1,85 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "foreach", + "version": "0.1", + "display_name": "Foreach", + "description": "Loop over elements of an array", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "targets_form.ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + } +} diff --git a/crates/cmds-std/node-definitions/http.json b/crates/cmds-std/node-definitions/http.json new file mode 100644 index 00000000..71f2a1f0 --- /dev/null +++ b/crates/cmds-std/node-definitions/http.json @@ -0,0 +1,124 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "http_request", + "version": "0.1", + "display_name": "HTTP Request", + "description": "", + "tags": ["std", "network"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-solana/src/http_request.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 200, + "height": 425, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "body", + "type": "free", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "headers", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "url", + "type_bounds": ["string"], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Request's URL" + }, + { + "name": "method", + "type_bounds": ["string"], + "required": false, + "passthrough": false, + "defaultValue": "GET", + "tooltip": "GET, POST, PATCH, etc." + }, + { + "name": "headers", + "type_bounds": ["kv"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "basic_auth", + "type_bounds": ["object"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "e.g. {\"user\": \"\", \"password\": \"\"}" + }, + { + "name": "query_params", + "type_bounds": ["array"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "body", + "type_bounds": ["json"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "Request's JSON body" + }, + { + "name": "form", + "type_bounds": ["kv"], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "content-type will be automatically set to multipart/form-data" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/interflow.json b/crates/cmds-std/node-definitions/interflow.json new file mode 100644 index 00000000..2145f1ef --- /dev/null +++ b/crates/cmds-std/node-definitions/interflow.json @@ -0,0 +1,63 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "interflow", + "version": "0.1", + "display_name": "Interflow", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#e58900", + "backgroundColor": "#ffd9b3" + }, + "options": {} + }, + "sources": [], + "targets": [ + { + "name": "flow", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/interflow_instructions.json b/crates/cmds-std/node-definitions/interflow_instructions.json new file mode 100644 index 00000000..9e9ada42 --- /dev/null +++ b/crates/cmds-std/node-definitions/interflow_instructions.json @@ -0,0 +1,82 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "interflow_instructions", + "version": "0.1", + "display_name": "Interflow Instructions", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "fee_payer", + "type": "keypair", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "signers", + "type": "array", + "defaultValue": "", + "tooltip": "an array of keypairs" + }, + { + "name": "instructions", + "type": "array", + "defaultValue": "", + "tooltip": "an array of instructions" + } + ], + "targets": [ + { + "name": "flow", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/json/json_get_field.json b/crates/cmds-std/node-definitions/json/json_get_field.json new file mode 100644 index 00000000..96113fcc --- /dev/null +++ b/crates/cmds-std/node-definitions/json/json_get_field.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "json_get_field", + "version": "0.1", + "display_name": "JSON Get Field", + "description": "Get a field from a JSON or JSON string", + "width": 200, + "height": 180, + "backgroundColorDark": "#000000", + "backgroundColor": "#ffd9b3", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "json_or_string", + "type_bounds": [ + "free" + ], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "field", + "type_bounds": [ + "string" + ], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "result_json", + "type": "json", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "result_string", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} \ No newline at end of file diff --git a/crates/cmds-std/node-definitions/json_extract.json b/crates/cmds-std/node-definitions/json_extract.json new file mode 100644 index 00000000..fce572dd --- /dev/null +++ b/crates/cmds-std/node-definitions/json_extract.json @@ -0,0 +1,102 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "json_extract", + "version": "0.1", + "display_name": "Json Extract", + "description": "Extracts a field from a JSON", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "json_input", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "field_path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "e.g. /data/records/0/fields/url to select the url field value\nnote the /0/ is equivalent to [0], to select the first index in an array", + "passthrough": false + } + ], + "sources": [ + { + "name": "value", + "type": "free", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "trimmed_json", + "type": "json", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "JSON Extract", + "properties": { + "json_input": { + "title": "JSON Input", + "type": "string" + }, + "field_path": { + "title": "Field Path", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "json_input": { + "ui:widget": "textarea" + }, + "ui:order": ["json_input", "field_path"] + } +} diff --git a/crates/cmds-std/node-definitions/json_insert.json b/crates/cmds-std/node-definitions/json_insert.json new file mode 100644 index 00000000..969ccadb --- /dev/null +++ b/crates/cmds-std/node-definitions/json_insert.json @@ -0,0 +1,87 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "json_insert", + "version": "0.1", + "display_name": "Json Insert", + "description": "Inserts a field from a JSON", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "json_input", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + }, + { + "name": "path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "e.g. /data/records/0/fields/url to insert into url field's value\nnote the /0/ to select [0] in an array", + "passthrough": false + }, + { + "name": "value", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + } + ], + + "sources": [ + { + "name": "updated_json", + "type": "json", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/kvstore/create_store.json b/crates/cmds-std/node-definitions/kvstore/create_store.json new file mode 100644 index 00000000..e6f7ac7b --- /dev/null +++ b/crates/cmds-std/node-definitions/kvstore/create_store.json @@ -0,0 +1,63 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "kv_create_store", + "version": "0.1", + "display_name": "Create Store", + "description": "Create a new KV Store", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [], + "targets": [ + { + "name": "store", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "Name of the store to create.", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/kvstore/delete_store.json b/crates/cmds-std/node-definitions/kvstore/delete_store.json new file mode 100644 index 00000000..c3d01bb8 --- /dev/null +++ b/crates/cmds-std/node-definitions/kvstore/delete_store.json @@ -0,0 +1,63 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "kv_delete_store", + "version": "0.1", + "display_name": "Delete Store", + "description": "Delete a KV Store", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [], + "targets": [ + { + "name": "store", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "Name of the store to delete.", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/kvstore/kv_explorer.json b/crates/cmds-std/node-definitions/kvstore/kv_explorer.json new file mode 100644 index 00000000..a31e2f1c --- /dev/null +++ b/crates/cmds-std/node-definitions/kvstore/kv_explorer.json @@ -0,0 +1,57 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "kvexplorer", + "version": "0.1", + "display_name": "K/V Explorer", + "description": "", + "tags": ["std", "storage"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { "name": "Values", "type": "array", "tooltip": "", "defaultValue": [] }, + { "name": "KVS", "type": "array", "tooltip": "", "defaultValue": [] } + ], + "targets": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/kvstore/read_item.json b/crates/cmds-std/node-definitions/kvstore/read_item.json new file mode 100644 index 00000000..86f5eb82 --- /dev/null +++ b/crates/cmds-std/node-definitions/kvstore/read_item.json @@ -0,0 +1,92 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "kv_read_item", + "version": "0.1", + "display_name": "Read Item", + "description": "Read an item by key", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "value", + "type": "free", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "found", + "type": "bool", + "defaultValue": null, + "tooltip": "Whether the value was found or not." + } + ], + "targets": [ + { + "name": "store", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "Name of the store to read from.", + "passthrough": false + }, + { + "name": "key", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "default", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "Default value to use when the value does not exist.\nWithout this, the command will fail if not found.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/kvstore/write_item.json b/crates/cmds-std/node-definitions/kvstore/write_item.json new file mode 100644 index 00000000..a5bec6a7 --- /dev/null +++ b/crates/cmds-std/node-definitions/kvstore/write_item.json @@ -0,0 +1,87 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "kv_write_item", + "version": "0.1", + "display_name": "Write Item", + "description": "Insert an item to the store, replace and return the old value if exists.", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "old_value", + "type": "free", + "optional": true, + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "store", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "Name of the store.", + "passthrough": true + }, + { + "name": "key", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + }, + { + "name": "value", + "type_bounds": ["free"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/note.json b/crates/cmds-std/node-definitions/note.json new file mode 100644 index 00000000..ad59b7e8 --- /dev/null +++ b/crates/cmds-std/node-definitions/note.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "note", + "version": "0.1", + "display_name": "Note", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "", + "type": "free", + "defaultValue": "", + "tooltip": "" + } + ], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_eq.json b/crates/cmds-std/node-definitions/postgrest/builder_eq.json new file mode 100644 index 00000000..88ada81b --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_eq.json @@ -0,0 +1,101 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_eq", + "version": "0.1", + "display_name": "DB eq", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.eq", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "column", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "filter", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB eq", + "properties": { + "column": { + "title": "column", + "type": "string" + }, + "filter": { + "title": "filter", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["column", "filter"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_insert.json b/crates/cmds-std/node-definitions/postgrest/builder_insert.json new file mode 100644 index 00000000..b3b62592 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_insert.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_insert", + "version": "0.1", + "display_name": "DB insert", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.insert", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "body", + "type_bounds": ["array", "object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_is.json b/crates/cmds-std/node-definitions/postgrest/builder_is.json new file mode 100644 index 00000000..f3edc9c1 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_is.json @@ -0,0 +1,101 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_is", + "version": "0.1", + "display_name": "DB is", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.is", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "column", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "filter", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB eq", + "properties": { + "column": { + "title": "column", + "type": "string" + }, + "filter": { + "title": "filter", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["column", "filter"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_limit.json b/crates/cmds-std/node-definitions/postgrest/builder_limit.json new file mode 100644 index 00000000..d3c535da --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_limit.json @@ -0,0 +1,89 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_limit", + "version": "0.1", + "display_name": "DB limit", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.limit", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "count", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB limit", + "properties": { + "count": { + "title": "count", + "type": "number" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["count"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_match.json b/crates/cmds-std/node-definitions/postgrest/builder_match.json new file mode 100644 index 00000000..294c89c8 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_match.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_match", + "version": "0.1", + "display_name": "DB match", + "description": "Expand into serveral 'DB eq' statements.", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "body", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_neq.json b/crates/cmds-std/node-definitions/postgrest/builder_neq.json new file mode 100644 index 00000000..ac88f6bd --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_neq.json @@ -0,0 +1,101 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_neq", + "version": "0.1", + "display_name": "DB neq", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.neq", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "column", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "filter", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB neq", + "properties": { + "column": { + "title": "column", + "type": "string" + }, + "filter": { + "title": "filter", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["column", "filter"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_not.json b/crates/cmds-std/node-definitions/postgrest/builder_not.json new file mode 100644 index 00000000..24a18826 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_not.json @@ -0,0 +1,113 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_not", + "version": "0.1", + "display_name": "DB is", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.not", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "operator", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "column", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "filter", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB is", + "properties": { + "operator": { + "title": "operator", + "type": "string" + }, + "column": { + "title": "column", + "type": "string" + }, + "filter": { + "title": "filter", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["operator", "column", "filter"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_order.json b/crates/cmds-std/node-definitions/postgrest/builder_order.json new file mode 100644 index 00000000..fa109f46 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_order.json @@ -0,0 +1,89 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_order", + "version": "0.1", + "display_name": "DB order", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.order", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "columns", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB order", + "properties": { + "columns": { + "title": "columns", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["columns"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_select.json b/crates/cmds-std/node-definitions/postgrest/builder_select.json new file mode 100644 index 00000000..0e8f94fb --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_select.json @@ -0,0 +1,89 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_select", + "version": "0.1", + "display_name": "DB select", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.select", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "columns", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "DB select", + "properties": { + "columns": { + "title": "columns", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["columns"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_update.json b/crates/cmds-std/node-definitions/postgrest/builder_update.json new file mode 100644 index 00000000..29740b72 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_update.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_update", + "version": "0.1", + "display_name": "DB update", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.update", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "body", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/builder_upsert.json b/crates/cmds-std/node-definitions/postgrest/builder_upsert.json new file mode 100644 index 00000000..d9d3e027 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/builder_upsert.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_builder_upsert", + "version": "0.1", + "display_name": "DB upsert", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Builder.html#method.upsert", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "body", + "type_bounds": ["array", "object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/execute_query.json b/crates/cmds-std/node-definitions/postgrest/execute_query.json new file mode 100644 index 00000000..83d3704f --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/execute_query.json @@ -0,0 +1,84 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_execute_query", + "version": "0.1", + "display_name": "Execute Query", + "description": "Send the query and return result", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "query", + "type_bounds": ["object"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "headers", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "result", + "type": "free", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "headers", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/postgrest/new_query.json b/crates/cmds-std/node-definitions/postgrest/new_query.json new file mode 100644 index 00000000..b3ea3ef3 --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/new_query.json @@ -0,0 +1,97 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_new_query", + "version": "0.1", + "display_name": "New Query", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Postgrest.html#method.from", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "url", + "type_bounds": ["string"], + "required": false, + "defaultValue": "https://base.spaceoperator.com/rest/v1/", + "tooltip": "", + "passthrough": false + }, + { + "name": "schema", + "type_bounds": ["string"], + "required": false, + "defaultValue": "public", + "tooltip": "", + "passthrough": false + }, + { + "name": "table", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "New Query", + "properties": { + "table": { + "title": "table", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["table"] + } +} diff --git a/crates/cmds-std/node-definitions/postgrest/new_rpc.json b/crates/cmds-std/node-definitions/postgrest/new_rpc.json new file mode 100644 index 00000000..35f76d2d --- /dev/null +++ b/crates/cmds-std/node-definitions/postgrest/new_rpc.json @@ -0,0 +1,105 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "postgrest_new_rpc", + "version": "0.1", + "display_name": "New RPC", + "description": "https://docs.rs/postgrest/latest/postgrest/struct.Postgrest.html#method.rpc", + "tags": ["database", "postgrest", "supabase"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "url", + "type_bounds": ["string"], + "required": false, + "defaultValue": "https://base.spaceoperator.com/rest/v1/", + "tooltip": "", + "passthrough": false + }, + { + "name": "schema", + "type_bounds": ["string"], + "required": false, + "defaultValue": "public", + "tooltip": "", + "passthrough": false + }, + { + "name": "function", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "params", + "type_bounds": ["free"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "query", + "type": "object", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "New RPC", + "properties": { + "function": { + "title": "function", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "ui:order": ["table"] + } +} diff --git a/crates/cmds-std/node-definitions/print.json b/crates/cmds-std/node-definitions/print.json new file mode 100644 index 00000000..0b93ed94 --- /dev/null +++ b/crates/cmds-std/node-definitions/print.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "print", + "version": "0.1", + "display_name": "Print", + "description": "Shows the result of an output", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "print", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true + } + ], + "sources": [ + { + "name": "__print_output", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/std/expression.json b/crates/cmds-std/node-definitions/std/expression.json new file mode 100644 index 00000000..c0d1fff2 --- /dev/null +++ b/crates/cmds-std/node-definitions/std/expression.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "expression", + "version": "0.1", + "display_name": "Expression", + "description": "Evaluate a Rhai expression", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "result", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "script", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + }, + { + "name": "values", + "type_bounds": ["json"], + "required": false, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/std/math_operation.json b/crates/cmds-std/node-definitions/std/math_operation.json new file mode 100644 index 00000000..2d47cd3c --- /dev/null +++ b/crates/cmds-std/node-definitions/std/math_operation.json @@ -0,0 +1,118 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "math_operation", + "version": "0.1", + "display_name": "Math Operation", + "description": "Perform on operation with two numbers", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "number_1", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "number_2", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + }, + { + "name": "operator", + "type_bounds": ["string"], + "required": false, + "defaultValue": "+", + "tooltip": "^ Exponentiation, % Modulo, / Division,* Multiplication, - Subtraction, + Addition", + "passthrough": false + } + ], + "sources": [ + { + "name": "result_f64", + "type": "f64", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "result_u64", + "type": "u64", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "result_i64", + "type": "i64", + "defaultValue": "", + "tooltip": "" + }, + { + "name": "result_string", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "properties": { + "operator": { + "type": "string", + "title": "Operator", + "default": "+" + } + } + }, + "targets_form.ui_schema": { + "operator": { + "ui:autofocus": true, + "ui:emptyValue": "", + "ui:help": "^ Exponentiation, % Modulo, / Division, * Multiplication, - Subtraction, + Addition" + } + } +} diff --git a/crates/cmds-std/node-definitions/std/range.json b/crates/cmds-std/node-definitions/std/range.json new file mode 100644 index 00000000..ae975a17 --- /dev/null +++ b/crates/cmds-std/node-definitions/std/range.json @@ -0,0 +1,86 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "range", + "version": "0.1", + "display_name": "Range", + "description": "Create an array number from 'start' to 'end'", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "start", + "type_bounds": ["number"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "end", + "type_bounds": ["number"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "step_by", + "type_bounds": ["number"], + "required": false, + "defaultValue": 1, + "tooltip": "", + "passthrough": false + } + ], + "sources": [ + { + "name": "result", + "type": "array", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/std/to_bytes.json b/crates/cmds-std/node-definitions/std/to_bytes.json new file mode 100644 index 00000000..32301f24 --- /dev/null +++ b/crates/cmds-std/node-definitions/std/to_bytes.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "to_bytes", + "version": "0.1", + "display_name": "To Bytes", + "description": "Convert string to bytes", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "bytes", + "type": "bytes", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "string", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/std/to_string.json b/crates/cmds-std/node-definitions/std/to_string.json new file mode 100644 index 00000000..4439974f --- /dev/null +++ b/crates/cmds-std/node-definitions/std/to_string.json @@ -0,0 +1,71 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "to_string", + "version": "0.1", + "display_name": "To String", + "description": "Convert to string", + "tags": ["std"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "stringify", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + + "sources": [ + { + "name": "result", + "type": "string", + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/std/to_vec.json b/crates/cmds-std/node-definitions/std/to_vec.json new file mode 100644 index 00000000..350c9f10 --- /dev/null +++ b/crates/cmds-std/node-definitions/std/to_vec.json @@ -0,0 +1,78 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "to_vec", + "version": "0.1", + "display_name": "To Vec", + "description": "Create a vector from two values", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "result", + "type": "free", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "first", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + }, + { + "name": "second", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/create_signed_url.json b/crates/cmds-std/node-definitions/storage/create_signed_url.json new file mode 100644 index 00000000..bc98bb42 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/create_signed_url.json @@ -0,0 +1,85 @@ +{ + "type": "native", + "data": { + "node_id": "storage_create_signed_url", + "version": "0.1", + "display_name": "Create Signed URL", + "description": "Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.\nSee: https://supabase.com/docs/reference/javascript/storage-from-createsignedurl", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "url", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "key", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Full path to file, including bucket name. This is the value returned in 'Upload File''s key output.", + "passthrough": false + }, + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "expires_in", + "type_bounds": [ + "decimal" + ], + "required": true, + "defaultValue": null, + "tooltip": "The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.", + "passthrough": false + }, + { + "name": "transform", + "type_bounds": [ + "json" + ], + "required": false, + "defaultValue": null, + "tooltip": "Transform the asset before serving it to the client. See: https://supabase.com/docs/reference/javascript/storage-from-createsignedurl", + "passthrough": false + }, + { + "name": "download", + "type_bounds": [ + "bool", + "string" + ], + "required": false, + "defaultValue": false, + "tooltip": "Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/delete.json b/crates/cmds-std/node-definitions/storage/delete.json new file mode 100644 index 00000000..b25b5d25 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/delete.json @@ -0,0 +1,54 @@ +{ + "type": "native", + "data": { + "node_id": "storage_delete", + "version": "0.1", + "display_name": "Delete File", + "description": "Delete 1 file.", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "key", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "key", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Full path to file, including bucket name. This is the value returned in 'Upload File''s key output.", + "passthrough": false + }, + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/download.json b/crates/cmds-std/node-definitions/storage/download.json new file mode 100644 index 00000000..71304b8e --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/download.json @@ -0,0 +1,66 @@ +{ + "type": "native", + "data": { + "node_id": "storage_download", + "version": "0.1", + "display_name": "Download File", + "description": "", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "content", + "type": "free", + "defaultValue": null, + "tooltip": "File's content, can be string or bytes. String if it is valid UTF-8." + }, + { + "name": "content_type", + "type": "string", + "defaultValue": null, + "tooltip": "Mimetype of the file." + }, + { + "name": "size", + "type": "u64", + "defaultValue": null, + "tooltip": "File size." + } + ], + "targets": [ + { + "name": "key", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Full path to file, including bucket name. This is the value returned in 'Upload File''s key output.", + "passthrough": false + }, + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/file_explorer.json b/crates/cmds-std/node-definitions/storage/file_explorer.json new file mode 100644 index 00000000..2decdf42 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/file_explorer.json @@ -0,0 +1,67 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "fileexplorer", + "version": "0.1", + "display_name": "File Explorer", + "description": "", + "tags": ["std", "storage", "files"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "Files", + "type": "array", + "tooltip": "", + "defaultValue": [] + }, + { + "name": "URLs", + "type": "array", + "tooltip": "", + "defaultValue": [] + } + ], + "targets": [], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/get_file_metadata.json b/crates/cmds-std/node-definitions/storage/get_file_metadata.json new file mode 100644 index 00000000..ba84410d --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/get_file_metadata.json @@ -0,0 +1,66 @@ +{ + "type": "native", + "data": { + "node_id": "storage_get_file_metadata", + "version": "0.1", + "display_name": "Get File Metadata", + "description": "", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "key", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "content_type", + "type": "string", + "defaultValue": null, + "tooltip": "" + }, + { + "name": "last_modified", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "key", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Full path to file, including bucket name. This is the value returned in 'Upload File''s key output.", + "passthrough": false + }, + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/get_public_url.json b/crates/cmds-std/node-definitions/storage/get_public_url.json new file mode 100644 index 00000000..8da2c7a8 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/get_public_url.json @@ -0,0 +1,54 @@ +{ + "type": "native", + "data": { + "node_id": "storage_get_public_url", + "version": "0.1", + "display_name": "Get Public URL", + "description": "https://supabase.com/docs/reference/javascript/storage-from-getpublicurl", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "url", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "key", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Full path to file, including bucket name. This is the value returned in 'Upload File''s key output.", + "passthrough": false + }, + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "Using 'key' often require knowing current user' ID. If you don't have this value, you can use 'bucket' + 'path' and the server will automatically insert user ID into the path.", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/list.json b/crates/cmds-std/node-definitions/storage/list.json new file mode 100644 index 00000000..5cc81f98 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/list.json @@ -0,0 +1,44 @@ +{ + "type": "native", + "data": { + "node_id": "storage_list", + "version": "0.1", + "display_name": "List Files", + "description": "", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "files", + "type": "array", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/storage/upload.json b/crates/cmds-std/node-definitions/storage/upload.json new file mode 100644 index 00000000..9fbd3671 --- /dev/null +++ b/crates/cmds-std/node-definitions/storage/upload.json @@ -0,0 +1,81 @@ +{ + "type": "native", + "data": { + "node_id": "storage_upload", + "version": "0.1", + "display_name": "Upload File", + "description": "Upload a file", + "width": 0, + "height": 0, + "backgroundColor": "#fff" + }, + "sources": [ + { + "name": "key", + "type": "string", + "defaultValue": null, + "tooltip": "The final key to access this file, usually has the form: '${bucket}/${user_id}/{$path}'." + }, + { + "name": "content_type", + "type": "string", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "bucket", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": "user-storages", + "tooltip": "Supabase's bucket to upload to.\n- Private bucket: user-storages\n- Public bucket: user-public-storages.", + "passthrough": false + }, + { + "name": "path", + "type_bounds": [ + "string" + ], + "required": true, + "defaultValue": null, + "tooltip": "Path to upload to. If bucket is 'user-storages' or 'user-public-storages', backend will automatically insert current user's ID to the beginning of the path", + "passthrough": false + }, + { + "name": "content_type", + "type_bounds": [ + "string" + ], + "required": false, + "defaultValue": null, + "tooltip": "If not supplied, content-type will be inferred from file's extension", + "passthrough": false + }, + { + "name": "overwrite", + "type_bounds": [ + "bool" + ], + "required": false, + "defaultValue": false, + "tooltip": "Overwrite existing file. Default is False.", + "passthrough": false + }, + { + "name": "content", + "type_bounds": [ + "string", + "bytes" + ], + "required": true, + "defaultValue": null, + "tooltip": "File's content", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/supabase/supabase.json b/crates/cmds-std/node-definitions/supabase/supabase.json new file mode 100644 index 00000000..4ad0fe78 --- /dev/null +++ b/crates/cmds-std/node-definitions/supabase/supabase.json @@ -0,0 +1,70 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "supabase", + "version": "0.1", + "display_name": "supabase", + "description": "C", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "res", + "type": "json", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "input", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "passthrough": false, + "tooltip": "" + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/node-definitions/wait.json b/crates/cmds-std/node-definitions/wait.json new file mode 100644 index 00000000..e64dcc8b --- /dev/null +++ b/crates/cmds-std/node-definitions/wait.json @@ -0,0 +1,79 @@ +{ + "type": "native", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "wait", + "version": "0.1", + "display_name": "Wait", + "description": "Wait for an output to complete before continuing", + "tags": ["std", "time"], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "crates/cmds-std/src/wait_cmd.rs", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [], + "targets": [ + { + "name": "value", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "passthrough value", + "passthrough": true + }, + { + "name": "wait_for", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "output to wait for", + "passthrough": true + }, + { + "name": "duration_ms", + "type_bounds": ["u64"], + "required": false, + "defaultValue": "0", + "tooltip": "duration in ms", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/cmds-std/src/const_cmd.rs b/crates/cmds-std/src/const_cmd.rs new file mode 100644 index 00000000..a0eb6d90 --- /dev/null +++ b/crates/cmds-std/src/const_cmd.rs @@ -0,0 +1,196 @@ +use flow_lib::command::prelude::*; + +#[derive(Debug)] +pub enum FormType { + Json, + File, + // Doesn't need special handling, + // we don't care about them + Other(String), +} + +impl From for FormType { + fn from(value: String) -> Self { + match value.as_str() { + "JSON" => FormType::Json, + "File" => FormType::File, + _ => FormType::Other(value), + } + } +} + +impl From<&str> for FormType { + fn from(value: &str) -> Self { + match value { + "JSON" => FormType::Json, + "File" => FormType::File, + _ => FormType::Other(value.to_owned()), + } + } +} + +impl<'de> serde::Deserialize<'de> for FormType { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: String = String::deserialize(d)?; + Ok(s.into()) + } +} + +#[derive(Debug)] +pub struct ConstCommand { + inner: Inner, +} + +pub const CONST_CMD: &str = "const"; + +const SOURCE: &str = "Source"; + +#[derive(Debug)] +enum FormValue { + Value(Value), + Urls(Vec), +} + +#[derive(Debug)] +struct Inner { + value: FormValue, + r#type: ValueType, +} + +#[derive(ThisError, Debug, Clone)] +pub enum Error { + #[error("{0}")] + Deserialize(String), +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e.to_string()) + } +} + +#[derive(Deserialize)] +struct FormData { + r#type: FormType, + value: JsonValue, +} + +fn read_form_data(form: FormData) -> Result { + match form.r#type { + FormType::Json => { + let s: String = serde_json::from_value(form.value)?; + let value: JsonValue = serde_json::from_str(&s)?; + Ok(Inner { + value: FormValue::Value(value.into()), + r#type: ValueType::Free, + }) + } + FormType::File => { + let urls: Vec = serde_json::from_value(form.value)?; + Ok(Inner { + value: FormValue::Urls(urls), + r#type: ValueType::Free, + }) + } + FormType::Other(_) => { + let value = serde_json::from_value::(form.value)?; + Ok(Inner { + value: FormValue::Value(value), + r#type: ValueType::Free, + }) + } + } +} + +impl ConstCommand { + fn new(data: &NodeData) -> Result { + let form: FormData = serde_json::from_value(data.targets_form.form_data.clone())?; + let inner = read_form_data(form)?; + Ok(Self { inner }) + } +} + +#[async_trait] +impl CommandTrait for ConstCommand { + fn name(&self) -> Name { + CONST_CMD.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: SOURCE.into(), + r#type: self.inner.r#type.clone(), + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, _inputs: ValueSet) -> Result { + match &self.inner.value { + FormValue::Value(value) => Ok(value::map! { + SOURCE => value.clone(), + }), + FormValue::Urls(urls) => { + // TODO: download the file + let urls: Vec = urls.iter().map(|url| Value::String(url.clone())).collect(); + Ok(value::map! { + SOURCE => urls, + }) + } + } + } +} + +flow_lib::submit!(CommandDescription::new(CONST_CMD, |data: &NodeData| { + Ok(Box::new(ConstCommand::new(data)?)) +})); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pdg_attr() { + const JSON: &str = r#" + { + "Smoke_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [ + 10 + ] + } + }"#; + + let res = read_form_data(FormData { + r#type: FormType::Json, + value: JsonValue::String(JSON.to_owned()), + }) + .unwrap(); + let val = match res.value { + FormValue::Value(val) => val, + _ => panic!("wrong type"), + }; + assert_eq!( + val, + Value::Map(value::map! { + "Smoke_amount" => value::map! { + "concat" => false, + "flag" => 0u64, + "own" => false, + "type" => 1u64, + "value" => vec![Value::U64(10)], + } + }) + ); + } +} diff --git a/crates/cmds-std/src/error.rs b/crates/cmds-std/src/error.rs new file mode 100644 index 00000000..fe96a28d --- /dev/null +++ b/crates/cmds-std/src/error.rs @@ -0,0 +1,34 @@ +use std::error::Error as StdError; +use std::result::Result as StdResult; +use thiserror::Error as ThisError; + +pub type BoxedError = Box; + +pub type Result = StdResult; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error(transparent)] + Any(#[from] anyhow::Error), + + #[error(transparent)] + Value(#[from] value::Error), + #[error(transparent)] + Http(#[from] reqwest::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error("worker stopped")] + WorkerStopped, + #[error("time-out waiting for signature")] + SignatureTimeout, + #[error("an error occured while running rhai expression: {0}")] + RhaiExecutionError(String), + #[error("value not found in field \"{0}\"")] + ValueNotFound(String), +} + +impl Error { + pub fn custom>(e: E) -> Self { + Error::Any(e.into()) + } +} diff --git a/crates/cmds-std/src/flow_run_info.rs b/crates/cmds-std/src/flow_run_info.rs new file mode 100644 index 00000000..e74ce396 --- /dev/null +++ b/crates/cmds-std/src/flow_run_info.rs @@ -0,0 +1,31 @@ +use flow_lib::{command::prelude::*, SolanaNet}; + +const NAME: &str = "flow_run_info"; + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("flow_run_info.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Deserialize, Debug)] +struct Input {} + +#[derive(Serialize, Debug)] +struct Output { + flow_owner: String, + started_by: String, + solana_net: SolanaNet, +} + +async fn run(ctx: Context, _: Input) -> Result { + Ok(Output { + flow_owner: ctx.flow_owner.id.to_string(), + started_by: ctx.started_by.id.to_string(), + solana_net: ctx.cfg.solana_client.cluster, + }) +} diff --git a/crates/cmds-std/src/http_request.rs b/crates/cmds-std/src/http_request.rs new file mode 100644 index 00000000..25484101 --- /dev/null +++ b/crates/cmds-std/src/http_request.rs @@ -0,0 +1,284 @@ +use crate::prelude::*; +use anyhow::anyhow; +use flow_lib::command::builder::{BuildResult, BuilderCache}; +use reqwest::{dns::Name as DomainName, Url}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +const HTTP_REQUEST: &str = "http_request"; + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(flow_lib::node_definition!("http.json"))?.check_name(HTTP_REQUEST) + }); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(HTTP_REQUEST, |_| build())); + +fn default_method() -> String { + "GET".to_owned() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BasicAuth { + pub user: String, + pub password: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub url: Url, + #[serde(default = "default_method")] + pub method: String, + #[serde(default)] + pub headers: Vec<(String, String)>, + pub basic_auth: Option, + #[serde(default)] + pub query_params: Vec<(String, String)>, + #[serde(default)] + pub body: Option, + #[serde(default)] + pub form: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + body: Value, + headers: HashMap, +} + +struct Resolver; + +fn is_global(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(ip) => Ipv4Ext::is_global(ip), + IpAddr::V6(ip) => Ipv6Ext::is_global(ip), + } +} + +impl Resolver { + fn resolve_impl(&self, name: String) -> reqwest::dns::Resolving { + Box::pin(async move { + tracing::debug!("resolving {}", name.as_str()); + let host = name + ":0"; + + let addrs = tokio::net::lookup_host(host).await?.collect::>(); + if let Some(addr) = addrs.iter().find(|addr| !is_global(&addr.ip())) { + return Err(format!("IP address not allowed: {}", addr.ip()).into()); + } + let addrs: Box + Send> = Box::new(addrs.into_iter()); + Ok(addrs) + }) + } +} + +impl reqwest::dns::Resolve for Resolver { + fn resolve(&self, name: DomainName) -> reqwest::dns::Resolving { + self.resolve_impl(name.as_str().to_owned()) + } +} + +// copied from nightly rust +trait Ipv4Ext { + fn is_global(&self) -> bool; + fn is_shared(&self) -> bool; + fn is_benchmarking(&self) -> bool; + fn is_reserved(&self) -> bool; +} + +impl Ipv4Ext for Ipv4Addr { + fn is_global(&self) -> bool { + !(self.octets()[0] == 0 // "This network" + || self.is_private() + || Ipv4Ext::is_shared(self) + || self.is_loopback() + || self.is_link_local() + // addresses reserved for future protocols (`192.0.0.0/24`) + ||(self.octets()[0] == 192 && self.octets()[1] == 0 && self.octets()[2] == 0) + || self.is_documentation() + || Ipv4Ext::is_benchmarking(self) + || Ipv4Ext::is_reserved(self) + || self.is_broadcast()) + } + + fn is_shared(&self) -> bool { + self.octets()[0] == 100 && (self.octets()[1] & 0b1100_0000 == 0b0100_0000) + } + + fn is_benchmarking(&self) -> bool { + self.octets()[0] == 198 && (self.octets()[1] & 0xfe) == 18 + } + + fn is_reserved(&self) -> bool { + self.octets()[0] & 240 == 240 && !self.is_broadcast() + } +} + +trait Ipv6Ext { + fn is_global(&self) -> bool; + fn is_documentation(&self) -> bool; + fn is_unique_local(&self) -> bool; + fn is_unicast_link_local(&self) -> bool; +} + +impl Ipv6Ext for Ipv6Addr { + fn is_global(&self) -> bool { + !(self.is_unspecified() + || self.is_loopback() + // IPv4-mapped Address (`::ffff:0:0/96`) + || matches!(self.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) + // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) + || matches!(self.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) + // Discard-Only Address Block (`100::/64`) + || matches!(self.segments(), [0x100, 0, 0, 0, _, _, _, _]) + // IETF Protocol Assignments (`2001::/23`) + || (matches!(self.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) + && !( + // Port Control Protocol Anycast (`2001:1::1`) + u128::from_be_bytes(self.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 + // Traversal Using Relays around NAT Anycast (`2001:1::2`) + || u128::from_be_bytes(self.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 + // AMT (`2001:3::/32`) + || matches!(self.segments(), [0x2001, 3, _, _, _, _, _, _]) + // AS112-v6 (`2001:4:112::/48`) + || matches!(self.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) + // ORCHIDv2 (`2001:20::/28`) + || matches!(self.segments(), [0x2001, b, _, _, _, _, _, _] if (0x20..=0x2F).contains(&b)) + )) + || Ipv6Ext::is_documentation(self) + || Ipv6Ext::is_unique_local(self) + || Ipv6Ext::is_unicast_link_local(self)) + } + + fn is_documentation(&self) -> bool { + (self.segments()[0] == 0x2001) && (self.segments()[1] == 0xdb8) + } + + fn is_unique_local(&self) -> bool { + (self.segments()[0] & 0xfe00) == 0xfc00 + } + + fn is_unicast_link_local(&self) -> bool { + (self.segments()[0] & 0xffc0) == 0xfe80 + } +} + +async fn run(ctx: Context, input: Input) -> Result { + match input.url.host() { + Some(url::Host::Domain(domain)) => { + let _ = Resolver + .resolve_impl(domain.to_owned()) + .await + .map_err(|error| anyhow!(error))?; + } + Some(url::Host::Ipv4(ip)) => { + if !Ipv4Ext::is_global(&ip) { + return Err(anyhow::anyhow!("IP address not allowed: {}", ip)); + } + } + Some(url::Host::Ipv6(ip)) => { + if !Ipv6Ext::is_global(&ip) { + return Err(anyhow::anyhow!("IP address not allowed: {}", ip)); + } + } + None => return Err(anyhow::anyhow!("URL has no host")), + } + + let client = ctx.http; + + let mut req = client.request(input.method.parse()?, input.url); + + if !input.query_params.is_empty() { + req = req.query(&input.query_params); + } + + for (k, v) in &input.headers { + req = req.header(k, v); + } + + if let Some(basic) = &input.basic_auth { + let passwd = basic.password.as_ref().filter(|p| !p.is_empty()); + req = req.basic_auth(&basic.user, passwd); + } + + if let Some(body) = input.body { + req = req.json(&body); + } + + if let Some(form) = input.form { + let mut multiform = reqwest::multipart::Form::new(); + for (k, v) in form { + multiform = multiform.text(k, v); + } + req = req.multipart(multiform); + } + + let resp = req.send().await?; + + let status = resp.status(); + + if status.is_success() { + let headers = resp + .headers() + .iter() + .map(|(k, v)| { + ( + k.as_str().to_lowercase(), + String::from_utf8_lossy(v.as_bytes()).into_owned(), + ) + }) + .collect::>(); + + let ct = headers + .get("content-type") + .map(String::as_str) + .unwrap_or("text/plain"); + let body: Value = if ct.starts_with("text/") { + resp.text().await?.into() + } else if ct.contains("json") { + resp.json::().await?.into() + } else { + resp.bytes().await?.into() + }; + + Ok(Output { headers, body }) + } else { + let body = resp.text().await.ok(); + Err(anyhow::anyhow!( + "status code: {}\n{}", + status.as_u16(), + body.unwrap_or_default() + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + async fn test_local() { + async fn test(url: &str) { + let c = Context::default(); + let e = run( + c.clone(), + value::from_map(value::map! {"url" => url}).unwrap(), + ) + .await + .unwrap_err() + .to_string(); + assert!(e.contains("IP address not allowed")); + } + + // local networks are not allowed because of security reason + test("http://localhost").await; + test("http://127.0.0.1:8080").await; + test("http://169.254.169.254/latest/api/token").await; + test("http://255.255.255.255").await; + } +} diff --git a/crates/cmds-std/src/json_extract.rs b/crates/cmds-std/src/json_extract.rs new file mode 100644 index 00000000..788ef22c --- /dev/null +++ b/crates/cmds-std/src/json_extract.rs @@ -0,0 +1,65 @@ +use flow_lib::command::prelude::*; + +const JSON_EXTRACT: &str = "json_extract"; + +#[derive(Deserialize, Debug)] +struct Input { + json_input: Value, + field_path: String, +} + +#[derive(Serialize, Debug)] +struct Output { + value: Value, + trimmed_json: Value, +} + +async fn run(_: Context, mut input: Input) -> Result { + let path = value::crud::path::Path::parse(&input.field_path)?; + let extracted = + value::crud::remove(&mut input.json_input, &path.segments).unwrap_or(Value::Null); + + Ok(Output { + value: extracted, + trimmed_json: input.json_input, + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("json_extract.json"))? + .check_name(JSON_EXTRACT)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(JSON_EXTRACT, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_json_extract() { + let inputs = value::map! { + "json_input" => value::map! { + "a" => 1i64, + "b" => 2i64, + "c" => 3i64, + }, + "field_path" => "c", + }; + + let mut outputs = build().unwrap().run(<_>::default(), inputs).await.unwrap(); + let value = outputs.swap_remove("value").unwrap(); + let trimmed = outputs.swap_remove("trimmed_json").unwrap(); + assert_eq!(value, Value::I64(3)); + assert_eq!( + trimmed, + Value::Map(value::map! { + "a" => 1i64, + "b" => 2i64, + }) + ); + } +} diff --git a/crates/cmds-std/src/json_insert.rs b/crates/cmds-std/src/json_insert.rs new file mode 100644 index 00000000..d3524a9d --- /dev/null +++ b/crates/cmds-std/src/json_insert.rs @@ -0,0 +1,67 @@ +use flow_lib::command::prelude::*; + +const JSON_INSERT: &str = "json_insert"; + +#[derive(Deserialize, Debug)] +struct Input { + json_input: Value, + path: String, + value: Value, +} + +#[derive(Serialize, Debug)] +struct Output { + updated_json: Value, +} + +async fn run(_: Context, mut input: Input) -> Result { + let path = value::crud::path::Path::parse(&input.path)?; + value::crud::insert(&mut input.json_input, &path.segments, input.value)?; + + Ok(Output { + updated_json: input.json_input, + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("json_insert.json"))? + .check_name(JSON_INSERT)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(JSON_INSERT, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_json_insert() { + let input = value::map! { + "json_input" => value::map! { + "a" => 1, + "b" => value::map! { + "c" => value::map! {} + } + }, + "path" => "/b/c", + "value" => value::map! { "d" => 3 }, + }; + let output = build().unwrap().run(<_>::default(), input).await.unwrap(); + assert_eq!( + output, + value::map! { + "updated_json" => value::map! { + "a" => 1, + "b" => value::map!{ + "c" => value::map! { + "d" => 3 + } + } + } + }, + ); + } +} diff --git a/crates/cmds-std/src/kvstore/create_store.rs b/crates/cmds-std/src/kvstore/create_store.rs new file mode 100644 index 00000000..120f0829 --- /dev/null +++ b/crates/cmds-std/src/kvstore/create_store.rs @@ -0,0 +1,51 @@ +use crate::supabase_error; +use anyhow::anyhow; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const NAME: &str = "kv_create_store"; + +const DEFINITION: &str = flow_lib::node_definition!("kvstore/create_store.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + store: String, +} + +#[derive(Serialize)] +struct Output {} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut req = ctx + .http + .post(format!("{}/kv/create_store", ctx.endpoints.flow_server)) + .json(&input); + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + let resp = req.send().await.map_err(|e| anyhow!("HTTP error: {}", e))?; + match resp.status() { + StatusCode::OK => Ok(Output {}), + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/kvstore/delete_store.rs b/crates/cmds-std/src/kvstore/delete_store.rs new file mode 100644 index 00000000..fd8e5205 --- /dev/null +++ b/crates/cmds-std/src/kvstore/delete_store.rs @@ -0,0 +1,51 @@ +use crate::supabase_error; +use anyhow::anyhow; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const NAME: &str = "kv_delete_store"; + +const DEFINITION: &str = flow_lib::node_definition!("kvstore/delete_store.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + store: String, +} + +#[derive(Serialize)] +struct Output {} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut req = ctx + .http + .post(format!("{}/kv/delete_store", ctx.endpoints.flow_server)) + .json(&input); + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + let resp = req.send().await.map_err(|e| anyhow!("HTTP error: {}", e))?; + match resp.status() { + StatusCode::OK => Ok(Output {}), + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/kvstore/explorer.rs b/crates/cmds-std/src/kvstore/explorer.rs new file mode 100644 index 00000000..34a374e2 --- /dev/null +++ b/crates/cmds-std/src/kvstore/explorer.rs @@ -0,0 +1,147 @@ +use super::read_item::SuccessBody; +use crate::supabase_error; +use flow_lib::command::prelude::*; +use futures_util::future::join_all; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const KV_EXPLORER: &str = "kvexplorer"; + +const VALUES: &str = "Values"; +const KVS: &str = "KVS"; + +#[derive(Serialize, Debug)] +struct ItemKey { + store: String, + key: String, +} + +#[derive(Debug)] +pub struct ExplorerCommand { + outputs: Vec, + pinned: Vec, +} + +impl ExplorerCommand { + fn new(data: &NodeData) -> Result { + let outputs = data + .sources + .iter() + .map(|o| Output { + name: o.name.clone(), + r#type: o.r#type.clone(), + optional: false, + }) + .collect(); + let pinned = serde_json::from_value::>( + data.targets_form + .extra + .rest + .get("pinned") + .cloned() + .unwrap_or_default(), + )? + .into_iter() + .filter_map(|s| { + let idx = s.find('/')?; + if idx + 1 >= s.len() { + return None; + } + Some(ItemKey { + store: s[..idx].to_owned(), + key: s[idx + 1..].to_owned(), + }) + }) + .collect(); + Ok(Self { outputs, pinned }) + } +} + +async fn read_item( + client: &reqwest::Client, + url: &str, + key: &ItemKey, + auth: &str, +) -> Result { + let mut req = client.post(url).json(&key); + req = req.header(AUTHORIZATION, auth); + + let resp = req.send().await?; + + let code = resp.status(); + if code == StatusCode::OK { + Ok(resp.json::().await?.value) + } else { + Err(supabase_error(code, resp).await) + } +} + +#[async_trait] +impl CommandTrait for ExplorerCommand { + fn name(&self) -> Name { + KV_EXPLORER.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + self.outputs.clone() + } + + fn permissions(&self) -> Permissions { + Permissions { user_tokens: true } + } + + async fn run(&self, mut ctx: Context, _: ValueSet) -> Result { + let auth = ctx.get_jwt_header().await?; + let url = format!("{}/kv/read_item", ctx.endpoints.flow_server); + let results = join_all( + self.pinned + .iter() + .map(|k| read_item(&ctx.http, &url, k, &auth)), + ) + .await; + + let push_values = self.outputs.iter().any(|o| o.name == VALUES); + let push_kvs = self.outputs.iter().any(|o| o.name == KVS); + let mut values = Vec::new(); + let mut kvs = Vec::new(); + let mut output = value::Map::new(); + self.pinned + .iter() + .zip(results) + .for_each(|(k, result)| match result { + Ok(value) => { + if push_values { + values.push(value.clone()); + } + if push_kvs { + kvs.push(Value::from(value::map! { + "store" => k.store.clone(), + "key" => k.key.clone(), + })); + } + let name = format!("{}/{}", k.store, k.key); + if self.outputs.iter().any(|o| o.name == name) { + output.insert(name, value); + } + } + Err(error) => { + tracing::error!("failed to get {:?}: {}", k, error); + } + }); + + if push_values { + output.insert(VALUES.to_owned(), Value::Array(values)); + } + if push_kvs { + output.insert(KVS.to_owned(), Value::Array(kvs)); + } + Ok(output) + } +} + +flow_lib::submit!(CommandDescription::new(KV_EXPLORER, |data: &NodeData| { + Ok(Box::new(ExplorerCommand::new(data)?)) +})); diff --git a/crates/cmds-std/src/kvstore/mod.rs b/crates/cmds-std/src/kvstore/mod.rs new file mode 100644 index 00000000..081229df --- /dev/null +++ b/crates/cmds-std/src/kvstore/mod.rs @@ -0,0 +1,5 @@ +pub mod create_store; +pub mod delete_store; +pub mod explorer; +pub mod read_item; +pub mod write_item; diff --git a/crates/cmds-std/src/kvstore/read_item.rs b/crates/cmds-std/src/kvstore/read_item.rs new file mode 100644 index 00000000..fdb93354 --- /dev/null +++ b/crates/cmds-std/src/kvstore/read_item.rs @@ -0,0 +1,75 @@ +use crate::supabase_error; +use anyhow::anyhow; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const NAME: &str = "kv_read_item"; + +const DEFINITION: &str = flow_lib::node_definition!("kvstore/read_item.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + store: String, + key: String, + #[serde(skip_serializing)] + default: Option, +} + +#[derive(Serialize)] +struct Output { + value: Value, + found: bool, +} + +#[derive(Deserialize)] +pub struct SuccessBody { + pub value: Value, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut req = ctx + .http + .post(format!("{}/kv/read_item", ctx.endpoints.flow_server)) + .json(&input); + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + let resp = req.send().await.map_err(|e| anyhow!("HTTP error: {}", e))?; + match resp.status() { + StatusCode::OK => { + let body = resp.json::().await?; + Ok(Output { + value: body.value, + found: true, + }) + } + StatusCode::NOT_FOUND => match input.default { + Some(default) => Ok(Output { + value: default, + found: false, + }), + None => Err(CommandError::msg("not found")), + }, + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/kvstore/write_item.rs b/crates/cmds-std/src/kvstore/write_item.rs new file mode 100644 index 00000000..dcedf709 --- /dev/null +++ b/crates/cmds-std/src/kvstore/write_item.rs @@ -0,0 +1,66 @@ +use crate::supabase_error; +use anyhow::anyhow; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const NAME: &str = "kv_write_item"; + +const DEFINITION: &str = flow_lib::node_definition!("kvstore/write_item.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + store: String, + key: String, + value: Value, +} + +#[derive(Serialize)] +struct Output { + #[serde(skip_serializing_if = "Option::is_none")] + old_value: Option, +} + +#[derive(Deserialize)] +struct SuccessBody { + old_value: Option, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let mut req = ctx + .http + .post(format!("{}/kv/write_item", ctx.endpoints.flow_server)) + .json(&input); + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + let resp = req.send().await.map_err(|e| anyhow!("HTTP error: {}", e))?; + match resp.status() { + StatusCode::OK => { + let body = resp.json::().await?; + Ok(Output { + old_value: body.old_value, + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/lib.rs b/crates/cmds-std/src/lib.rs new file mode 100644 index 00000000..f65a3036 --- /dev/null +++ b/crates/cmds-std/src/lib.rs @@ -0,0 +1,51 @@ +use flow_lib::command::CommandError; + +pub mod const_cmd; +pub mod error; +pub mod flow_run_info; +pub mod http_request; +pub mod json_extract; +pub mod json_insert; +pub mod kvstore; +pub mod note; +pub mod postgrest; +pub mod print_cmd; +pub mod std; +pub mod storage; +pub mod supabase; +pub mod wait_cmd; + +pub mod prelude { + pub use async_trait::async_trait; + pub use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, BuilderError, CmdBuilder}, + CommandDescription, CommandError, CommandTrait, InstructionInfo, + }, + context::Context, + solana::{Instructions, KeypairExt}, + CmdInputDescription as CmdInput, CmdOutputDescription as CmdOutput, Name, SolanaNet, + ValueSet, ValueType, + }; + pub use rust_decimal::Decimal; + pub use serde::{Deserialize, Serialize}; + + pub use std::sync::Arc; + pub use value::{HashMap, Value}; +} + +#[derive(serde::Deserialize)] +pub struct ErrorBody { + pub error: String, +} + +pub async fn supabase_error(code: reqwest::StatusCode, resp: reqwest::Response) -> CommandError { + let bytes = resp.bytes().await.unwrap_or_default(); + match serde_json::from_slice::(&bytes) { + Ok(ErrorBody { error }) => CommandError::msg(error), + _ => { + let body = String::from_utf8_lossy(&bytes); + anyhow::anyhow!("{}: {}", code, body) + } + } +} diff --git a/crates/cmds-std/src/note.rs b/crates/cmds-std/src/note.rs new file mode 100644 index 00000000..40ee41b6 --- /dev/null +++ b/crates/cmds-std/src/note.rs @@ -0,0 +1,29 @@ +use flow_lib::command::prelude::*; + +#[derive(Debug)] +pub struct NoteCommand {} + +const NOTE: &str = "note"; + +#[async_trait] +impl CommandTrait for NoteCommand { + fn name(&self) -> Name { + NOTE.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + [].to_vec() + } + + async fn run(&self, _ctx: Context, _inputs: ValueSet) -> Result { + Ok(ValueSet::new()) + } +} + +flow_lib::submit!(CommandDescription::new(NOTE, |_| Ok(Box::new( + NoteCommand {} +)))); diff --git a/crates/cmds-std/src/postgrest/builder_eq.rs b/crates/cmds-std/src/postgrest/builder_eq.rs new file mode 100644 index 00000000..a1f4afbc --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_eq.rs @@ -0,0 +1,43 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_eq"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + column: String, + filter: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .eq(input.column, input.filter) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_eq.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_insert.rs b/crates/cmds-std/src/postgrest/builder_insert.rs new file mode 100644 index 00000000..92193eb5 --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_insert.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_insert"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + body: JsonValue, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .insert(serde_json::to_string(&input.body)?) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_insert.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_is.rs b/crates/cmds-std/src/postgrest/builder_is.rs new file mode 100644 index 00000000..f6d807dd --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_is.rs @@ -0,0 +1,43 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_is"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + column: String, + filter: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .is(input.column, input.filter) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_is.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_limit.rs b/crates/cmds-std/src/postgrest/builder_limit.rs new file mode 100644 index 00000000..c3786c59 --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_limit.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_limit"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + count: u64, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .limit(input.count as usize) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_limit.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_match.rs b/crates/cmds-std/src/postgrest/builder_match.rs new file mode 100644 index 00000000..33c5ee3e --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_match.rs @@ -0,0 +1,54 @@ +use anyhow::anyhow; +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_match"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + body: serde_json::Map, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + let mut query = postgrest::Builder::from_query(input.query, ctx.http); + for (k, v) in input.body { + let v = match v { + JsonValue::Null => "null".to_owned(), + JsonValue::Bool(x) => x.to_string(), + JsonValue::Number(x) => x.to_string(), + JsonValue::String(x) => x, + JsonValue::Array(_) => return Err(anyhow!("array type is not supported")), + JsonValue::Object(_) => return Err(anyhow!("object type is not supported")), + }; + query = query.eq(k, &v); + } + + Ok(Output { + query: query.into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_match.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_neq.rs b/crates/cmds-std/src/postgrest/builder_neq.rs new file mode 100644 index 00000000..eb3f0052 --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_neq.rs @@ -0,0 +1,43 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_neq"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + column: String, + filter: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .neq(input.column, input.filter) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_neq.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_not.rs b/crates/cmds-std/src/postgrest/builder_not.rs new file mode 100644 index 00000000..d061912e --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_not.rs @@ -0,0 +1,44 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_not"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + operator: String, + column: String, + filter: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .not(input.operator, input.column, input.filter) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_not.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_order.rs b/crates/cmds-std/src/postgrest/builder_order.rs new file mode 100644 index 00000000..3d26d1db --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_order.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_order"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + columns: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .order(input.columns) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_order.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_select.rs b/crates/cmds-std/src/postgrest/builder_select.rs new file mode 100644 index 00000000..f7035da0 --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_select.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_select"; + +#[derive(Deserialize, Debug)] +pub struct Input { + pub query: postgrest::Query, + pub columns: String, +} + +#[derive(Serialize, Debug)] +pub struct Output { + pub query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .select(input.columns) + .into(), + }) +} + +pub fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_select.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_update.rs b/crates/cmds-std/src/postgrest/builder_update.rs new file mode 100644 index 00000000..93b201ec --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_update.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_update"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + body: serde_json::Map, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .update(serde_json::to_string(&input.body)?) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_update.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/builder_upsert.rs b/crates/cmds-std/src/postgrest/builder_upsert.rs new file mode 100644 index 00000000..1084fc36 --- /dev/null +++ b/crates/cmds-std/src/postgrest/builder_upsert.rs @@ -0,0 +1,42 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_builder_upsert"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + body: JsonValue, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + Ok(Output { + query: postgrest::Builder::from_query(input.query, ctx.http) + .upsert(serde_json::to_string(&input.body)?) + .into(), + }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/builder_upsert.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/execute_query.rs b/crates/cmds-std/src/postgrest/execute_query.rs new file mode 100644 index 00000000..df905ce3 --- /dev/null +++ b/crates/cmds-std/src/postgrest/execute_query.rs @@ -0,0 +1,95 @@ +use crate::supabase_error; +use flow_lib::command::prelude::*; +use reqwest::header::{HeaderName, AUTHORIZATION}; +use std::{collections::HashMap, str::FromStr}; + +const NAME: &str = "postgrest_execute_query"; + +#[derive(Deserialize, Debug)] +struct Input { + query: postgrest::Query, + #[serde(default)] + pub headers: Vec<(String, String)>, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let contain_auth_header = !input.headers.iter().any(|(k, _)| { + HeaderName::from_str(k) + .ok() + .map(|name| name == AUTHORIZATION) + .unwrap_or(false) + }); + let is_supabase = input + .query + .url + .starts_with(&format!("{}/rest/v1", ctx.endpoints.supabase)); + + let mut req = postgrest::Builder::from_query(input.query, ctx.http.clone()).build(); + for (k, v) in input.headers { + req = req.header(k, v); + } + if contain_auth_header && is_supabase { + tracing::info!("using JWT of user: {}", ctx.flow_owner.id); + req = req.header("apikey", &ctx.endpoints.supabase_anon_key); + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + } + let resp = ctx.http.execute(req.build()?).await?; + + if resp.status().is_success() { + let headers = resp + .headers() + .iter() + .map(|(k, v)| { + ( + k.as_str().to_lowercase(), + String::from_utf8_lossy(v.as_bytes()).into_owned(), + ) + }) + .collect::>(); + + let content_type = headers + .get("content-type") + .map(String::as_str) + .unwrap_or("text/plain"); + let body: Value = if content_type.starts_with("text/") { + resp.text().await?.into() + } else if content_type.contains("json") { + resp.json::().await?.into() + } else { + resp.bytes().await?.into() + }; + + let headers = headers + .into_iter() + .map(|(k, v)| (k, Value::String(v))) + .collect::(); + + Ok(value::map! { + "result" => body, + "headers" => headers, + }) + } else { + Err(supabase_error(resp.status(), resp).await) + } +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/execute_query.json"))? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true }) + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/mod.rs b/crates/cmds-std/src/postgrest/mod.rs new file mode 100644 index 00000000..6c3ba56e --- /dev/null +++ b/crates/cmds-std/src/postgrest/mod.rs @@ -0,0 +1,14 @@ +pub mod builder_eq; +pub mod builder_insert; +pub mod builder_is; +pub mod builder_limit; +pub mod builder_match; +pub mod builder_neq; +pub mod builder_not; +pub mod builder_order; +pub mod builder_select; +pub mod builder_update; +pub mod builder_upsert; +pub mod execute_query; +pub mod new_query; +pub mod new_rpc; diff --git a/crates/cmds-std/src/postgrest/new_query.rs b/crates/cmds-std/src/postgrest/new_query.rs new file mode 100644 index 00000000..f7646dfb --- /dev/null +++ b/crates/cmds-std/src/postgrest/new_query.rs @@ -0,0 +1,43 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_new_query"; + +#[derive(Deserialize, Debug)] +struct Input { + url: Option, + schema: Option, + table: String, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + let url = input + .url + .unwrap_or_else(|| format!("{}/rest/v1/{}", ctx.endpoints.supabase, input.table)); + let query = postgrest::Builder::new(url, input.schema, <_>::default(), ctx.http).into(); + Ok(Output { query }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/new_query.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/postgrest/new_rpc.rs b/crates/cmds-std/src/postgrest/new_rpc.rs new file mode 100644 index 00000000..622caaf3 --- /dev/null +++ b/crates/cmds-std/src/postgrest/new_rpc.rs @@ -0,0 +1,48 @@ +use flow_lib::command::prelude::*; + +const NAME: &str = "postgrest_new_rpc"; + +#[derive(Deserialize, Debug)] +struct Input { + url: Option, + schema: Option, + function: String, + params: JsonValue, +} + +#[derive(Serialize, Debug)] +struct Output { + query: postgrest::Query, +} + +async fn run(ctx: Context, input: Input) -> Result { + let url = input + .url + .unwrap_or_else(|| format!("{}/rest/v1", ctx.endpoints.supabase)); + let url = format!("{}/rpc/{}", url, input.function); + let query = postgrest::Builder::new(url, input.schema, <_>::default(), ctx.http) + .rpc(serde_json::to_string(&input.params)?) + .into(); + + Ok(Output { query }) +} + +fn build() -> BuildResult { + Ok( + CmdBuilder::new(flow_lib::node_definition!("postgrest/new_rpc.json"))? + .check_name(NAME)? + .build(run), + ) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/print_cmd.rs b/crates/cmds-std/src/print_cmd.rs new file mode 100644 index 00000000..b426bd58 --- /dev/null +++ b/crates/cmds-std/src/print_cmd.rs @@ -0,0 +1,65 @@ +use anyhow::anyhow; +use flow_lib::command::prelude::*; + +#[derive(Debug)] +pub struct PrintCommand {} + +pub const PRINT_CMD: &str = "print"; + +// Inputs +pub const PRINT: &str = "print"; + +// Outputs +pub const PRINT_OUTPUT: &str = "__print_output"; + +#[async_trait] +impl CommandTrait for PrintCommand { + fn name(&self) -> Name { + PRINT_CMD.into() + } + + fn inputs(&self) -> Vec { + [Input { + name: PRINT.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: true, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: PRINT_OUTPUT.into(), + r#type: ValueType::String, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, mut inputs: ValueSet) -> Result { + let input = inputs + .swap_remove(PRINT) + .ok_or_else(|| anyhow!("input not found: {}", PRINT))?; + let output = match input { + Value::Decimal(v) => v.to_string(), + Value::U64(v) => v.to_string(), + Value::I64(v) => v.to_string(), + Value::U128(v) => v.to_string(), + Value::I128(v) => v.to_string(), + Value::F64(v) => v.to_string(), + Value::B32(v) => bs58::encode(&v).into_string(), + Value::B64(v) => bs58::encode(&v).into_string(), + Value::String(s) => s, + other => serde_json::to_string_pretty(&other).unwrap(), + }; + Ok(ValueSet::from([( + PRINT_OUTPUT.into(), + Value::String(output), + )])) + } +} + +flow_lib::submit!(CommandDescription::new(PRINT_CMD, |_| Ok(Box::new( + PrintCommand {} +)))); diff --git a/crates/cmds-std/src/std/json_get_field.rs b/crates/cmds-std/src/std/json_get_field.rs new file mode 100644 index 00000000..51759e34 --- /dev/null +++ b/crates/cmds-std/src/std/json_get_field.rs @@ -0,0 +1,141 @@ +use serde_json::Value as JsonValue; + +use value::from_value; + +use crate::error::Error::ValueNotFound; +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct JsonGetField; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + field: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + result_json: JsonValue, + result_string: String, +} + +// Name +const JSON_GET_FIELD: &str = "json_get_field"; + +// Inputs +const JSON_OR_STRING: &str = "json_or_string"; +const FIELD: &str = "field"; + +// Outputs +const RESULT_JSON: &str = "result_json"; +const RESULT_STRING: &str = "result_string"; + +#[async_trait] +impl CommandTrait for JsonGetField { + fn name(&self) -> Name { + JSON_GET_FIELD.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: JSON_OR_STRING.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: FIELD.into(), + type_bounds: [ValueType::String].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [ + CmdOutput { + name: RESULT_JSON.into(), + r#type: ValueType::Json, + optional: false, + }, + CmdOutput { + name: RESULT_STRING.into(), + r#type: ValueType::String, + optional: false, + }, + ] + .to_vec() + } + + async fn run(&self, _ctx: Context, mut inputs: ValueSet) -> Result { + let Input { field } = value::from_map(inputs.clone())?; + + let json = inputs + .swap_remove(JSON_OR_STRING) + .ok_or_else(|| crate::error::Error::ValueNotFound(JSON_OR_STRING.into()))?; + + match json { + Value::Map(map) => { + let value = map.get(&field).ok_or_else(|| ValueNotFound(field))?; + + let result_json: JsonValue = from_value(value.to_owned())?; + let result_string = result_json.to_string(); + + Ok(value::to_map(&Output { + result_json, + result_string, + })?) + } + Value::String(s) => { + let json: Result, _> = serde_json::from_str(&s); + + let value = json + .ok() + .and_then(|mut object| object.swap_remove(&field)) + .unwrap_or_default(); + + let result_json: JsonValue = value; + let result_string = result_json.to_string(); + + Ok(value::to_map(&Output { + result_json, + result_string, + })?) + } + _ => { + let result_json: JsonValue = JsonValue::Null; + let result_string = "".into(); + Ok(value::to_map(&Output { + result_json, + result_string, + })?) + } + } + } +} + +flow_lib::submit!(CommandDescription::new(JSON_GET_FIELD, |_| Ok(Box::new( + JsonGetField {} +)))); + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_valid() { + let inputs = value::map! { + JSON_OR_STRING => value::map! { + "amount" => 100, + }, + FIELD => "amount", + }; + + let output = JsonGetField.run(Context::default(), inputs).await.unwrap(); + let result = value::from_map::(output).unwrap().result_json; + assert_eq!(result, 100); + } +} diff --git a/crates/cmds-std/src/std/mod.rs b/crates/cmds-std/src/std/mod.rs new file mode 100644 index 00000000..b1fac9bc --- /dev/null +++ b/crates/cmds-std/src/std/mod.rs @@ -0,0 +1,5 @@ +pub mod json_get_field; +pub mod range; +pub mod to_bytes; +pub mod to_string; +pub mod to_vec; diff --git a/crates/cmds-std/src/std/range.rs b/crates/cmds-std/src/std/range.rs new file mode 100644 index 00000000..875f0911 --- /dev/null +++ b/crates/cmds-std/src/std/range.rs @@ -0,0 +1,58 @@ +use crate::prelude::*; + +// Command Name +const NAME: &str = "range"; + +const DEFINITION: &str = flow_lib::node_definition!("std/range.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + #[serde(with = "value::decimal")] + pub start: Decimal, + #[serde(with = "value::decimal")] + pub end: Decimal, + #[serde(default, with = "value::decimal::opt")] + pub step_by: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub result: Vec, +} + +async fn run(_: Context, input: Input) -> Result { + const MAX_LENGTH: usize = 10_000_000; + let mut start = input.start; + let end = input.end; + let step = input.step_by.unwrap_or(Decimal::ONE); + let length: usize = ((end - start).abs() / step).floor().try_into()?; + if length > MAX_LENGTH { + return Err(anyhow::anyhow!( + "too large, maximum length is {}", + MAX_LENGTH, + )); + } + let mut result = Vec::with_capacity(length); + for _ in 0..length { + result.push(Value::Decimal(start)); + start += step; + } + Ok(Output { result }) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/std/to_bytes.rs b/crates/cmds-std/src/std/to_bytes.rs new file mode 100644 index 00000000..51f08e79 --- /dev/null +++ b/crates/cmds-std/src/std/to_bytes.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +// Command Name +const NAME: &str = "to_bytes"; + +const DEFINITION: &str = flow_lib::node_definition!("std/to_bytes.json"); + +fn build() -> BuildResult { + use once_cell::sync::Lazy; + static CACHE: Lazy> = + Lazy::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub string: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub bytes: bytes::Bytes, +} + +async fn run(mut _ctx: Context, input: Input) -> Result { + let string = input.string; + let bytes = bytes::Bytes::from(string); + + Ok(Output { bytes }) +} diff --git a/crates/cmds-std/src/std/to_string.rs b/crates/cmds-std/src/std/to_string.rs new file mode 100644 index 00000000..3318cfa8 --- /dev/null +++ b/crates/cmds-std/src/std/to_string.rs @@ -0,0 +1,68 @@ +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct ToString; + +const TO_STRING: &str = "to_string"; + +// Input +const STRINGIFY: &str = "stringify"; + +// Output +const RESULT: &str = "result"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub result: String, +} + +#[async_trait] +impl CommandTrait for ToString { + fn name(&self) -> Name { + TO_STRING.into() + } + + fn inputs(&self) -> Vec { + [CmdInput { + name: STRINGIFY.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: RESULT.into(), + r#type: ValueType::String, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _: Context, mut inputs: ValueSet) -> Result { + let input = inputs.swap_remove(STRINGIFY).unwrap_or("".into()); + + let result = match input { + Value::Decimal(v) => v.to_string(), + Value::U64(v) => v.to_string(), + Value::I64(v) => v.to_string(), + Value::U128(v) => v.to_string(), + Value::I128(v) => v.to_string(), + Value::F64(v) => v.to_string(), + Value::B32(v) => bs58::encode(&v).into_string(), + Value::B64(v) => bs58::encode(&v).into_string(), + Value::String(s) => s, + other => serde_json::to_string_pretty(&other).unwrap(), + }; + + // let result = serde_json::to_string(&output).unwrap(); + + Ok(value::to_map(&Output { result })?) + } +} + +flow_lib::submit!(CommandDescription::new(TO_STRING, |_| Ok(Box::new( + ToString +)))); diff --git a/crates/cmds-std/src/std/to_vec.rs b/crates/cmds-std/src/std/to_vec.rs new file mode 100644 index 00000000..16e76b9d --- /dev/null +++ b/crates/cmds-std/src/std/to_vec.rs @@ -0,0 +1,73 @@ +use crate::prelude::*; + +#[derive(Debug, Clone)] +pub struct ToVec; + +// Name +const TO_VEC: &str = "to_vec"; + +// Inputs +const FIRST: &str = "first"; +const SECOND: &str = "second"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub first: Value, + pub second: Option, +} + +// Outputs +const RESULT: &str = "result"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub result: Vec, +} + +#[async_trait] +impl CommandTrait for ToVec { + fn name(&self) -> Name { + TO_VEC.into() + } + + fn inputs(&self) -> Vec { + [ + CmdInput { + name: FIRST.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }, + CmdInput { + name: SECOND.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }, + ] + .to_vec() + } + + fn outputs(&self) -> Vec { + [CmdOutput { + name: RESULT.into(), + r#type: ValueType::Array, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, inputs: ValueSet) -> Result { + let Input { first, second } = value::from_map::(inputs)?; + + let result = if let Some(second) = second { + vec![first, second] + } else { + vec![first] + }; + + Ok(value::to_map(&Output { result })?) + } +} + +flow_lib::submit!(CommandDescription::new(TO_VEC, |_| Ok(Box::new(ToVec {})))); diff --git a/crates/cmds-std/src/storage/create_signed_url.rs b/crates/cmds-std/src/storage/create_signed_url.rs new file mode 100644 index 00000000..ab99b9d5 --- /dev/null +++ b/crates/cmds-std/src/storage/create_signed_url.rs @@ -0,0 +1,137 @@ +use super::FileSpec; +use crate::supabase_error; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; +use rust_decimal::Decimal; +use std::borrow::Cow; + +pub const NAME: &str = "storage_create_signed_url"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/create_signed_url.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize, Default)] +struct Transform { + #[serde(skip_serializing_if = "Option::is_none")] + width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + resize: Option, + #[serde(skip_serializing_if = "Option::is_none")] + format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + quality: Option, +} + +impl Transform { + fn all_none(&self) -> bool { + self.width.is_none() + && self.height.is_none() + && self.resize.is_none() + && self.format.is_none() + && self.quality.is_none() + } +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +enum StringOrBool { + String(String), + Bool(bool), +} + +impl Default for StringOrBool { + fn default() -> Self { + StringOrBool::Bool(false) + } +} + +#[derive(Serialize, Deserialize)] +struct Input { + #[serde(flatten)] + file: FileSpec, + #[serde(with = "value::decimal")] + expires_in: Decimal, + #[serde(default)] + transform: Transform, + #[serde(default)] + download: StringOrBool, +} + +#[derive(Serialize)] +struct Output { + url: String, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +struct RequestBody { + #[serde(with = "rust_decimal::serde::float")] + expiresIn: Decimal, + #[serde(skip_serializing_if = "Transform::all_none")] + transform: Transform, +} + +#[allow(non_snake_case)] +#[derive(Deserialize)] +struct SuccessBody { + signedURL: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let key = input.file.key(&ctx.flow_owner.id); + let url = format!("{}/storage/v1/object/sign/{}", ctx.endpoints.supabase, key); + tracing::debug!("using URL: {}", url); + let mut req = ctx.http.post(url); + + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + + let body = serde_json::value::to_raw_value(&RequestBody { + expiresIn: input.expires_in, + transform: input.transform, + })?; + tracing::debug!("using body: {}", body.get()); + + let resp = req.json(&body).send().await?; + + match resp.status() { + StatusCode::OK => { + let body = resp.json::().await?; + // https://github.com/supabase/storage-js/blob/fa44be8156295ba6320ffeff96bdf91016536a46/src/packages/StorageFileApi.ts#L395-L397 + let download = match input.download { + StringOrBool::String(s) => Cow::Owned(format!("&download={}", s)), + StringOrBool::Bool(true) => Cow::Borrowed("&download="), + StringOrBool::Bool(false) => Cow::Borrowed(""), + }; + Ok(Output { + url: format!( + "{}/storage/v1{}{}", + ctx.endpoints.supabase, body.signedURL, download + ), + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/delete.rs b/crates/cmds-std/src/storage/delete.rs new file mode 100644 index 00000000..e166acb2 --- /dev/null +++ b/crates/cmds-std/src/storage/delete.rs @@ -0,0 +1,52 @@ +use super::FileSpec; +use crate::supabase_error; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +pub const NAME: &str = "storage_delete"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/delete.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize)] +struct Output { + key: String, +} + +async fn run(mut ctx: Context, input: FileSpec) -> Result { + let key = input.key(&ctx.flow_owner.id); + let url = format!("{}/storage/v1/object/{}", ctx.endpoints.supabase, key); + tracing::debug!("using URL: {}", url); + let resp = ctx + .http + .delete(url) + .header(AUTHORIZATION, ctx.get_jwt_header().await?) + .send() + .await?; + + match resp.status() { + StatusCode::OK => Ok(Output { key }), + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/download.rs b/crates/cmds-std/src/storage/download.rs new file mode 100644 index 00000000..925762ba --- /dev/null +++ b/crates/cmds-std/src/storage/download.rs @@ -0,0 +1,78 @@ +use super::FileSpec; +use crate::supabase_error; +use flow_lib::command::prelude::*; +use reqwest::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + StatusCode, +}; + +pub const NAME: &str = "storage_download"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/download.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize)] +struct Output { + content: Value, + size: u64, + content_type: String, +} + +async fn run(mut ctx: Context, input: FileSpec) -> Result { + let key = input.key(&ctx.flow_owner.id); + let url = format!( + "{}/storage/v1/object/authenticated/{}", + ctx.endpoints.supabase, key + ); + tracing::debug!("using URL: {}", url); + let resp = ctx + .http + .get(url) + .header(AUTHORIZATION, ctx.get_jwt_header().await?) + .send() + .await?; + + match resp.status() { + StatusCode::OK => { + let content_type = resp + .headers() + .get(CONTENT_TYPE) + .and_then(|t| t.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_owned(); + let bytes = resp.bytes().await?; + let size = bytes.len() as u64; + let content = match std::str::from_utf8(&bytes) { + Ok(_) => Value::String(unsafe { String::from_utf8_unchecked(bytes.into()) }), + Err(_) => Value::Bytes(bytes), + }; + Ok(Output { + content, + size, + content_type, + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/explorer.rs b/crates/cmds-std/src/storage/explorer.rs new file mode 100644 index 00000000..6387ad84 --- /dev/null +++ b/crates/cmds-std/src/storage/explorer.rs @@ -0,0 +1,59 @@ +use flow_lib::command::prelude::*; + +pub const FIRE_EXPLORER: &str = "fileexplorer"; + +#[derive(Debug)] +pub struct ExplorerCommand { + outputs: Vec, + result: ValueSet, +} + +impl ExplorerCommand { + fn new(data: &NodeData) -> Self { + let outputs = data + .sources + .iter() + .map(|o| Output { + name: o.name.clone(), + r#type: o.r#type.clone(), + optional: false, + }) + .collect(); + Self { + outputs, + result: data + .targets_form + .form_data + .as_object() + .map(|o| { + o.iter() + .map(|(k, v)| (k.clone(), Value::from(v.clone()))) + .collect() + }) + .unwrap_or_default(), + } + } +} + +#[async_trait] +impl CommandTrait for ExplorerCommand { + fn name(&self) -> Name { + FIRE_EXPLORER.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + self.outputs.clone() + } + + async fn run(&self, _: Context, _: ValueSet) -> Result { + Ok(self.result.clone()) + } +} + +flow_lib::submit!(CommandDescription::new(FIRE_EXPLORER, |data: &NodeData| { + Ok(Box::new(ExplorerCommand::new(data))) +})); diff --git a/crates/cmds-std/src/storage/get_file_metadata.rs b/crates/cmds-std/src/storage/get_file_metadata.rs new file mode 100644 index 00000000..5c7ba630 --- /dev/null +++ b/crates/cmds-std/src/storage/get_file_metadata.rs @@ -0,0 +1,80 @@ +use super::FileSpec; +use crate::supabase_error; +use anyhow::anyhow; +use flow_lib::command::prelude::*; +use reqwest::{ + header::{AUTHORIZATION, CONTENT_TYPE, LAST_MODIFIED}, + StatusCode, +}; + +pub const NAME: &str = "storage_get_file_metadata"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/get_file_metadata.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize)] +struct Output { + key: String, + content_type: String, + last_modified: String, +} + +async fn run(mut ctx: Context, input: FileSpec) -> Result { + let key = input.key(&ctx.flow_owner.id); + let url = format!( + "{}/storage/v1/object/info/authenticated/{}", + ctx.endpoints.supabase, key + ); + tracing::debug!("using URL: {}", url); + let resp = ctx + .http + .head(url) + .header(AUTHORIZATION, ctx.get_jwt_header().await?) + .send() + .await?; + + match resp.status() { + StatusCode::OK => { + let headers = resp.headers(); + Ok(Output { + key, + content_type: String::from_utf8_lossy( + headers + .get(CONTENT_TYPE) + .ok_or_else(|| anyhow!("missing header: content-type"))? + .as_bytes(), + ) + .to_string(), + last_modified: String::from_utf8_lossy( + headers + .get(LAST_MODIFIED) + .ok_or_else(|| anyhow!("missing header: last-modified"))? + .as_bytes(), + ) + .to_string(), + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/get_public_url.rs b/crates/cmds-std/src/storage/get_public_url.rs new file mode 100644 index 00000000..ba87c7fa --- /dev/null +++ b/crates/cmds-std/src/storage/get_public_url.rs @@ -0,0 +1,39 @@ +use super::FileSpec; +use flow_lib::command::prelude::*; + +pub const NAME: &str = "storage_get_public_url"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/get_public_url.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize)] +struct Output { + url: String, +} + +async fn run(ctx: Context, input: FileSpec) -> Result { + let key = input.key(&ctx.flow_owner.id); + let url = format!( + "{}/storage/v1/object/public/{}", + ctx.endpoints.supabase, key + ); + Ok(Output { url }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/list.rs b/crates/cmds-std/src/storage/list.rs new file mode 100644 index 00000000..f0a1f027 --- /dev/null +++ b/crates/cmds-std/src/storage/list.rs @@ -0,0 +1,92 @@ +use crate::supabase_error; +use flow_lib::command::prelude::*; +use reqwest::{header::AUTHORIZATION, StatusCode}; +use serde_json::json; +use std::path::PathBuf; + +pub const NAME: &str = "storage_list"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/list.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +#[derive(Serialize, Deserialize)] +struct Input { + bucket: String, + #[serde(default)] + path: PathBuf, +} + +#[derive(Serialize)] +struct Output { + files: Vec, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let path = input.path; + let prefix = if ["user-storages", "user-public-storages"].contains(&input.bucket.as_str()) { + format!("{}/{}", ctx.flow_owner.id, path.display()) + } else { + format!("{}", path.display()) + }; + let url = format!( + "{}/storage/v1/object/list/{}", + ctx.endpoints.supabase, input.bucket, + ); + tracing::debug!("using URL: {}", url); + tracing::debug!("using prefix: {}", prefix); + let req = ctx + .http + .post(url) + .header(AUTHORIZATION, ctx.get_jwt_header().await?); + + let body = json!({ + "prefix": &prefix, + "limit": 1000, + "offset": 0, + "sortBy": { + "column": "name", + "order": "asc" + }, + }); + + let resp = req.json(&body).send().await?; + + match resp.status() { + StatusCode::OK => { + let body = resp.json::().await?; + #[derive(Deserialize)] + struct File { + name: PathBuf, + } + let files = serde_json::from_value::>(body)?; + Ok(Output { + files: files + .into_iter() + .map(|f| path.join(f.name).display().to_string()) + .collect(), + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/storage/mod.rs b/crates/cmds-std/src/storage/mod.rs new file mode 100644 index 00000000..e58a1a62 --- /dev/null +++ b/crates/cmds-std/src/storage/mod.rs @@ -0,0 +1,33 @@ +use flow_lib::UserId; +use std::path::PathBuf; + +pub mod create_signed_url; +pub mod delete; +pub mod download; +pub mod explorer; +pub mod get_file_metadata; +pub mod get_public_url; +pub mod list; +pub mod upload; + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum FileSpec { + Key { key: String }, + BucketPath { bucket: String, path: PathBuf }, +} + +impl FileSpec { + pub fn key(&self, user_id: &UserId) -> String { + match self { + FileSpec::Key { key } => key.clone(), + FileSpec::BucketPath { bucket, path } => { + if ["user-storages", "user-public-storages"].contains(&bucket.as_str()) { + format!("{}/{}/{}", bucket, user_id, path.display()) + } else { + format!("{}/{}", bucket, path.display()) + } + } + } + } +} diff --git a/crates/cmds-std/src/storage/upload.rs b/crates/cmds-std/src/storage/upload.rs new file mode 100644 index 00000000..9e7e1253 --- /dev/null +++ b/crates/cmds-std/src/storage/upload.rs @@ -0,0 +1,116 @@ +use crate::supabase_error; +use bytes::Bytes; +use flow_lib::command::prelude::*; +use mime_guess::MimeGuess; +use reqwest::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + StatusCode, +}; +use std::{borrow::Cow, path::PathBuf}; + +pub const NAME: &str = "storage_upload"; + +const DEFINITION: &str = flow_lib::node_definition!("storage/upload.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + +const fn bool_false() -> bool { + false +} + +const fn default_bucket() -> Cow<'static, str> { + Cow::Borrowed("user-storages") +} + +#[derive(Serialize, Deserialize)] +struct Input { + #[serde(default = "default_bucket")] + bucket: Cow<'static, str>, + path: PathBuf, + content_type: Option, + content: Bytes, + #[serde(default = "bool_false")] + overwrite: bool, +} + +#[derive(Serialize)] +struct Output { + key: String, + content_type: String, +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct SuccessBody { + Key: String, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + let auth = ctx.get_jwt_header().await?; + + let content_type = input.content_type.unwrap_or_else(|| { + MimeGuess::from_path(&input.path) + .first_raw() + .unwrap_or_else(|| match std::str::from_utf8(&input.content) { + Ok(_) => "text/plain", + Err(_) => "application/octet-stream", + }) + .to_owned() + }); + let url = { + use std::fmt::Write; + let mut url = format!( + "{}/storage/v1/object/{}", + ctx.endpoints.supabase, input.bucket + ); + if ["user-storages", "user-public-storages"].contains(&input.bucket.as_ref()) { + write!(&mut url, "/{}", ctx.flow_owner.id).unwrap(); + } + write!(&mut url, "/{}", input.path.display()).unwrap(); + url + }; + + let mut req = ctx + .http + .post(url) + .header(AUTHORIZATION, auth) + .header(CONTENT_TYPE, &content_type) + .body(input.content); + + if input.overwrite { + req = req.header("x-upsert", "true"); + } + + let resp = req.send().await?; + + match resp.status() { + StatusCode::OK => { + let resp = resp.json::().await?; + Ok(Output { + key: resp.Key, + content_type, + }) + } + code => Err(supabase_error(code, resp).await), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } +} diff --git a/crates/cmds-std/src/supabase/mod.rs b/crates/cmds-std/src/supabase/mod.rs new file mode 100644 index 00000000..5b15d16c --- /dev/null +++ b/crates/cmds-std/src/supabase/mod.rs @@ -0,0 +1,64 @@ +use crate::prelude::*; +use anyhow::anyhow; +use flow_lib::config::node::Permissions; +use reqwest::{header::AUTHORIZATION, StatusCode}; + +// Command Name +const NAME: &str = "supabase"; + +const DEFINITION: &str = flow_lib::node_definition!("supabase/supabase.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = BuilderCache::new(|| { + Ok(CmdBuilder::new(DEFINITION)? + .check_name(NAME)? + .permissions(Permissions { user_tokens: true })) + }); + + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + pub string: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub res: HashMap, +} + +async fn run(mut ctx: Context, input: Input) -> Result { + // info!("{:#?}", ctx.environment); + + // info!("{:#?}", ctx.user.id); + // let bearer = ctx.environment.get("authorization_bearer").unwrap(); + // let apikey = ctx.environment.get("apikey").unwrap(); + + // // headers + // let mut headers = reqwest::header::HeaderMap::new(); + // headers.insert( + // "authorization_bearer", + // HeaderValue::from_str(&bearer).unwrap(), + // ); + // headers.insert("apikey", HeaderValue::from_str(&apikey).unwrap()); + + let mut req = ctx + .http + .post(format!("{}/rest/v1/users_nft", ctx.endpoints.supabase)) + .json(&input.string); + + req = req.header(AUTHORIZATION, ctx.get_jwt_header().await?); + + let resp = req.send().await.map_err(|e| anyhow!("HTTP error: {}", e))?; + + match resp.status() { + StatusCode::OK => Ok(Output { + res: resp.json::>().await?, + }), + code => Err(anyhow!("HTTP error: {}", code)), + } + // https://hyjboblkjeevkzaqsyxe.supabase.co/rest/v1/users_nft?id=eq.1&select=* +} diff --git a/crates/cmds-std/src/wait_cmd.rs b/crates/cmds-std/src/wait_cmd.rs new file mode 100644 index 00000000..e3484542 --- /dev/null +++ b/crates/cmds-std/src/wait_cmd.rs @@ -0,0 +1,33 @@ +use flow_lib::command::prelude::*; +use std::time::Duration; +use tokio::time; + +pub const NAME: &str = "wait"; + +const DEFINITION: &str = flow_lib::node_definition!("wait.json"); + +fn build() -> BuildResult { + static CACHE: BuilderCache = + BuilderCache::new(|| CmdBuilder::new(DEFINITION)?.check_name(NAME)); + Ok(CACHE.clone()?.build(run)) +} + +flow_lib::submit!(CommandDescription::new(NAME, |_| { build() })); + +#[derive(Serialize, Deserialize, Debug)] +pub struct Input { + value: Value, + wait_for: Value, + duration_ms: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output {} + +async fn run(_ctx: Context, input: Input) -> Result { + if let Some(duration) = input.duration_ms { + time::sleep(Duration::from_millis(duration)).await; + } + + Ok(Output {}) +} diff --git a/crates/command-rpc/Cargo.toml b/crates/command-rpc/Cargo.toml new file mode 100644 index 00000000..56e78273 --- /dev/null +++ b/crates/command-rpc/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "command-rpc" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix = "0.13.3" +async-trait = "0.1.79" +flow-lib.workspace = true +futures = "0.3.30" +inventory = "0.3.15" +serde.workspace = true +serde_with = "3.7.0" +srpc.workspace = true +thiserror = "1.0.63" +tokio = { version = "1", features = ["net"] } +tokio-tungstenite = { version = "0.24.0", features = ["__rustls-tls"] } +tower = "0.4.13" +tracing = "0.1.40" +url = { version = "2.5.0", features = ["serde"] } + +[dev-dependencies] +tokio = { version = "1", features = ["macros"] } diff --git a/crates/command-rpc/src/client.rs b/crates/command-rpc/src/client.rs new file mode 100644 index 00000000..094dfa32 --- /dev/null +++ b/crates/command-rpc/src/client.rs @@ -0,0 +1,278 @@ +//! A command proxy which calls remote server to execute the actual command. + +use async_trait::async_trait; +use flow_lib::{ + command::{prelude::*, InstructionInfo}, + config::Endpoints, + context::CommandContext, + ContextConfig, FlowRunId, NodeId, User, +}; +use serde_with::{serde_as, DisplayFromStr}; +use srpc::GetBaseUrl; +use std::{collections::HashMap, convert::Infallible}; +use tower::util::ServiceExt; +use tracing::Instrument; +use url::Url; + +struct LogSvc { + span: tracing::Span, +} + +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +struct Log { + #[serde_as(as = "DisplayFromStr")] + level: tracing::Level, + content: String, +} + +impl tower::Service for LogSvc { + type Error = Infallible; + type Response = (); + type Future = std::future::Ready>; + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + fn call(&mut self, req: Log) -> Self::Future { + self.span.in_scope(|| { + match req.level { + tracing::Level::ERROR => tracing::error!(message = req.content), + tracing::Level::WARN => tracing::warn!(message = req.content), + tracing::Level::INFO => tracing::info!(message = req.content), + tracing::Level::DEBUG => tracing::debug!(message = req.content), + tracing::Level::TRACE => tracing::trace!(message = req.content), + } + std::future::ready(Ok(())) + }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct ServiceProxy { + name: String, + id: String, + base_url: Url, + #[serde(skip)] + drop: Option>, +} + +impl Drop for ServiceProxy { + fn drop(&mut self) { + if let Some(addr) = &self.drop { + addr.do_send(srpc::RemoveService { + name: self.name.clone(), + id: self.id.clone(), + }); + } + } +} + +impl ServiceProxy { + fn new( + result: srpc::RegisterServiceResult, + base_url: Url, + server: &actix::Addr, + ) -> Self { + Self { + name: result.name, + id: result.id, + base_url, + drop: result.old_service.is_none().then(|| server.clone()), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct CommandContextData { + flow_run_id: FlowRunId, + node_id: NodeId, + times: u32, + svc: ServiceProxy, + log: ServiceProxy, +} + +#[derive(Serialize, Deserialize, Debug)] +struct ContextProxy { + flow_owner: User, + started_by: User, + cfg: ContextConfig, + environment: HashMap, + endpoints: Endpoints, + command: Option, + signer: ServiceProxy, +} + +impl ContextProxy { + async fn new( + Context { + flow_owner, + started_by, + cfg, + http: _, + solana_client: _, + environment, + endpoints, + extensions, + command, + signer, + get_jwt: _, + }: Context, + ) -> Result { + let server = extensions + .get::>() + .ok_or_else(|| CommandError::msg("srpc::Server not available"))?; + let our_base_url = server + .send(GetBaseUrl) + .await? + .ok_or_else(|| CommandError::msg("srpc::Server is not listening on any interfaces"))?; + let flow_run_id = command + .as_ref() + .map(|c| c.flow_run_id) + .ok_or_else(|| CommandError::msg("CommandContext not available"))?; + + let signer = server + .send(srpc::RegisterJsonService::new( + "signer".to_owned(), + flow_run_id.to_string(), + signer, + )) + .await?; + + let command = match command { + Some(command) => { + Some(CommandContextData::new(command, our_base_url.clone(), server).await?) + } + None => None, + }; + + Ok(Self { + flow_owner, + started_by, + cfg, + environment, + endpoints, + command, + signer: ServiceProxy::new(signer, our_base_url, server), + }) + } +} + +impl CommandContextData { + async fn new( + CommandContext { + svc, + flow_run_id, + node_id, + times, + }: CommandContext, + base_url: Url, + server: &actix::Addr, + ) -> Result { + let span = tracing::Span::current(); + let svc = server + .send(srpc::RegisterJsonService::new( + "execute".to_owned(), + format!("{};{};{}", flow_run_id, node_id, times), + svc.map_future({ + let span = span.clone(); + move |f| f.instrument(span.clone()) + }), + )) + .await?; + let log = server + .send(srpc::RegisterJsonService::new( + "log".to_owned(), + format!("{};{};{}", flow_run_id, node_id, times), + LogSvc { span }, + )) + .await?; + Ok(Self { + flow_run_id, + node_id, + times, + svc: ServiceProxy::new(svc, base_url.clone(), server), + log: ServiceProxy::new(log, base_url, server), + }) + } +} + +#[derive(Serialize, Debug)] +struct RunInput<'a> { + ctx: &'a ContextProxy, + params: ValueSet, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct RunOutput(Result); + +pub struct RpcCommandClient { + base_url: Url, + svc_id: String, + node_data: NodeData, +} + +impl RpcCommandClient { + pub fn new(base_url: Url, svc_id: String, node_data: NodeData) -> Self { + Self { + base_url, + svc_id, + node_data, + } + } +} + +const RUN_SVC: &str = "run"; + +#[async_trait] +impl CommandTrait for RpcCommandClient { + fn name(&self) -> Name { + self.node_data.node_id.clone() + } + + fn inputs(&self) -> Vec { + self.node_data.inputs() + } + + fn outputs(&self) -> Vec { + self.node_data.outputs() + } + + async fn run(&self, ctx: Context, params: ValueSet) -> Result { + let url = self.base_url.join("call").unwrap(); + let http = ctx.http.clone(); + let ctx_proxy = ContextProxy::new(ctx).await?; + let resp = http + .post(url) + .json(&srpc::Request { + // HTTP protocol doesn't need an envelope + envelope: "".to_owned(), + svc_name: RUN_SVC.into(), + svc_id: self.svc_id.clone(), + input: RunInput { + ctx: &ctx_proxy, + params, + }, + }) + .send() + .await? + .json::>() + .await? + .data + .0 + .map_err(CommandError::msg); + + // ctx_proxy must persist for the duration of the HTTP request + // although rust won't drop it early + // we call drop here to make the intention explicit + drop(ctx_proxy); + + resp + } + + fn instruction_info(&self) -> Option { + self.node_data.instruction_info.clone() + } +} diff --git a/crates/command-rpc/src/lib.rs b/crates/command-rpc/src/lib.rs new file mode 100644 index 00000000..c07f47e0 --- /dev/null +++ b/crates/command-rpc/src/lib.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod server; diff --git a/crates/command-rpc/src/server.rs b/crates/command-rpc/src/server.rs new file mode 100644 index 00000000..fadde374 --- /dev/null +++ b/crates/command-rpc/src/server.rs @@ -0,0 +1,53 @@ +//! 2-way communication: +//! - CommandHost: contain code to run some commands. +//! - FlowHost: run flows and commands, but might need to use CommandHost to run some commands. +//! +//! CommandHost will connect first, FlowHost will listen on publicly available interfaces. +//! +//! When starting commands: +//! - FlowHost make a proxy on localhost that forward to CommandHost +//! - RpcCommandClient connect to the localhost proxy +//! - CommandHost start the actual command and interchange data though the proxy +//! +//! Starting a command: +//! - Send a request using RUN_SVC, input type is RunInput +//! - FlowHost must create proxies for signer, log, execute services. +//! - When the command finished, the server will return RunOutput + +use flow_lib::command::CommandDescription; +use std::{borrow::Cow, collections::BTreeMap}; +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +pub enum Error { + #[error(transparent)] + Ws(#[from] tokio_tungstenite::tungstenite::Error), +} + +pub type WsStream = + tokio_tungstenite::WebSocketStream>; + +#[allow(dead_code)] +pub struct CommandHost { + natives: BTreeMap, CommandDescription>, + stream: WsStream, +} + +impl CommandHost { + pub async fn connect(url: &str) -> Result { + let (stream, _) = tokio_tungstenite::connect_async(url).await?; + let mut natives = BTreeMap::new(); + for d in inventory::iter::() { + let name = d.name.clone(); + if natives.insert(name.clone(), d.clone()).is_some() { + tracing::error!("duplicated command {:?}", name); + } + } + Ok(Self { + stream, + natives: Default::default(), + }) + } +} + +pub struct FlowHost {} diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml new file mode 100644 index 00000000..0d121970 --- /dev/null +++ b/crates/db/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "db" +version = "0.0.0" +edition = "2021" + +[dependencies] +flow = { workspace = true } +flow-lib = { workspace = true } +value = { workspace = true } +utils = { workspace = true } + +serde = "1" +serde_json = { version = "1", features = ["raw_value"] } +thiserror = "1.0.31" +tokio = "1" +uuid = { version = "1.0.0", features = ["v4", "v7", "serde"] } +deadpool-postgres = { version = "0.11", features = ["rt_tokio_1"] } +tokio-postgres-rustls = "0.9" +rustls = "0.20" +rustls-pemfile = "1" +rustls-native-certs = "0.6.3" +chrono = "0.4" +reqwest = { version = "0.12", features = ["rustls-tls", "gzip"] } +bytes = "1" +url = "2.2" +bcrypt = { version = "0.13", default-features = false, features = ["std"] } +bs58 = "0.4" +base64 = "0.13" +toml = "0.5" +rand = "0.8" +blake3 = "1.3" +anyhow = "1" +tracing = "0.1" +tracing-subscriber = "0.3" +hashbrown = { version = "0.14", features = ["serde"] } +futures-util = "0.3" +kv = { version = "=0.24", features = ["json-value", "bincode-value", "msgpack-value"] } +async-trait = "0.1" +tower = "0.4" +either = "1.9" +serde_bytes = "0.11" +csv = "1.3.0" +chacha20poly1305 = "0.10.1" +serde_with = { version = "3.9.0", features = ["base64"] } +zeroize = "1.8.1" +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +actix-web = { version = "4.9.0", default-features = false } + +[dependencies.tokio-postgres] +version = "0.7.10" +features = ["with-serde_json-1", "with-uuid-1", "with-chrono-0_4"] + +[dev-dependencies] +tempfile = "3.10.1" diff --git a/crates/db/src/apikey.rs b/crates/db/src/apikey.rs new file mode 100644 index 00000000..54eb306f --- /dev/null +++ b/crates/db/src/apikey.rs @@ -0,0 +1,258 @@ +use crate::{ + connection::{AdminConn, DbClient, UserConnection}, + Error, +}; +use chrono::NaiveDateTime; +use flow_lib::UserId; +use serde::Serialize; +use thiserror::Error as ThisError; +use tokio_postgres::error::SqlState; + +#[derive(Clone, Serialize)] +pub struct KeyInfo { + pub name: String, + pub user_id: UserId, + pub created_at: NaiveDateTime, +} + +impl KeyInfo { + pub fn new(name: &str, user_id: UserId) -> Self { + Self { + name: name.trim().to_owned(), + user_id, + created_at: chrono::Utc::now().naive_utc(), + } + } +} + +#[derive(Clone, Serialize)] +pub struct APIKey { + pub key_hash: String, + pub trimmed_key: String, + #[serde(flatten)] + pub info: KeyInfo, +} + +/// A BLAKE3 key +pub const KEY_PREFIX: &str = "b3-"; + +impl APIKey { + pub fn generate(rng: &mut R, info: KeyInfo) -> (Self, String) { + let mut key = KEY_PREFIX.to_owned(); + key.push_str(&base64::encode_config( + rng.gen::<[u8; 32]>(), + base64::URL_SAFE_NO_PAD, + )); + let trimmed_key = "*****".to_owned() + &key[key.len() - 5..]; + let key_hash = + base64::encode_config(blake3::hash(key.as_bytes()).as_bytes(), base64::URL_SAFE); + ( + Self { + key_hash, + trimmed_key, + info, + }, + key, + ) + } +} + +#[derive(ThisError, Debug)] +#[error("name-conflict")] +pub struct NameConflict; + +fn convert_error(error: Error) -> Error { + match error { + Error::Unauthorized => Error::Unauthorized, + Error::SpawnError(e) => Error::SpawnError(e), + Error::EncryptionError => Error::EncryptionError, + Error::Timeout => Error::Timeout, + Error::NoEncryptionKey => Error::NoEncryptionKey, + Error::NotSupported => Error::NotSupported, + Error::LogicError(_) => unreachable!("get_conn should not return this variant"), + Error::CreatePool(e) => Error::CreatePool(e), + Error::GetDbConnection(e) => Error::GetDbConnection(e), + Error::InitDb(e) => Error::InitDb(e), + Error::Execute { + error, + context, + location, + } => Error::Execute { + error, + context, + location, + }, + Error::Data { + error, + context, + location, + } => Error::Data { + error, + context, + location, + }, + Error::Json { + error, + context, + location, + } => Error::Json { + error, + context, + location, + }, + Error::ResourceNotFound { kind, id, location } => { + Error::ResourceNotFound { kind, id, location } + } + Error::Io(e) => Error::Io(e), + Error::NoCert => Error::NoCert, + Error::AddCert(e) => Error::AddCert(e), + Error::Deserialize(e) => Error::Deserialize(e), + Error::Storage(e) => Error::Storage(e), + Error::Bcrypt => Error::Bcrypt, + Error::Base58 => Error::Base58, + Error::LocalStorage { + error, + context, + location, + } => Error::LocalStorage { + error, + context, + location, + }, + Error::ProxyError(e) => Error::ProxyError(e), + Error::Parsing { + error, + context, + location, + } => Error::Parsing { + error, + context, + location, + }, + } +} + +impl UserConnection { + pub async fn create_apikey(&self, name: &str) -> Result<(APIKey, String), Error> { + let mut rng = rand::thread_rng(); + let info = KeyInfo::new(name, self.user_id); + + let conn = self.pool.get_conn().await.map_err(convert_error)?; + let stmt = conn + .prepare_cached( + "INSERT INTO apikeys ( + key_hash, + user_id, + name, + trimmed_key, + created_at + ) VALUES ($1, $2, $3, $4, $5)", + ) + .await + .map_err(Error::exec("prepare"))?; + + let (key, full_key) = loop { + let (key, full_key) = APIKey::generate(&mut rng, info.clone()); + let result = conn + .execute( + &stmt, + &[ + &key.key_hash, + &key.info.user_id, + &key.info.name, + &key.trimmed_key, + &key.info.created_at, + ], + ) + .await; + match result { + Ok(_) => break (key, full_key), + Err(error) => { + if let Some(db_error) = error.as_db_error() { + if *db_error.code() == SqlState::UNIQUE_VIOLATION { + match db_error.constraint() { + Some("uc-user_id-name") => { + return Err(Error::LogicError(NameConflict)) + } + Some("apikeys_pkey") => { + continue; + } + _ => {} + } + } + } + return Err(Error::exec("insert_apikey")(error)); + } + } + }; + + Ok((key, full_key)) + } + + pub async fn delete_apikey(&self, key_hash: &str) -> crate::Result<()> { + let conn = self.pool.get_conn().await?; + let affected = conn + .do_execute( + "DELETE FROM apikeys WHERE key_hash = $1 AND user_id = $2", + &[&key_hash, &self.user_id], + ) + .await + .map_err(Error::exec("delete_apikey"))?; + if affected != 1 { + return Err(Error::not_found("apikey", key_hash)); + } + Ok(()) + } +} + +pub struct User { + pub user_id: UserId, + pub pubkey: [u8; 32], +} + +impl AdminConn { + pub async fn get_user_from_apikey(self, key: &str) -> crate::Result { + let key_hash = + base64::encode_config(blake3::hash(key.as_bytes()).as_bytes(), base64::URL_SAFE); + let conn = self.pool.get_conn().await?; + let row = conn + .do_query_one( + "SELECT + users_public.user_id, + users_public.pub_key + FROM apikeys LEFT JOIN users_public + ON apikeys.user_id = users_public.user_id + WHERE apikeys.key_hash = $1", + &[&key_hash], + ) + .await + .map_err(Error::exec("get_apikey"))?; + let user_id: UserId = row.try_get(0).map_err(Error::data("user_id"))?; + let pubkey = { + let s: String = row.try_get(1).map_err(Error::data("pub_key"))?; + let mut buf = [0u8; 32]; + let size = bs58::decode(&s).into(&mut buf).map_err(|_| Error::Base58)?; + if size != buf.len() { + return Err(Error::Base58); + } + buf + }; + + Ok(User { user_id, pubkey }) + } + + pub async fn get_user_id_from_apikey(self, key: &str) -> crate::Result { + let key_hash = + base64::encode_config(blake3::hash(key.as_bytes()).as_bytes(), base64::URL_SAFE); + let conn = self.pool.get_conn().await?; + let row = conn + .do_query_one( + "SELECT user_id FROM apikeys WHERE key_hash = $1", + &[&key_hash], + ) + .await + .map_err(Error::exec("get_apikey"))?; + let user_id = row.try_get(0).map_err(Error::data("user_id"))?; + Ok(user_id) + } +} diff --git a/crates/db/src/config.rs b/crates/db/src/config.rs new file mode 100644 index 00000000..1d343479 --- /dev/null +++ b/crates/db/src/config.rs @@ -0,0 +1,131 @@ +use chacha20poly1305::{aead::Aead, AeadCore, ChaCha20Poly1305, KeyInit}; +use flow_lib::solana::Keypair; +use serde::{Deserialize, Serialize}; +use serde_with::{base64::Base64, serde_as}; +use std::fmt::Display; +use zeroize::Zeroize; + +#[serde_as] +#[derive(Deserialize, Clone)] +pub(crate) struct EncryptionKey(#[serde_as(as = "Base64")] [u8; 32]); + +impl Drop for EncryptionKey { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Encrypted { + #[serde_as(as = "Base64")] + #[serde(rename = "n")] + pub nonce: [u8; 12], + #[serde_as(as = "Base64")] + #[serde(rename = "c")] + pub ciphertext: Vec, +} + +impl EncryptionKey { + pub(crate) fn encrypt(&self, plaintext: &[u8]) -> Encrypted { + let cipher = ChaCha20Poly1305::new_from_slice(&self.0).expect("we use correct length"); + let nonce = ChaCha20Poly1305::generate_nonce(&mut rand::thread_rng()); + let ciphertext = cipher.encrypt(&nonce, plaintext).unwrap(); + Encrypted { + nonce: nonce.into(), + ciphertext, + } + } + + pub(crate) fn decrypt( + &self, + encrypted: &Encrypted, + ) -> Result, chacha20poly1305::Error> { + let cipher = ChaCha20Poly1305::new_from_slice(&self.0).expect("we use correct length"); + cipher.decrypt((&encrypted.nonce).into(), encrypted.ciphertext.as_ref()) + } + + pub(crate) fn encrypt_keypair(&self, keypair: &Keypair) -> Encrypted { + self.encrypt(keypair.secret().as_bytes()) + } + + pub(crate) fn decrypt_keypair( + &self, + encrypted: &Encrypted, + ) -> Result { + let mut secret = self.decrypt(encrypted)?; + let signing_key = ed25519_dalek::SigningKey::from_bytes( + &secret + .as_slice() + .try_into() + .map_err(|_| chacha20poly1305::Error)?, + ); + secret.zeroize(); + Keypair::from_bytes(&signing_key.to_keypair_bytes()).map_err(|_| chacha20poly1305::Error) + } +} + +#[derive(Deserialize)] +pub struct DbConfig { + pub user: String, + pub password: String, + pub dbname: String, + pub host: String, + pub port: u16, + #[serde(default)] + pub ssl: SslConfig, + pub max_pool_size: Option, + pub(crate) encryption_key: Option, +} + +#[derive(Deserialize, Clone, Default)] +pub struct SslConfig { + pub enabled: bool, + pub cert: Option, +} + +impl Display for DbConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "host={} port={} user={} password={} dbname={}", + self.host, self.port, self.user, self.password, self.dbname, + )) + } +} + +impl Default for DbConfig { + fn default() -> Self { + Self { + user: "postgres".into(), + password: "spacepass".into(), + dbname: "space-operator-db".into(), + host: "127.0.0.1".into(), + port: 7979, + ssl: <_>::default(), + max_pool_size: None, + encryption_key: None, + } + } +} + +#[derive(Deserialize, Clone)] +pub struct ProxiedDbConfig { + pub upstream_url: String, + pub api_keys: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encryption() { + let mut rng = rand::thread_rng(); + let key = EncryptionKey(ChaCha20Poly1305::generate_key(&mut rng).into()); + let keypair = + Keypair::from_bytes(&ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes()) + .unwrap(); + let decrypted = key.decrypt_keypair(&key.encrypt_keypair(&keypair)).unwrap(); + assert_eq!(keypair, decrypted); + } +} diff --git a/crates/db/src/connection/admin.rs b/crates/db/src/connection/admin.rs new file mode 100644 index 00000000..73c9923e --- /dev/null +++ b/crates/db/src/connection/admin.rs @@ -0,0 +1,578 @@ +use crate::{local_storage::CacheBucket, pool::RealDbPool, Error, FlowRunLogsRow, LocalStorage}; +use anyhow::anyhow; +use bytes::Bytes; +use chrono::Utc; +use deadpool_postgres::Transaction; +use flow_lib::{FlowRunId, UserId}; +use futures_util::SinkExt; +use std::{borrow::Borrow, time::Duration}; +use tokio_postgres::{ + binary_copy::BinaryCopyInWriter, + types::{Json, Type}, +}; +use value::Value; + +use super::{csv_export, DbClient, ExportedUserData}; + +pub struct AdminConn { + pub(crate) pool: RealDbPool, + pub(crate) local: LocalStorage, +} + +#[derive(Debug)] +pub struct LoginCredential { + pub email: String, + pub password: String, +} + +#[derive(Debug)] +pub struct FlowRunInfo { + pub user_id: UserId, + pub shared_with: Vec, +} + +struct UserIdCache; + +impl CacheBucket for UserIdCache { + type Key = str; + type EncodedKey = kv::Raw; + type Object = UserId; + fn name() -> &'static str { + "UserIdCache" + } + fn encode_key(key: &Self::Key) -> Self::EncodedKey { + key.into() + } + fn cache_time() -> Duration { + Duration::from_secs(120) + } + fn can_read(_obj: &Self::Object, _user_id: &UserId) -> bool { + true + } +} + +impl AdminConn { + pub fn new(pool: RealDbPool, local: LocalStorage) -> AdminConn { + Self { pool, local } + } + + pub async fn get_user_id_by_pubkey(&self, pk_bs58: &str) -> crate::Result> { + if let Some(cached) = self.local.get_cache::(&UserId::nil(), pk_bs58) { + return Ok(Some(cached)); + } + let result = self.get_user_id_by_pubkey_impl(pk_bs58).await; + if let Ok(Some(id)) = &result { + if let Err(error) = self.local.set_cache::(pk_bs58, *id) { + tracing::error!("set_cache error: {}", error); + } + } + result + } + + async fn get_user_id_by_pubkey_impl(&self, pk_bs58: &str) -> crate::Result> { + let conn = self.pool.get_conn().await?; + conn.do_query_opt( + "SELECT user_id FROM users_public WHERE pub_key = $1", + &[&pk_bs58], + ) + .await + .map_err(Error::exec("query users_public"))? + .map(|row| row.try_get(0)) + .transpose() + .map_err(Error::data("users_public.user_id")) + } + + pub async fn get_login_credential(&self, user_id: UserId) -> crate::Result { + let pw = self.local.get_or_generate_password(&user_id)?; + let conn = self.pool.get_conn().await?; + let email = conn + .do_query_one( + "UPDATE auth.users SET encrypted_password = $1 WHERE id = $2 RETURNING email", + &[&pw.encrypted_password, &user_id], + ) + .await + .map_err(Error::exec("update users"))? + .try_get::<_, String>(0) + .map_err(Error::data("users.email"))?; + Ok(LoginCredential { + email, + password: pw.password, + }) + } + + pub async fn get_flow_run_info(&self, run_id: FlowRunId) -> crate::Result { + let conn = self.pool.get_conn().await?; + let user_id: UserId = conn + .do_query_one("SELECT user_id FROM flow_run WHERE id = $1", &[&run_id]) + .await + .map_err(Error::exec("query flow_run table"))? + .try_get(0) + .map_err(Error::data("flow_run.user_id"))?; + + let shared_with = conn + .do_query( + "SELECT user_id FROM flow_run_shared WHERE flow_run_id = $1", + &[&run_id], + ) + .await + .map_err(Error::exec("query flow_run_shared"))? + .into_iter() + .map(|row| row.try_get(0)) + .collect::, _>>() + .map_err(Error::data("flow_run_shared.user_id"))?; + Ok(FlowRunInfo { + user_id, + shared_with, + }) + } + + pub async fn get_flow_run_output(&self, run_id: FlowRunId) -> crate::Result { + let conn = self.pool.get_conn().await?; + let output = conn + .do_query_one("SELECT output FROM flow_run WHERE id = $1", &[&run_id]) + .await + .map_err(Error::exec("query flow_run"))? + .try_get::<_, Json>(0) + .map_err(Error::data("flow_run.output"))? + .0; + Ok(output) + } + + pub async fn insert_whitelist(&self, pk_bs58: &str) -> crate::Result<()> { + let info = format!("inserted at {}", Utc::now()); + let stmt = "INSERT INTO pubkey_whitelists (pubkey, info) VALUES ($1, $2) + ON CONFLICT (pubkey) DO NOTHING"; + let conn = self.pool.get_conn().await?; + conn.do_execute(stmt, &[&pk_bs58, &info]) + .await + .map_err(Error::exec("insert_whitelist"))?; + Ok(()) + } + + pub async fn get_natives_commands(self) -> crate::Result> { + let conn = self.pool.get_conn().await?; + conn.query( + r#"SELECT data->>'node_id' FROM nodes WHERE type = 'native' AND "isPublic""#, + &[], + ) + .await + .map_err(Error::exec("get_natives_commands"))? + .into_iter() + .map(|r| r.try_get::<_, String>(0)) + .collect::, _>>() + .map_err(Error::data("nodes.data->>'node_id'")) + } + + pub async fn copy_in_flow_run_logs(&self, rows: I) -> crate::Result + where + I: IntoIterator, + I::Item: Borrow, + { + let conn = self.pool.get_conn().await?; + let stmt = conn + .prepare_cached( + "COPY flow_run_logs ( + user_id, + flow_run_id, + log_index, + node_id, + times, + time, + log_level, + content, + module + ) FROM STDIN WITH (FORMAT binary)", + ) + .await + .map_err(Error::exec("prepare copy_in_flow_run_logs"))?; + let sink = conn + .copy_in::<_, Bytes>(&stmt) + .await + .map_err(Error::exec("start copy_in_flow_run_logs"))?; + let writer = BinaryCopyInWriter::new( + sink, + &[ + Type::UUID, // user_id + Type::UUID, // flow_run_id + Type::INT4, // log_index + Type::UUID, // node_id + Type::INT4, // times + Type::TIMESTAMP, // time + Type::VARCHAR, // log_level + Type::TEXT, // content + Type::TEXT, // module + ], + ); + futures_util::pin_mut!(writer); + let mut size = 0; + for row in rows { + let r = row.borrow(); + writer + .as_mut() + .write(&[ + &r.user_id, + &r.flow_run_id, + &r.log_index, + &r.node_id, + &r.times, + &r.time.naive_utc(), + &r.log_level, + &r.content, + &r.module, + ]) + .await + .map_err(Error::exec("write copy_in_flow_run_logs"))?; + size += 1; + } + let inserted = writer + .finish() + .await + .map_err(Error::exec("finish copy_in_flow_run_logs"))?; + if inserted != size { + return Err(Error::LogicError(anyhow!( + "size={}, inserted={}", + size, + inserted + ))); + } + Ok(inserted) + } + + pub async fn create_store(&mut self, user_id: &UserId, store_name: &str) -> crate::Result<()> { + let mut conn = self.pool.get_conn().await?; + let tx = conn + .transaction() + .await + .map_err(Error::exec("begin create_store"))?; + + let stmt = "INSERT INTO kvstore_metadata (user_id, store_name) VALUES ($1, $2)"; + tx.do_execute(stmt, &[user_id, &store_name]) + .await + .map_err(Error::exec("insert kvstore_metadata"))?; + + let stmt = "INSERT INTO user_quotas + (user_id, kvstore_count, kvstore_size) + VALUES ($1, 1, $2) + ON CONFLICT (user_id) DO UPDATE + SET kvstore_count = user_quotas.kvstore_count + 1, + kvstore_size = user_quotas.kvstore_size + $2 + WHERE user_quotas.kvstore_count + 1 <= user_quotas.kvstore_count_limit + AND user_quotas.kvstore_size + $2 <= user_quotas.kvstore_size_limit + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &(store_name.len() as i64)]) + .await + .map_err(Error::exec("update user_quotas"))?; + + tx.commit() + .await + .map_err(Error::exec("commit create_store"))?; + + Ok(()) + } + + pub async fn delete_store( + &mut self, + user_id: &UserId, + store_name: &str, + ) -> crate::Result { + let mut conn = self.pool.get_conn().await?; + let tx = conn + .transaction() + .await + .map_err(Error::exec("begin delete_store"))?; + + let stmt = "DELETE FROM kvstore_metadata + WHERE user_id = $1 AND store_name = $2 + RETURNING stats_size"; + let res = tx + .do_query_opt(stmt, &[user_id, &store_name]) + .await + .map_err(Error::exec("delete_store"))?; + let deleted = res.is_some(); + if let Some(row) = res { + let size: i64 = row + .try_get(0) + .map_err(Error::data("kvstore_metadata.stats_size"))?; + let size = size + store_name.len() as i64; + let stmt = "UPDATE user_quotas + SET kvstore_size = kvstore_size - $2, + kvstore_count = kvstore_count - 1 + WHERE user_id = $1 + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &size]) + .await + .map_err(Error::exec("update user_quotas"))?; + } + + tx.commit() + .await + .map_err(Error::exec("commit delete_store"))?; + + Ok(deleted) + } + + pub async fn insert_or_replace_item( + &mut self, + user_id: &UserId, + store_name: &str, + key: &str, + value: &Value, + ) -> crate::Result> { + let json = serde_json::value::to_raw_value(value).map_err(Error::json("json serialize"))?; + let mut conn = self.pool.get_conn().await?; + let tx = conn + .transaction() + .await + .map_err(Error::exec("insert_item start"))?; + + let stmt = "SELECT LENGTH(value::TEXT) + LENGTH(key), value FROM kvstore + WHERE user_id = $1 + AND store_name = $2 + AND key = $3"; + let (old_size, old_value) = tx + .do_query_opt(stmt, &[user_id, &store_name, &key]) + .await + .map_err(Error::exec("get existing value"))? + .map(|row| { + ( + row.try_get::<_, i32>(0), + row.try_get::<_, Json>(1).map(|v| Some(v.0)), + ) + }) + .map(|(r1, r2)| r1.and_then(|r1| Ok((r1, r2?)))) + .transpose() + .map_err(Error::data("parse value"))? + .unwrap_or((0, None)); + + let stmt = "INSERT INTO kvstore (user_id, store_name, key, value) + VALUES ($1, $2, $3, $4) + ON CONFLICT (user_id, store_name, key) + DO UPDATE SET value = $4 + RETURNING LENGTH(value::text) + LENGTH(key)"; + let new_size: i32 = tx + .do_query_one(stmt, &[user_id, &store_name, &key, &Json(&json)]) + .await + .map_err(Error::exec("update kvstore"))? + .try_get::<_, i32>(0) + .map_err(Error::data("INTEGER"))?; + + let changed = (new_size - old_size) as i64; + + let stmt = "UPDATE user_quotas + SET kvstore_size = kvstore_size + $2 + WHERE + user_id = $1 + AND kvstore_size + $2 < kvstore_size_limit + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &changed]) + .await + .map_err(Error::exec("update user_quotas"))?; + + let stmt = "UPDATE kvstore_metadata + SET stats_size = stats_size + $3 + WHERE + user_id = $1 AND store_name = $2 + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &store_name, &changed]) + .await + .map_err(Error::exec("update kvstore_metadata"))?; + + tx.commit() + .await + .map_err(Error::exec("insert_item commit"))?; + Ok(old_value) + } + + pub async fn remove_item( + &mut self, + user_id: &UserId, + store_name: &str, + key: &str, + ) -> crate::Result { + let mut conn = self.pool.get_conn().await?; + let tx = conn + .transaction() + .await + .map_err(Error::exec("remove_item start"))?; + + let stmt = "DELETE FROM kvstore + WHERE user_id = $1 + AND store_name = $2 + AND key = $3 + RETURNING LENGTH(value::TEXT) + LENGTH(key), value"; + let (old_size, old_value) = tx + .do_query_one(stmt, &[user_id, &store_name, &key]) + .await + .and_then(|row| { + Ok(( + row.try_get::<_, i32>(0)?, + row.try_get::<_, Json>(1).map(|v| v.0)?, + )) + }) + .map_err(Error::exec("remove_item"))?; + + let old_size = old_size as i64; + + let stmt = "UPDATE user_quotas + SET kvstore_size = kvstore_size - $2 + WHERE + user_id = $1 + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &old_size]) + .await + .map_err(Error::exec("update user_quotas"))?; + + let stmt = "UPDATE kvstore_metadata + SET stats_size = stats_size - $3 + WHERE + user_id = $1 AND store_name = $2 + RETURNING 0"; + tx.do_query_one(stmt, &[user_id, &store_name, &old_size]) + .await + .map_err(Error::exec("update kvstore_metadata"))?; + + tx.commit() + .await + .map_err(Error::exec("remove_item commit"))?; + Ok(old_value) + } + + pub async fn import_data(&mut self, data: ExportedUserData) -> crate::Result<()> { + let mut conn = self.pool.get_conn().await?; + let tx = conn.transaction().await.map_err(Error::exec("start"))?; + + tx.execute("SELECT auth.disable_users_triggers()", &[]) + .await + .map_err(Error::exec("disable trigger"))?; + + tx.execute("CREATE TEMP TABLE tmp_table (LIKE pubkey_whitelists INCLUDING DEFAULTS) ON COMMIT DROP", &[]).await.map_err(Error::exec("create temp table"))?; + copy_in(&tx, "tmp_table", data.pubkey_whitelists).await?; + tx.execute( + "INSERT INTO pubkey_whitelists + SELECT * FROM tmp_table + ON CONFLICT DO NOTHING;", + &[], + ) + .await + .map_err(Error::exec("bulk insert"))?; + + copy_in(&tx, "auth.users", data.users).await?; + tx.execute( + "UPDATE auth.users + SET + confirmation_token = '', + recovery_token = '', + email_change_token_new = '', + email_change = '', + email_change_token_current = '', + reauthentication_token = '', + phone_change = '', + phone_change_token = '' + WHERE id = $1 + ", + &[&data.user_id], + ) + .await + .map_err(Error::exec("fix users row"))?; + + copy_in(&tx, "auth.identities", data.identities).await?; + copy_in(&tx, "users_public", data.users_public).await?; + + copy_in(&tx, "wallets", data.wallets).await?; + update_id_sequence(&tx, "wallets", "id", "wallets_id_seq").await?; + + copy_in(&tx, "apikeys", data.apikeys).await?; + copy_in(&tx, "user_quotas", data.user_quotas).await?; + copy_in(&tx, "kvstore_metadata", data.kvstore_metadata).await?; + copy_in(&tx, "kvstore", data.kvstore).await?; + + copy_in(&tx, "flows", data.flows).await?; + update_id_sequence(&tx, "flows", "id", "flows_id_seq").await?; + + copy_in(&tx, "nodes", data.nodes).await?; + update_id_sequence(&tx, "nodes", "id", "nodes_id_seq").await?; + + tx.execute("SELECT auth.enable_users_triggers()", &[]) + .await + .map_err(Error::exec("enable trigger"))?; + + tx.commit().await.map_err(Error::exec("commit"))?; + + Ok(()) + } +} + +async fn update_id_sequence( + tx: &Transaction<'_>, + table: &str, + column: &str, + sequence_name: &str, +) -> crate::Result<()> { + let query = format!( + "SELECT setval('{}', (SELECT max({}) FROM {}), TRUE)", + sequence_name, column, table + ); + let stmt = tx + .prepare_cached(&query) + .await + .map_err(Error::exec("prepare"))?; + tx.execute(&stmt, &[]) + .await + .map_err(Error::exec("update sequence"))?; + Ok(()) +} + +async fn copy_in(tx: &Transaction<'_>, table: &str, data: String) -> crate::Result<()> { + let header = csv_export::reader() + .from_reader(data.as_bytes()) + .headers() + .map_err(Error::parsing("csv"))? + .into_iter() + .fold(String::new(), |mut r, header| { + if !r.is_empty() { + r.push(','); + } + std::fmt::write(&mut r, format_args!("{:?}", header)).unwrap(); + r + }); + let query = format!( + "COPY {} ({}) FROM stdin WITH (FORMAT csv, DELIMITER ';', QUOTE '''', HEADER MATCH)", + table, header + ); + let sink = tx + .copy_in::<_, Bytes>(&query) + .await + .map_err(Error::exec("copy-in users"))?; + futures_util::pin_mut!(sink); + sink.send(data.into()) + .await + .map_err(Error::data("write copy-in"))?; + sink.finish().await.map_err(Error::data("finish copy_in"))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{ + config::DbConfig, connection::ExportedUserData, pool::RealDbPool, LocalStorage, WasmStorage, + }; + use serde::Deserialize; + use toml::value::Table; + + #[tokio::test] + #[ignore] + async fn test_import() { + let full_config: Table = + toml::from_str(&std::fs::read_to_string("/tmp/local.toml").unwrap()).unwrap(); + let db_config = DbConfig::deserialize(full_config["db"].clone()).unwrap(); + let wasm = WasmStorage::new("http://localhost".parse().unwrap(), "", "").unwrap(); + let temp = tempfile::tempdir().unwrap(); + let local = LocalStorage::new(temp.path()).unwrap(); + let pool = RealDbPool::new(&db_config, wasm, local).await.unwrap(); + let mut conn = pool.get_admin_conn().await.unwrap(); + let data: ExportedUserData = + serde_json::from_str(&std::fs::read_to_string("/tmp/data.json").unwrap()).unwrap(); + conn.import_data(data).await.unwrap(); + } +} diff --git a/crates/db/src/connection/conn_impl.rs b/crates/db/src/connection/conn_impl.rs new file mode 100644 index 00000000..42f5adc3 --- /dev/null +++ b/crates/db/src/connection/conn_impl.rs @@ -0,0 +1,1627 @@ +use crate::{ + config::{Encrypted, EncryptionKey}, + local_storage::CacheBucket, + EncryptedWallet, +}; +use anyhow::anyhow; +use bytes::{Bytes, BytesMut}; +use client::FlowRow; +use deadpool_postgres::Transaction; +use flow::flow_set::{DeploymentId, Flow, FlowDeployment}; +use flow_lib::{ + config::client::NodeDataSkipWasm, + solana::{Pubkey, SolanaActionConfig}, +}; +use futures_util::StreamExt; +use std::str::FromStr; +use tokio::task::spawn_blocking; +use tokio_postgres::{binary_copy::BinaryCopyInWriter, types::Type}; +use utils::bs58_decode; + +use super::*; + +struct FlowRowCache; + +impl CacheBucket for FlowRowCache { + type Key = FlowId; + type EncodedKey = kv::Integer; + type Object = FlowRow; + + fn name() -> &'static str { + "FlowRowCache" + } + + fn can_read(obj: &Self::Object, user_id: &UserId) -> bool { + obj.user_id == *user_id + } + + fn encode_key(key: &Self::Key) -> Self::EncodedKey { + kv::Integer::from(*key) + } + + fn cache_time() -> Duration { + Duration::from_secs(10) + } +} + +struct FlowInfoCache; + +impl CacheBucket for FlowInfoCache { + type Key = FlowId; + type EncodedKey = kv::Integer; + type Object = FlowInfo; + + fn name() -> &'static str { + "FlowInfoCache" + } + + fn can_read(obj: &Self::Object, user_id: &UserId) -> bool { + obj.is_public || obj.user_id == *user_id + } + + fn encode_key(key: &Self::Key) -> Self::EncodedKey { + kv::Integer::from(*key) + } + + fn cache_time() -> Duration { + Duration::from_secs(10) + } +} + +struct EncryptedWalletCache; + +impl CacheBucket for EncryptedWalletCache { + type Key = UserId; + type EncodedKey = kv::Raw; + type Object = Vec; + + fn name() -> &'static str { + "EncryptedWalletCache" + } + + fn can_read(_: &Self::Object, _: &UserId) -> bool { + true + } + + fn encode_key(key: &Self::Key) -> Self::EncodedKey { + key.as_bytes().into() + } + + fn cache_time() -> Duration { + Duration::from_secs(10) + } +} + +struct FlowConfigCache; + +impl CacheBucket for FlowConfigCache { + type Key = FlowId; + type EncodedKey = kv::Integer; + type Object = ClientConfig; + + fn name() -> &'static str { + "FlowConfigCache" + } + + fn can_read(obj: &Self::Object, user_id: &UserId) -> bool { + obj.user_id == *user_id + } + + fn encode_key(key: &Self::Key) -> Self::EncodedKey { + kv::Integer::from(*key) + } + + fn cache_time() -> Duration { + Duration::from_secs(10) + } +} + +fn decrypt(key: &EncryptionKey, encrypted: I) -> crate::Result +where + I: IntoIterator, + C: FromIterator, +{ + Ok(encrypted + .into_iter() + .map(|e| { + Ok(Wallet { + id: e.id, + pubkey: e.pubkey, + keypair: e + .encrypted_keypair + .map(|e| key.decrypt_keypair(&e)) + .transpose()? + .map(|k| k.to_bytes()), + }) + }) + .collect::>()?) +} + +#[async_trait] +impl UserConnectionTrait for UserConnection { + async fn get_wallet_by_pubkey(&self, pubkey: &[u8; 32]) -> crate::Result { + // TODO: caching + let w = self.get_encrypted_wallet_by_pubkey(pubkey).await?; + let key = self.pool.encryption_key()?; + Ok(decrypt::<_, Vec>(key, [w])?.pop().unwrap()) + } + + async fn get_deployment_id_from_tag( + &self, + entrypoint: &FlowId, + tag: &str, + ) -> crate::Result { + // TODO: caching + self.get_deployment_id_from_tag_impl(entrypoint, tag).await + } + + async fn get_deployment(&self, id: &DeploymentId) -> crate::Result { + // TODO: caching + self.get_deployment_impl(id).await + } + + async fn get_deployment_wallets(&self, id: &DeploymentId) -> crate::Result> { + // TODO: caching + self.get_deployment_wallets_impl(id).await + } + + async fn get_deployment_flows( + &self, + id: &DeploymentId, + ) -> crate::Result> { + // TODO: caching + self.get_deployment_flows_impl(id).await + } + + fn clone_connection(&self) -> Box { + Box::new(self.clone()) + } + + async fn insert_deployment(&self, d: &FlowDeployment) -> crate::Result { + self.insert_deployment_impl(d).await + } + + async fn get_flow(&self, id: FlowId) -> crate::Result { + if let Some(cached) = self.local.get_cache::(&self.user_id, &id) { + return Ok(cached); + } + let result = self.get_flow(id).await; + if let Ok(result) = &result { + if let Err(error) = self.local.set_cache::(&id, result.clone()) { + tracing::error!("set_cache error: {}", error); + } + } + result + } + + async fn download_storage_file(&self, path: &str) -> crate::Result { + // TODO: cache + self.download_storage_file(path).await + } + + async fn share_flow_run(&self, id: FlowRunId, user: UserId) -> crate::Result<()> { + self.share_flow_run(id, user).await + } + + async fn get_flow_info(&self, flow_id: FlowId) -> crate::Result { + if let Some(cached) = self + .local + .get_cache::(&self.user_id, &flow_id) + { + return Ok(cached); + } + let result = self.get_flow_info(flow_id).await; + if let Ok(result) = &result { + if let Err(error) = self + .local + .set_cache::(&flow_id, result.clone()) + { + tracing::error!("set_cache error: {}", error); + } + } + result + } + + async fn get_some_wallets(&self, ids: &[i64]) -> crate::Result> { + // TODO: caching + let key = self.pool.encryption_key()?.clone(); + let encrypted = self.get_some_wallets_impl(ids).await?; + Ok(spawn_blocking(move || decrypt(&key, encrypted)).await??) + } + + async fn get_wallets(&self) -> crate::Result> { + let key = self.pool.encryption_key()?.clone(); + let encrypted = self.get_encrypted_wallets().await?; + Ok(spawn_blocking(move || decrypt(&key, encrypted)).await??) + } + + async fn clone_flow(&mut self, flow_id: FlowId) -> crate::Result> { + self.clone_flow(flow_id).await + } + + async fn new_flow_run( + &self, + config: &ClientConfig, + inputs: &ValueSet, + ) -> crate::Result { + self.new_flow_run(config, inputs).await + } + + async fn get_previous_values( + &self, + nodes: &HashMap, + ) -> crate::Result>> { + self.get_previous_values(nodes).await + } + + async fn get_flow_config(&self, id: FlowId) -> crate::Result { + if let Some(cached) = self.local.get_cache::(&self.user_id, &id) { + return Ok(cached); + } + let result = self.get_flow_config(id).await; + if let Ok(result) = &result { + if let Err(error) = self.local.set_cache::(&id, result.clone()) { + tracing::error!("set_cache error: {}", error); + } + } + result + } + + async fn set_start_time(&self, id: &FlowRunId, time: &DateTime) -> crate::Result<()> { + self.set_start_time(id, time).await + } + + async fn push_flow_error(&self, id: &FlowRunId, error: &str) -> crate::Result<()> { + self.push_flow_error(id, error).await + } + + async fn set_run_result( + &self, + id: &FlowRunId, + time: &DateTime, + not_run: &[NodeId], + output: &Value, + ) -> crate::Result<()> { + self.set_run_result(id, time, not_run, output).await + } + + async fn new_node_run( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + input: &Value, + ) -> crate::Result<()> { + self.new_node_run(id, node_id, times, time, input).await + } + + async fn save_node_output( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + output: &Value, + ) -> crate::Result<()> { + self.save_node_output(id, node_id, times, output).await + } + + async fn push_node_error( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + error: &str, + ) -> crate::Result<()> { + self.push_node_error(id, node_id, times, error).await + } + + async fn set_node_finish( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + ) -> crate::Result<()> { + self.set_node_finish(id, node_id, times, time).await + } + + async fn new_signature_request( + &self, + pubkey: &[u8; 32], + message: &[u8], + flow_run_id: Option<&FlowRunId>, + signatures: Option<&[Presigner]>, + ) -> crate::Result { + self.new_signature_request(pubkey, message, flow_run_id, signatures) + .await + } + + async fn save_signature( + &self, + id: &i64, + signature: &[u8; 64], + new_message: Option<&Bytes>, + ) -> crate::Result<()> { + self.save_signature(id, signature, new_message).await + } + + async fn read_item(&self, store: &str, key: &str) -> crate::Result> { + self.read_item(store, key).await + } + + async fn export_user_data(&mut self) -> crate::Result { + self.export_user_data().await + } +} + +#[track_caller] +fn row_to_flow_row(r: tokio_postgres::Row) -> crate::Result { + Ok(FlowRow { + id: r.try_get("id").map_err(Error::data("flows.id"))?, + user_id: r.try_get("user_id").map_err(Error::data("flows.user_id"))?, + nodes: r + .try_get::<_, Vec>>("nodes") + .map_err(Error::data("flows.nodes"))? + .into_iter() + .map(|x| x.0) + .collect(), + edges: r + .try_get::<_, Vec>>("edges") + .map_err(Error::data("flows.edges"))? + .into_iter() + .map(|x| x.0) + .collect(), + environment: r + .try_get::<_, Json>>("environment") + .map_err(Error::data("flows.environment"))? + .0, + current_network: r + .try_get::<_, Json>("current_network") + .map_err(Error::data("flows.current_network"))? + .0, + instructions_bundling: r + .try_get::<_, Json>("instructions_bundling") + .map_err(Error::data("flows.instructions_bundling"))? + .0, + is_public: r + .try_get::<_, bool>("isPublic") + .map_err(Error::data("flows.isPublic"))?, + start_shared: r + .try_get::<_, bool>("start_shared") + .map_err(Error::data("flows.start_shared"))?, + start_unverified: r + .try_get::<_, bool>("start_unverified") + .map_err(Error::data("flows.start_unverified"))?, + }) +} + +impl UserConnection { + pub fn new( + pool: RealDbPool, + wasm_storage: WasmStorage, + user_id: Uuid, + local: LocalStorage, + ) -> Self { + Self { + pool, + user_id, + wasm_storage, + local, + } + } + + async fn get_encrypted_wallet_by_pubkey( + &self, + pubkey: &[u8; 32], + ) -> crate::Result { + let pubkey_str = bs58::encode(pubkey).into_string(); + let conn = self.pool.get_conn().await?; + parse_encrypted_wallet( + conn.do_query_one( + "select public_key, encrypted_keypair, id + from wallets where user_id = $1 and public_key = $2", + &[&self.user_id, &pubkey_str], + ) + .await + .map_err(Error::exec("select wallet"))?, + ) + } + + async fn insert_deployment_impl(&self, d: &FlowDeployment) -> crate::Result { + if self.user_id != d.user_id { + return Err(Error::Unauthorized); + } + let mut conn = self.pool.get_conn().await?; + let tx = conn.transaction().await.map_err(Error::exec("start"))?; + + let id = DeploymentId::now_v7(); + tx.do_execute( + "INSERT INTO flow_deployments + (id, user_id, entrypoint, start_permission, output_instructions, action_identity, action_config, fees) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + &[ + &id, + &d.user_id, + &d.entrypoint, + &Json(d.start_permission), + &d.output_instructions, + &d.action_identity.as_ref().map(|p| p.to_string()), + &d.action_config.as_ref().map(Json), + &d.fees.iter().map(Json).collect::>(), + ], + ) + .await + .map_err(Error::exec("insert flow_deployments"))?; + + let stmt = tx + .prepare_cached( + "COPY flow_deployments_wallets ( + user_id, + deployment_id, + wallet_id + ) FROM STDIN WITH (FORMAT binary)", + ) + .await + .map_err(Error::exec("prepare"))?; + let sink = tx + .copy_in::<_, Bytes>(&stmt) + .await + .map_err(Error::exec("copy in"))?; + let writer = BinaryCopyInWriter::new(sink, &[Type::UUID, Type::UUID, Type::INT8]); + futures_util::pin_mut!(writer); + for wallet_id in &d.wallets_id { + writer + .as_mut() + .write(&[&d.user_id, &id, &wallet_id]) + .await + .map_err(Error::exec("copy in write"))?; + } + let written = writer + .finish() + .await + .map_err(Error::exec("copy in finish"))?; + if written != d.wallets_id.len() as u64 { + return Err(Error::LogicError(anyhow!( + "size={}; written={}", + d.wallets_id.len(), + written + ))); + } + + let stmt = tx + .prepare_cached( + "COPY flow_deployments_flows ( + deployment_id, + flow_id, + user_id, + data + ) FROM STDIN WITH (FORMAT binary)", + ) + .await + .map_err(Error::exec("prepare"))?; + let sink = tx + .copy_in::<_, Bytes>(&stmt) + .await + .map_err(Error::exec("copy in"))?; + let writer = + BinaryCopyInWriter::new(sink, &[Type::UUID, Type::INT4, Type::UUID, Type::JSONB]); + futures_util::pin_mut!(writer); + for f in d.flows.values() { + let f = &f.row; + writer + .as_mut() + .write(&[&id, &f.id, &f.user_id, &Json(f.data())]) + .await + .map_err(Error::exec("copy in write"))?; + } + let written = writer + .finish() + .await + .map_err(Error::exec("copy in finish"))?; + if written != d.flows.len() as u64 { + return Err(Error::LogicError(anyhow!( + "size={}; written={}", + d.wallets_id.len(), + written + ))); + } + + tx.commit().await.map_err(Error::exec("commit"))?; + + Ok(id) + } + + async fn get_deployment_id_from_tag_impl( + &self, + entrypoint: &FlowId, + tag: &str, + ) -> crate::Result { + let conn = self.pool.get_conn().await?; + Ok(conn + .do_query_opt( + "select deployment_id flow_deployments_tags + where entrypoint = $1 and tag = $2", + &[entrypoint, &tag, &self.user_id], + ) + .await + .map_err(Error::exec("get_deployment_id_from_tag"))? + .ok_or_else(|| Error::not_found("deployment", format!("{}:{}", entrypoint, tag)))? + .try_get::<_, Uuid>(0) + .map_err(Error::data("flow_deployments_tags.deployment_id"))?) + } + + async fn get_deployment_impl(&self, id: &DeploymentId) -> crate::Result { + let conn = self.pool.get_conn().await?; + const QUERY: &str = // + r#"select + user_id, + entrypoint, + start_permission, + output_instructions, + action_identity, + action_config, + fees + from flow_deployments + where id = $1 and ( + (start_permission = '"Anonymous"') + or (start_permission = '"Authenticated"' and $2::uuid <> '00000000-0000-0000-0000-000000000000') + or (start_permission = '"Owner"' and $2::uuid = user_id) + )"#; + let r = conn + .do_query_opt(QUERY, &[id, &self.user_id]) + .await + .map_err(Error::exec("select flow_deployments"))? + .ok_or_else(|| Error::not_found("flow_deployments", id))?; + let d = FlowDeployment { + user_id: r + .try_get("user_id") + .map_err(Error::data("flow_deployments.entrypoint"))?, + entrypoint: r + .try_get("entrypoint") + .map_err(Error::data("flow_deployments.entrypoint"))?, + flows: Default::default(), + start_permission: r + .try_get::<_, Json<_>>("start_permission") + .map_err(Error::data("flow_deployments.start_permission"))? + .0, + wallets_id: Default::default(), + output_instructions: r + .try_get("output_instructions") + .map_err(Error::data("flow_deployments.output_instructions"))?, + action_identity: r + .try_get::<_, Option<&str>>("action_identity") + .map_err(Error::data("flow_deployments.action_identity"))? + .map(|s| { + s.parse::() + .map_err(Error::parsing("flow_deployments.action_identity")) + }) + .transpose()?, + action_config: r + .try_get::<_, Option>>("action_config") + .map_err(Error::data("flow_deployments.action_config"))? + .map(|Json(x)| x), + fees: r + .try_get::<_, Option>>>("fees") + .map_err(Error::data("flow_deployments.fees"))? + .map(|v| { + v.into_iter() + .map(|Json((pubkey, amount))| { + Pubkey::from_str(&pubkey).map(|pk| (pk, amount)) + }) + .collect::, _>>() + }) + .transpose() + .map_err(Error::parsing("flow_deployments.fees"))? + .unwrap_or_default(), + }; + Ok(d) + } + + async fn get_deployment_wallets_impl(&self, id: &DeploymentId) -> crate::Result> { + let conn = self.pool.get_conn().await?; + let ids = conn + .do_query( + "SELECT wallet_id FROM flow_deployments_wallets + WHERE deployment_id = $1 AND user_id = $2", + &[id, &self.user_id], + ) + .await + .map_err(Error::exec("select flow_deployments_wallets"))? + .into_iter() + .map(|r| r.try_get(0)) + .collect::, _>>() + .map_err(Error::data("flow_deployments_wallets.wallet_id"))?; + Ok(ids) + } + + async fn get_deployment_flows_impl( + &self, + id: &DeploymentId, + ) -> crate::Result> { + fn parse(r: Row) -> crate::Result<(FlowId, Flow)> { + let id = r + .try_get("flow_id") + .map_err(Error::data("flow_deployments_flows.flow_id"))?; + let Json(flow) = r + .try_get::<_, Json>("data") + .map_err(Error::data("flow_deployments_flows.data"))?; + Ok((id, Flow { row: flow })) + } + + let conn = self.pool.get_conn().await?; + let flows = conn + .do_query( + "SELECT flow_id, data FROM flow_deployments_flows + WHERE deployment_id = $1 AND user_id = $2", + &[id, &self.user_id], + ) + .await + .map_err(Error::exec("select flow_deployments_flows"))? + .into_iter() + .map(parse) + .collect::, _>>()?; + Ok(flows) + } + + async fn get_flow(&self, id: FlowId) -> crate::Result { + let conn = self.pool.get_conn().await?; + let flow = conn + .do_query_opt( + r#"SELECT id, + user_id, + nodes, + edges, + environment, + current_network, + instructions_bundling, + "isPublic", + start_shared, + start_unverified + FROM flows + WHERE id = $1 AND user_id = $2"#, + &[&id, &self.user_id], + ) + .await + .map_err(Error::exec("get_flow_config"))? + .ok_or_else(|| Error::not_found("flow", id)) + .and_then(row_to_flow_row)?; + + Ok(flow) + } + + async fn download_storage_file(&self, path: &str) -> crate::Result { + Ok(self.wasm_storage.download(path).await?) + } + + async fn get_encrypted_wallets(&self) -> crate::Result> { + if let Some(cached) = self + .local + .get_cache::(&self.user_id, &self.user_id) + { + return Ok(cached); + } + let result = self.get_encrypted_wallets_query().await; + if let Ok(result) = &result { + if let Err(error) = self + .local + .set_cache::(&self.user_id, result.clone()) + { + tracing::error!("set_cache error: {}", error); + } + } + result + } + + async fn share_flow_run(&self, id: FlowRunId, user: UserId) -> crate::Result<()> { + // Same user, not doing anything + if user == self.user_id { + return Ok(()); + } + + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "SELECT 1 FROM flow_run WHERE id = $1 AND user_id = $2", + &[&id, &self.user_id], + ) + .await + .map_err(Error::exec("check conn permission"))?; + + conn.do_execute( + "INSERT INTO flow_run_shared (flow_run_id, user_id) + VALUES ($1, $2) + ON CONFLICT (flow_run_id, user_id) DO NOTHING", + &[&id, &user], + ) + .await + .map_err(Error::exec("insert flow_run_shared"))?; + + Ok(()) + } + + async fn get_flow_info(&self, flow_id: FlowId) -> crate::Result { + let conn = self.pool.get_conn().await?; + conn.do_query_opt( + r#"SELECT user_id, start_shared, start_unverified, "isPublic" FROM flows + WHERE id = $1 AND (user_id = $2 OR "isPublic" = TRUE)"#, + &[&flow_id, &self.user_id], + ) + .await + .map_err(Error::exec("get_flow_info"))? + .ok_or_else(|| Error::not_found("flow", flow_id))? + .try_into() + } + + async fn get_some_wallets_impl(&self, ids: &[i64]) -> crate::Result> { + let conn = self.pool.get_conn().await?; + let result = conn + .do_query( + "select public_key, encrypted_keypair, id from wallets + where id = any($1::bigint[]) and user_id = $2", + &[&ids, &self.user_id], + ) + .await + .map_err(Error::exec("select wallets"))? + .into_iter() + .map(parse_encrypted_wallet) + .collect::, _>>()?; + + Ok(result) + } + + async fn get_encrypted_wallets_query(&self) -> crate::Result> { + let conn = self.pool.get_conn().await?; + let result = conn + .do_query( + "SELECT public_key, encrypted_keypair, id FROM wallets WHERE user_id = $1", + &[&self.user_id], + ) + .await + .map_err(Error::exec("get wallets"))? + .into_iter() + .map(parse_encrypted_wallet) + .collect::>>()?; + + Ok(result) + } + + async fn clone_flow(&mut self, flow_id: FlowId) -> crate::Result> { + let mut conn = self.pool.get_conn().await?; + let tx = conn.transaction().await.map_err(Error::exec("start"))?; + + let flow_owner = { + let owner: UserId = tx + .do_query_one( + r#"SELECT user_id FROM flows + WHERE id = $1 AND (user_id = $2 OR "isPublic")"#, + &[&flow_id, &self.user_id], + ) + .await + .map_err(Error::exec("get flow's owner"))? + .try_get(0) + .map_err(Error::data("flows.user_id"))?; + owner + }; + + let get_wallets = "SELECT id, public_key FROM wallets WHERE user_id = $1"; + let owner_wallets = tx + .query(get_wallets, &[&flow_owner]) + .await + .map_err(Error::exec("get_wallets"))? + .into_iter() + .map(|r| { + Ok::<_, Error>(( + r.try_get::<_, i64>(0).map_err(Error::data("wallets.id"))?, + r.try_get::<_, String>(1) + .map_err(Error::data("wallets.public_key"))?, + )) + }) + .collect::, _>>()?; + let is_same_user = self.user_id == flow_owner; + let user_wallet = if is_same_user { + owner_wallets.clone() + } else { + tx.do_query(get_wallets, &[&self.user_id]) + .await + .map_err(Error::exec("get_wallets"))? + .into_iter() + .map(|r| { + Ok::<_, Error>(( + r.try_get::<_, i64>(0).map_err(Error::data("wallets.id"))?, + r.try_get::<_, String>(1) + .map_err(Error::data("wallets.public_key"))?, + )) + }) + .collect::, _>>()? + }; + if user_wallet.is_empty() { + return Err(Error::LogicError(anyhow::anyhow!("user has no wallets"))); + } + + let wallet_map = { + let mut res = HashMap::with_capacity(owner_wallets.len()); + for wallet in &owner_wallets { + let (id, owner_pk) = wallet; + let value = is_same_user + .then_some(wallet) + .or_else(|| user_wallet.iter().find(|(_, pk)| pk == owner_pk)); + if let Some(value) = value { + res.insert(id, value); + } + } + res + }; + let default_wallet_id = user_wallet[0].0; + let default_wallet_pubkey = user_wallet[0].1.as_str(); + + let mut ids = HashSet::::new(); + let mut queue = vec![flow_id]; + let get_interflows = r#"WITH nodes AS + ( + SELECT unnest(nodes) AS node + FROM flows WHERE id = $1 + ) + SELECT CAST(node #>> '{data,targets_form,form_data,id}' AS INT) AS id + FROM nodes WHERE + node #>> '{data,node_id}' IN ('interflow', 'interflow_instructions') + AND node->>'type' = 'native'"#; + let check_flow = r#"SELECT id FROM flows WHERE id = $1 AND (user_id = $2 OR "isPublic")"#; + while let Some(id) = queue.pop() { + if tx + .do_query_opt(check_flow, &[&id, &self.user_id]) + .await + .map_err(Error::exec("check flow"))? + .is_some() + { + ids.insert(id); + } else { + return Err(Error::LogicError(anyhow::anyhow!( + "flow {:?} not found or not public", + id + ))); + } + + let rows = tx + .do_query(get_interflows, &[&id]) + .await + .map_err(Error::exec("get interflows"))?; + for row in rows { + let id: i32 = row + .try_get(0) + .map_err(Error::data("data.targets_form.form_data.id"))?; + if !ids.contains(&id) { + queue.push(id); + } + } + } + let ids: Vec = ids.into_iter().collect(); + + let copy_flow = r#"INSERT INTO flows ( + guide, + name, + mosaic, + description, + tags, + custom_networks, + current_network, + instructions_bundling, + environment, + nodes, + edges, + user_id, + parent_flow + ) SELECT + guide, + name, + mosaic, + description, + tags, + custom_networks, + current_network, + instructions_bundling, + environment, + nodes, + edges, + $2 AS user_id, + id as parent_flow + FROM flows WHERE id = $1 + RETURNING id"#; + let mut flow_id_map = HashMap::new(); + let mut new_ids = Vec::new(); + for id in &ids { + let new_id: i32 = tx + .do_query_one(copy_flow, &[id, &self.user_id]) + .await + .map_err(Error::exec("copy flow"))? + .try_get(0) + .map_err(Error::data("flows.id"))?; + flow_id_map.insert(*id, new_id); + new_ids.push(new_id); + } + let update_flow = + "UPDATE flows SET nodes = q.nodes FROM ( + SELECT + f.id, + ARRAY_AGG( + CASE + WHEN + node #>> '{data,node_id}' IN ('interflow', 'interflow_instructions') + AND node->>'type' = 'native' + THEN jsonb_set( + node, + '{data,targets_form,form_data,id}', + $2::JSONB->(node #>> '{data,targets_form,form_data,id}') + ) + + WHEN + node #>> '{data,node_id}' IN ('wallet') + AND node->>'type' = 'native' + THEN jsonb_set( + jsonb_set( + node, + '{data,targets_form,form_data,public_key}', + COALESCE($3::JSONB->(node #>> '{data,targets_form,form_data,wallet_id}')->1, $5::JSONB) + ), + '{data,targets_form,form_data,wallet_id}', + COALESCE($3::JSONB->(node #>> '{data,targets_form,form_data,wallet_id}')->0, $4::JSONB) + ) + + ELSE node + END + + ORDER BY idx + ) AS nodes + FROM flows f CROSS JOIN unnest(f.nodes) WITH ORDINALITY AS n(node, idx) + WHERE f.id = ANY($1::INT[]) + GROUP BY f.id + ) AS q + WHERE flows.id = q.id"; + tx.do_execute( + update_flow, + &[ + &new_ids, + &Json(&flow_id_map), + &Json(&wallet_map), + &Json(default_wallet_id), + &Json(default_wallet_pubkey), + ], + ) + .await + .map_err(Error::exec("update interflow IDs"))?; + tx.commit() + .await + .map_err(Error::exec("commit clone_flow"))?; + + Ok(flow_id_map) + } + + async fn new_flow_run( + &self, + config: &ClientConfig, + inputs: &ValueSet, + ) -> crate::Result { + let conn = self.pool.get_conn().await?; + let r = conn + .do_query_one( + "INSERT INTO flow_run ( + id, + user_id, + flow_id, + inputs, + environment, + instructions_bundling, + network, + call_depth, + origin, + nodes, + edges, + collect_instructions, + partial_config, + signers) + VALUES ( + gen_random_uuid(), + $1, $2, + jsonb_build_object('M', $3::JSONB), + $4, $5, + jsonb_build_object('SOL', $6::JSONB), + $7, $8, $9, $10, $11, $12, $13) + RETURNING id", + &[ + &self.user_id, + &config.id, + &Json(&inputs), + &Json(&config.environment), + &Json(&config.instructions_bundling), + &Json(&config.sol_network), + &(config.call_depth as i32), + &Json(&config.origin), + &config + .nodes + .iter() + .map(|n| { + Json(serde_json::json!({ + "id": n.id, + "data": NodeDataSkipWasm::from(n.data.clone()), + })) + }) + .collect::>(), + &config.edges.iter().map(Json).collect::>(), + &config.collect_instructions, + &config.partial_config.as_ref().map(Json), + &Json(&config.signers), + ], + ) + .await + .map_err(Error::exec("new flow run"))?; + Ok(r.get(0)) + } + + async fn get_previous_values( + &self, + nodes: &HashMap, + ) -> crate::Result>> { + struct FormatArg<'a>(&'a HashMap); + impl std::fmt::Display for FormatArg<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + f.write_str("(")?; + for (k, v) in self.0 { + if first { + first = false; + } else { + f.write_str(",")? + } + f.write_str("('")?; + k.fmt(f)?; + f.write_str("','")?; + v.fmt(f)?; + f.write_str("')")?; + } + f.write_str(")")?; + Ok(()) + } + } + let stmt = format!( + "SELECT + node_id, + ARRAY_AGG(output ORDER BY times ASC) + FROM node_run + WHERE + (node_id, flow_run_id) IN {} + AND user_id = $1 + AND output IS NOT NULL + GROUP BY node_id", + FormatArg(nodes) + ); + let conn = self.pool.get_conn().await?; + conn.query(&stmt, &[&self.user_id]) + .await + .map_err(Error::exec("select node_run"))? + .into_iter() + .map(|row| { + let node_id: Uuid = row.try_get(0).map_err(Error::data("flow_run.node_id"))?; + let outputs: Vec = + row.try_get(1).map_err(Error::data("flow_run.output"))?; + let outputs: Vec = outputs + .into_iter() + .map(serde_json::from_value) + .collect::, _>>() + .map_err(Error::json("flow_run.output"))?; + Ok((node_id, outputs)) + }) + .collect::>, Error>>() + } + + async fn get_flow_config(&self, id: FlowId) -> crate::Result { + let conn = self.pool.get_conn().await?; + let row = conn + .do_query_opt( + "SELECT nodes, + edges, + environment, + (current_network->>'url')::TEXT AS network_url, + (current_network->>'cluster')::TEXT AS network_cluster, + instructions_bundling + FROM flows + WHERE id = $1 AND user_id = $2", + &[&id, &self.user_id], + ) + .await + .map_err(Error::exec("get_flow_config"))? + .ok_or_else(|| Error::not_found("flow", id))?; + + let nodes = row + .try_get::<_, Vec>(0) + .map_err(Error::data("flows.nodes"))?; + + let edges = row + .try_get::<_, Vec>(1) + .map_err(Error::data("flows.edges"))?; + + let environment = row + .try_get::<_, Json>>(2) + .unwrap_or_else(|_| Json(HashMap::new())) + .0; + + let network_url = row + .try_get::<_, &str>(3) + .map_err(Error::data("network_url"))?; + + let cluster = row + .try_get::<_, &str>(4) + .map_err(Error::data("network_cluster"))?; + + let instructions_bundling = row + .try_get::<_, Json>(5) + .map_err(Error::data("flows.instructions_bundling"))? + .0; + + let config = serde_json::json!({ + "user_id": self.user_id, + "id": id, + "nodes": nodes, + "edges": edges, + "sol_network": { + "url": network_url, + "cluster": cluster, + }, + "environment": environment, + "instructions_bundling": instructions_bundling, + }); + + let mut config = + serde_json::from_value::(config).map_err(Error::Deserialize)?; + + for node in &mut config.nodes { + if node.data.r#type == CommandType::Wasm { + if let Err(error) = self + .fetch_wasm_bytes(&mut node.data.targets_form, &conn) + .await + { + tracing::warn!("{}", error); + } + } + } + + Ok(config) + } + + async fn fetch_wasm_bytes( + &self, + data: &mut client::TargetsForm, + conn: &Connection, + ) -> crate::Result<()> { + if data.wasm_bytes.is_some() { + return Ok(()); + } + + let id = data + .extra + .supabase_id + .ok_or_else(|| Error::not_found("json", "supabase_id"))?; + + let path: String = conn + .do_query_opt( + r#"SELECT storage_path FROM nodes + WHERE id = $1 AND (user_id = $2 OR "isPublic" = TRUE)"#, + &[&id, &self.user_id], + ) + .await + .map_err(Error::exec("get storage_path"))? + .ok_or_else(|| Error::not_found("node", id))? + .try_get(0) + .map_err(Error::data("nodes.storage_path"))?; + + let bytes = self.wasm_storage.download(&path).await?; + + data.wasm_bytes = Some(bytes); + + Ok(()) + } + + async fn set_start_time(&self, id: &FlowRunId, time: &DateTime) -> crate::Result<()> { + let time = time.naive_utc(); + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE flow_run SET start_time = $1 WHERE id = $2 RETURNING id", + &[&time, id], + ) + .await + .map_err(Error::exec("set start time"))?; + Ok(()) + } + + async fn push_flow_error(&self, id: &FlowRunId, error: &str) -> crate::Result<()> { + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE flow_run + SET errors = array_append(errors, $2) + WHERE id = $1 + RETURNING id", + &[id, &error], + ) + .await + .map_err(Error::exec("push flow errors"))?; + Ok(()) + } + + async fn set_run_result( + &self, + id: &FlowRunId, + time: &DateTime, + not_run: &[NodeId], + output: &Value, + ) -> crate::Result<()> { + let time = time.naive_utc(); + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE flow_run + SET end_time = $2, + not_run = $3, + output = $4 + WHERE id = $1 AND end_time IS NULL + RETURNING id", + &[id, &time, ¬_run, &Json(output)], + ) + .await + .map_err(Error::exec("set run result"))?; + Ok(()) + } + + async fn new_node_run( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + input: &Value, + ) -> crate::Result<()> { + let time = time.naive_utc(); + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "INSERT INTO node_run + (flow_run_id, node_id, times, user_id, start_time, input) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING flow_run_id", + &[id, node_id, times, &self.user_id, &time, &Json(input)], + ) + .await + .map_err(Error::exec("new node run"))?; + Ok(()) + } + + async fn save_node_output( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + output: &Value, + ) -> crate::Result<()> { + const MAP: &str = value::keys::MAP; + let stmt = format!( + r#"UPDATE node_run + SET output = COALESCE( + jsonb_set( + output, + '{{{MAP}}}', + (output->'{MAP}') || ($4::JSONB->'{MAP}') + ), + $4::JSONB + ) + WHERE flow_run_id = $1 AND node_id = $2 AND times = $3 + RETURNING flow_run_id"# + ); + let conn = self.pool.get_conn().await?; + conn.do_query_one(&stmt, &[id, node_id, times, &Json(output)]) + .await + .map_err(Error::exec("set node finish"))?; + Ok(()) + } + + async fn push_node_error( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + error: &str, + ) -> crate::Result<()> { + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE node_run + SET errors = array_append(errors, $4) + WHERE flow_run_id = $1 AND node_id = $2 AND times = $3 + RETURNING flow_run_id", + &[id, node_id, times, &error], + ) + .await + .map_err(Error::exec("push node error"))?; + Ok(()) + } + + async fn set_node_finish( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + ) -> crate::Result<()> { + let time = time.naive_utc(); + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE node_run + SET end_time = $4 + WHERE flow_run_id = $1 AND node_id = $2 AND times = $3 + AND end_time IS NULL + RETURNING flow_run_id", + &[id, node_id, times, &time], + ) + .await + .map_err(Error::exec("set node finish"))?; + Ok(()) + } + + async fn new_signature_request( + &self, + pubkey: &[u8; 32], + message: &[u8], + flow_run_id: Option<&FlowRunId>, + signatures: Option<&[Presigner]>, + ) -> crate::Result { + let pubkey = bs58::encode(pubkey).into_string(); + let message = base64::encode(message); + let signatures = signatures.map(|arr| arr.iter().map(Json).collect::>()); + let conn = self.pool.get_conn().await?; + let id = conn + .do_query_one( + "INSERT INTO signature_requests ( + user_id, + msg, + pubkey, + flow_run_id, + signatures + ) VALUES ($1, $2, $3, $4, $5) RETURNING id", + &[&self.user_id, &message, &pubkey, &flow_run_id, &signatures], + ) + .await + .map_err(Error::exec("new_signature_request"))? + .try_get(0) + .map_err(Error::data("id"))?; + + Ok(id) + } + + async fn save_signature( + &self, + id: &i64, + signature: &[u8; 64], + new_message: Option<&Bytes>, + ) -> crate::Result<()> { + let new_msg_base64 = new_message.map(base64::encode); + let signature = bs58::encode(signature).into_string(); + let conn = self.pool.get_conn().await?; + conn.do_query_one( + "UPDATE signature_requests + SET signature = $1, + new_msg = $4 + WHERE user_id = $2 AND id = $3 AND signature IS NULL + RETURNING id", + &[&signature, &self.user_id, id, &new_msg_base64], + ) + .await + .map_err(Error::exec("save_signature"))?; + + Ok(()) + } + + async fn read_item(&self, store: &str, key: &str) -> crate::Result> { + let conn = self.pool.get_conn().await?; + let opt = conn + .do_query_opt( + "SELECT value FROM kvstore + WHERE user_id = $1 AND store_name = $2 AND key = $3", + &[&self.user_id, &store, &key], + ) + .await + .map_err(Error::exec("read item kvstore"))?; + match opt { + Some(row) => Ok(Some( + row.try_get::<_, Json>(0) + .map_err(Error::data("kvstore.value"))? + .0, + )), + None => Ok(None), + } + } + + async fn export_user_data(&mut self) -> crate::Result { + let mut conn = self.pool.get_conn().await?; + let tx = conn.transaction().await.map_err(Error::exec("start"))?; + + let pubkey = tx + .do_query_one( + "SELECT pub_key FROM users_public WHERE user_id = $1", + &[&self.user_id], + ) + .await + .map_err(Error::exec("get pub_key"))? + .try_get::<_, String>(0) + .map_err(Error::data("users_public.pub_key"))?; + bs58_decode::<32>(&pubkey).map_err(Error::parsing("base58"))?; + + let users = copy_out( + &tx, + &format!("SELECT * FROM auth.users WHERE id = '{}'", self.user_id), + ) + .await?; + let users = csv_export::clear_column(users, "encrypted_password")?; + let users = csv_export::remove_column(users, "confirmed_at")?; + + let nodes = copy_out( + &tx, + &format!( + r#"SELECT * FROM nodes WHERE + user_id = '{}' + OR (user_id IS NULL AND "isPublic")"#, + self.user_id + ), + ) + .await?; + + let identities = copy_out( + &tx, + &format!( + "SELECT * FROM auth.identities WHERE user_id = '{}'", + self.user_id + ), + ) + .await?; + let identities = csv_export::remove_column(identities, "email")?; + + let pubkey_whitelists = copy_out( + &tx, + &format!( + "SELECT * FROM pubkey_whitelists WHERE pubkey = '{}'", + pubkey + ), + ) + .await?; + + let users_public = copy_out( + &tx, + &format!( + "SELECT * FROM users_public WHERE user_id = '{}'", + self.user_id + ), + ) + .await?; + + let wallets = copy_out( + &tx, + &format!("SELECT * FROM wallets WHERE user_id = '{}'", self.user_id), + ) + .await?; + + let apikeys = copy_out( + &tx, + &format!("SELECT * FROM apikeys WHERE user_id = '{}'", self.user_id), + ) + .await?; + + let flows = copy_out( + &tx, + &format!("SELECT * FROM flows WHERE user_id = '{}'", self.user_id), + ) + .await?; + let flows = csv_export::clear_column(flows, "lastest_flow_run_id")?; + + let user_quotas = copy_out( + &tx, + &format!( + "SELECT * FROM user_quotas WHERE user_id = '{}'", + self.user_id + ), + ) + .await?; + + let kvstore = copy_out( + &tx, + &format!("SELECT * FROM kvstore WHERE user_id = '{}'", self.user_id), + ) + .await?; + + let kvstore_metadata = copy_out( + &tx, + &format!( + "SELECT * FROM kvstore_metadata WHERE user_id = '{}'", + self.user_id + ), + ) + .await?; + + tx.commit().await.map_err(Error::exec("commit"))?; + Ok(ExportedUserData { + user_id: self.user_id, + users, + identities, + pubkey_whitelists, + users_public, + wallets, + user_quotas, + kvstore, + kvstore_metadata, + apikeys, + flows, + nodes, + }) + } +} + +fn parse_encrypted_wallet(r: Row) -> Result { + let pubkey_str = r + .try_get::<_, String>(0) + .map_err(Error::data("wallets.public_key"))?; + let pubkey = bs58_decode(&pubkey_str).map_err(Error::parsing("wallets.public_key"))?; + + let encrypted_keypair = r + .try_get::<_, Option>>(1) + .map_err(Error::data("wallets.encrypted_keypair"))? + .map(|json| json.0); + + let id = r.try_get(2).map_err(Error::data("wallets.id"))?; + + Ok(EncryptedWallet { + id, + pubkey, + encrypted_keypair, + }) +} + +async fn copy_out(tx: &Transaction<'_>, query: &str) -> crate::Result { + let query = format!( + r#"COPY ({}) TO stdout WITH (FORMAT csv, DELIMITER ';', QUOTE '''', HEADER)"#, + query + ); + let stream = tx.copy_out(&query).await.map_err(Error::exec("copy-out"))?; + futures_util::pin_mut!(stream); + + let mut buffer = BytesMut::new(); + while let Some(result) = stream.next().await { + match result { + Ok(data) => { + // tracing::debug!("read {} bytes", data.len()); + buffer.extend_from_slice(&data[..]); + } + Err(error) => { + // tracing::debug!("{}", String::from_utf8_lossy(&buffer)); + return Err(Error::exec("read copy-out stream")(error)); + } + } + } + String::from_utf8(buffer.into()).map_err(Error::parsing("UTF8")) +} + +#[cfg(test)] +mod tests { + use crate::{config::DbConfig, pool::RealDbPool, LocalStorage, WasmStorage}; + use flow_lib::UserId; + use serde::Deserialize; + use toml::value::Table; + + #[tokio::test] + #[ignore] + async fn test_export() { + let user_id = std::env::var("USER_ID").unwrap().parse::().unwrap(); + let full_config: Table = toml::from_str( + &std::fs::read_to_string(std::env::var("CONFIG_FILE").unwrap()).unwrap(), + ) + .unwrap(); + let db_config = DbConfig::deserialize(full_config["db"].clone()).unwrap(); + let wasm = WasmStorage::new("http://localhost".parse().unwrap(), "", "").unwrap(); + let temp = tempfile::tempdir().unwrap(); + let local = LocalStorage::new(temp.path()).unwrap(); + let pool = RealDbPool::new(&db_config, wasm, local).await.unwrap(); + let mut conn = pool.get_user_conn(user_id).await.unwrap(); + let result = conn.export_user_data().await.unwrap(); + std::fs::write("/tmp/data.json", serde_json::to_vec(&result).unwrap()).unwrap(); + } +} diff --git a/crates/db/src/connection/csv_export.rs b/crates/db/src/connection/csv_export.rs new file mode 100644 index 00000000..a28a605f --- /dev/null +++ b/crates/db/src/connection/csv_export.rs @@ -0,0 +1,93 @@ +use crate::Error; + +pub fn reader() -> csv::ReaderBuilder { + let mut r = csv::ReaderBuilder::new(); + r.delimiter(b';').quote(b'\''); + r +} + +pub fn writer() -> csv::WriterBuilder { + let mut w = csv::WriterBuilder::new(); + w.delimiter(b';').quote(b'\''); + w +} + +pub fn clear_column(data: String, column: &str) -> crate::Result { + let mut reader = reader().from_reader(data.as_bytes()); + let headers = reader + .headers() + .map_err(Error::parsing("csv headers"))? + .clone(); + let col_idx = headers + .iter() + .position(|col| col == column) + .ok_or_else(|| Error::not_found("column", column))?; + let records = reader + .records() + .map(|r| { + r.map_err(Error::parsing("csv iter")).map(|r| { + r.into_iter() + .enumerate() + .map(|(i, v)| if i == col_idx { "" } else { v }) + .collect::() + }) + }) + .collect::, _>>()?; + let mut buffer = Vec::new(); + let mut writer = writer().from_writer(&mut buffer); + writer + .write_record(&headers) + .map_err(Error::parsing("build csv"))?; + for r in records { + writer + .write_record(&r) + .map_err(Error::parsing("build csv"))?; + } + writer.flush().map_err(Error::parsing("build csv"))?; + drop(writer); + String::from_utf8(buffer).map_err(Error::parsing("UTF8")) +} + +pub fn remove_column(data: String, column: &str) -> crate::Result { + let mut reader = reader().from_reader(data.as_bytes()); + let headers = reader + .headers() + .map_err(Error::parsing("csv headers"))? + .clone(); + let col_idx = headers + .iter() + .position(|col| col == column) + .ok_or_else(|| Error::not_found("column", column))?; + let records = reader + .records() + .map(|r| { + r.map_err(Error::parsing("csv iter")).map(|r| { + r.into_iter() + .enumerate() + .filter_map(|(i, v)| (i != col_idx).then_some(v)) + .collect::() + }) + }) + .collect::, _>>()?; + + let mut buffer = Vec::new(); + let mut writer = writer().from_writer(&mut buffer); + writer + .write_record( + &headers + .into_iter() + .enumerate() + .filter_map(|(i, v)| (i != col_idx).then_some(v)) + .collect::(), + ) + .map_err(Error::parsing("build csv"))?; + for r in records { + writer + .write_record(&r) + .map_err(Error::parsing("build csv"))?; + } + writer.flush().map_err(Error::parsing("build csv"))?; + drop(writer); + + String::from_utf8(buffer).map_err(Error::parsing("UTF8")) +} diff --git a/crates/db/src/connection/mod.rs b/crates/db/src/connection/mod.rs new file mode 100644 index 00000000..d5c48824 --- /dev/null +++ b/crates/db/src/connection/mod.rs @@ -0,0 +1,318 @@ +use crate::{pool::RealDbPool, Error, LocalStorage, Wallet, WasmStorage}; +use async_trait::async_trait; +use bytes::Bytes; +use chrono::{DateTime, Utc}; +use deadpool_postgres::{Object as Connection, Transaction}; +use flow::flow_set::{get_flow_row, DeploymentId, Flow, FlowDeployment}; +use flow_lib::{ + config::client::{self, ClientConfig, FlowRow}, + context::signer::Presigner, + utils::BoxFuture, + CommandType, FlowId, FlowRunId, NodeId, UserId, ValueSet, +}; +use hashbrown::{HashMap, HashSet}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::{any::Any, future::Future, time::Duration}; +use tokio_postgres::{ + types::{Json, ToSql}, + Error as PgError, Row, +}; +use uuid::Uuid; +use value::Value; + +pub mod csv_export; + +mod admin; +pub use admin::*; + +pub mod proxied_user_conn; + +#[derive(Clone)] +pub struct UserConnection { + pub local: LocalStorage, + pub wasm_storage: WasmStorage, + pub pool: RealDbPool, + pub user_id: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ExportedUserData { + pub user_id: UserId, + pub users: String, + pub identities: String, + pub pubkey_whitelists: String, + pub users_public: String, + pub wallets: String, + pub apikeys: String, + pub user_quotas: String, + pub kvstore: String, + pub kvstore_metadata: String, + pub flows: String, + pub nodes: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct FlowInfo { + pub user_id: Uuid, + pub is_public: bool, + pub start_shared: bool, + pub start_unverified: bool, +} + +impl TryFrom for FlowInfo { + type Error = crate::Error; + fn try_from(r: Row) -> Result { + Ok(Self { + user_id: r.try_get("user_id").map_err(Error::data("flow.user_id"))?, + is_public: r + .try_get("isPublic") + .map_err(Error::data("flow.isPublic"))?, + start_shared: r + .try_get("start_shared") + .map_err(Error::data("flow.start_shared"))?, + start_unverified: r + .try_get("start_unverified") + .map_err(Error::data("flow.start_unverified"))?, + }) + } +} + +impl tower::Service for Box { + type Response = get_flow_row::Response; + type Error = get_flow_row::Error; + type Future = BoxFuture<'static, Result>; + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + fn call(&mut self, req: get_flow_row::Request) -> Self::Future { + let this = self.clone_connection(); + Box::pin(async move { + let result = this.get_flow(req.flow_id).await; + match result { + Ok(row) => Ok(get_flow_row::Response { row }), + Err(error) => Err(match error { + Error::Unauthorized => get_flow_row::Error::Unauthorized, + Error::ResourceNotFound { .. } => get_flow_row::Error::NotFound, + error => get_flow_row::Error::Other(error.into()), + }), + } + }) + } +} + +#[async_trait] +pub trait UserConnectionTrait: Any + Send + 'static { + async fn get_wallet_by_pubkey(&self, pubkey: &[u8; 32]) -> crate::Result; + + async fn get_deployment_id_from_tag( + &self, + entrypoint: &FlowId, + tag: &str, + ) -> crate::Result; + + async fn get_deployment(&self, id: &DeploymentId) -> crate::Result; + + async fn get_deployment_wallets(&self, id: &DeploymentId) -> crate::Result>; + + async fn get_deployment_flows(&self, id: &DeploymentId) + -> crate::Result>; + + async fn insert_deployment(&self, d: &FlowDeployment) -> crate::Result; + + fn clone_connection(&self) -> Box; + + async fn get_flow(&self, id: FlowId) -> crate::Result; + + async fn download_storage_file(&self, path: &str) -> crate::Result; + + async fn share_flow_run(&self, id: FlowRunId, user: UserId) -> crate::Result<()>; + + async fn get_flow_info(&self, flow_id: FlowId) -> crate::Result; + + async fn clone_flow(&mut self, flow_id: FlowId) -> crate::Result>; + + async fn get_some_wallets(&self, ids: &[i64]) -> crate::Result>; + + async fn get_wallets(&self) -> crate::Result>; + + async fn new_flow_run( + &self, + config: &ClientConfig, + inputs: &ValueSet, + ) -> crate::Result; + + async fn get_previous_values( + &self, + nodes: &HashMap, + ) -> crate::Result>>; + + async fn get_flow_config(&self, id: FlowId) -> crate::Result; + + async fn set_start_time(&self, id: &FlowRunId, time: &DateTime) -> crate::Result<()>; + + async fn push_flow_error(&self, id: &FlowRunId, error: &str) -> crate::Result<()>; + + async fn set_run_result( + &self, + id: &FlowRunId, + time: &DateTime, + not_run: &[NodeId], + output: &Value, + ) -> crate::Result<()>; + + async fn new_node_run( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + input: &Value, + ) -> crate::Result<()>; + + async fn save_node_output( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + output: &Value, + ) -> crate::Result<()>; + + async fn push_node_error( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + error: &str, + ) -> crate::Result<()>; + + async fn set_node_finish( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + ) -> crate::Result<()>; + + async fn new_signature_request( + &self, + pubkey: &[u8; 32], + message: &[u8], + flow_run_id: Option<&FlowRunId>, + signatures: Option<&[Presigner]>, + ) -> crate::Result; + + async fn save_signature( + &self, + id: &i64, + signature: &[u8; 64], + new_msg: Option<&Bytes>, + ) -> crate::Result<()>; + + async fn read_item(&self, store: &str, key: &str) -> crate::Result>; + + async fn export_user_data(&mut self) -> crate::Result; +} + +pub trait DbClient { + #[track_caller] + fn do_query_one( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> impl Future>; + + #[track_caller] + fn do_query( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> impl Future, PgError>>; + + #[track_caller] + fn do_execute( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> impl Future>; + + #[track_caller] + fn do_query_opt( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> impl Future, PgError>>; +} + +impl DbClient for Connection { + async fn do_query_one( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result { + let stmt = self.prepare_cached(stmt).await?; + self.query_one(&stmt, params).await + } + + async fn do_query( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result, PgError> { + let stmt = self.prepare_cached(stmt).await?; + self.query(&stmt, params).await + } + + async fn do_execute(&self, stmt: &str, params: &[&(dyn ToSql + Sync)]) -> Result { + let stmt = self.prepare_cached(stmt).await?; + self.execute(&stmt, params).await + } + + async fn do_query_opt( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result, PgError> { + let stmt = self.prepare_cached(stmt).await?; + self.query_opt(&stmt, params).await + } +} + +impl<'a> DbClient for Transaction<'a> { + async fn do_query_one( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result { + let stmt = self.prepare_cached(stmt).await?; + self.query_one(&stmt, params).await + } + + async fn do_query( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result, PgError> { + let stmt = self.prepare_cached(stmt).await?; + self.query(&stmt, params).await + } + + async fn do_execute(&self, stmt: &str, params: &[&(dyn ToSql + Sync)]) -> Result { + let stmt = self.prepare_cached(stmt).await?; + self.execute(&stmt, params).await + } + + async fn do_query_opt( + &self, + stmt: &str, + params: &[&(dyn ToSql + Sync)], + ) -> Result, PgError> { + let stmt = self.prepare_cached(stmt).await?; + self.query_opt(&stmt, params).await + } +} + +mod conn_impl; diff --git a/crates/db/src/connection/proxied_user_conn.rs b/crates/db/src/connection/proxied_user_conn.rs new file mode 100644 index 00000000..ac66acb2 --- /dev/null +++ b/crates/db/src/connection/proxied_user_conn.rs @@ -0,0 +1,406 @@ +#![allow(clippy::let_unit_value)] + +use super::*; +use crate::FlowRunLogsRow; +use flow_lib::{context::get_jwt, BoxError, UserId}; +use reqwest::header::AUTHORIZATION; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::value::RawValue; +use thiserror::Error as ThisError; +use value::ConstBytes; + +#[derive(ThisError, Debug)] +pub enum Error { + #[error(transparent)] + Http(#[from] reqwest::Error), + #[error("failed to get JWT: {}", .0)] + Jwt(#[from] get_jwt::Error), + #[error("{}", .0)] + Upstream(String), +} + +#[derive(Clone)] +pub struct ProxiedUserConn { + pub user_id: UserId, + pub client: reqwest::Client, + pub rpc_url: String, + pub push_logs_url: String, + pub jwt_svc: get_jwt::Svc, +} + +#[derive(Serialize, Deserialize)] +pub struct RpcRequest<'a, T: Serialize> { + method: &'a str, + params: T, +} + +impl ProxiedUserConn { + pub async fn push_logs(&self, rows: &[FlowRunLogsRow]) -> crate::Result<()> { + let jwt = self + .jwt_svc + .call_ref(get_jwt::Request { + user_id: self.user_id, + }) + .await + .map_err(Error::from)? + .access_token; + self.client + .post(self.push_logs_url.clone()) + .header(AUTHORIZATION, &format!("Bearer {}", jwt)) + .json(&rows) + .send() + .await + .map_err(Error::from)? + .error_for_status() + .map_err(Error::from)?; + Ok(()) + } + + async fn send(&self, method: &str, params: &P) -> crate::Result + where + P: Serialize, + T: DeserializeOwned, + { + let jwt = self + .jwt_svc + .call_ref(get_jwt::Request { + user_id: self.user_id, + }) + .await + .map_err(Error::from)? + .access_token; + let result = self + .client + .post(self.rpc_url.clone()) + .header(AUTHORIZATION, &format!("Bearer {}", jwt)) + .json(&RpcRequest { method, params }) + .send() + .await + .map_err(Error::from)? + .json::>() + .await + .map_err(Error::from)?; + Ok(result.map_err(Error::Upstream)?) + } +} + +#[async_trait::async_trait] +impl UserConnectionTrait for ProxiedUserConn { + async fn get_wallet_by_pubkey(&self, _: &[u8; 32]) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_some_wallets(&self, _: &[i64]) -> crate::Result> { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_deployment_id_from_tag(&self, _: &FlowId, _: &str) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_deployment(&self, _: &DeploymentId) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_deployment_wallets(&self, _: &DeploymentId) -> crate::Result> { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_deployment_flows(&self, _: &DeploymentId) -> crate::Result> { + // TODO + Err(crate::Error::NotSupported) + } + + fn clone_connection(&self) -> Box { + Box::new(self.clone()) + } + + async fn insert_deployment(&self, d: &FlowDeployment) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn get_flow(&self, id: FlowId) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn download_storage_file(&self, path: &str) -> crate::Result { + // TODO + Err(crate::Error::NotSupported) + } + + async fn share_flow_run(&self, id: FlowRunId, user: UserId) -> crate::Result<()> { + self.send("share_flow_run", &(id, user)).await + } + + async fn get_flow_info(&self, flow_id: FlowId) -> crate::Result { + self.send("get_flow_info", &(flow_id,)).await + } + + async fn get_wallets(&self) -> crate::Result> { + self.send::<[(); 0], _>("get_wallets", &[]).await + } + + async fn clone_flow(&mut self, flow_id: FlowId) -> crate::Result> { + self.send("clone_flow", &(flow_id,)).await + } + + async fn new_flow_run( + &self, + config: &ClientConfig, + inputs: &ValueSet, + ) -> crate::Result { + self.send("new_flow_run", &(&config, &inputs)).await + } + + async fn get_previous_values( + &self, + nodes: &HashMap, + ) -> crate::Result>> { + self.send("get_previous_values", &(nodes,)).await + } + + async fn get_flow_config(&self, id: FlowId) -> crate::Result { + self.send("get_flow_config", &(id,)).await + } + + async fn set_start_time(&self, id: &FlowRunId, time: &DateTime) -> crate::Result<()> { + self.send("set_start_time", &(&id, &time)).await + } + + async fn push_flow_error(&self, id: &FlowRunId, error: &str) -> crate::Result<()> { + self.send("push_flow_error", &(&id, &error)).await + } + + async fn set_run_result( + &self, + id: &FlowRunId, + time: &DateTime, + not_run: &[NodeId], + output: &Value, + ) -> crate::Result<()> { + self.send("set_run_result", &(&id, &time, ¬_run, &output)) + .await + } + + async fn new_node_run( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + input: &Value, + ) -> crate::Result<()> { + self.send("new_node_run", &(&id, &node_id, ×, &time, &input)) + .await + } + + async fn save_node_output( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + output: &Value, + ) -> crate::Result<()> { + self.send("save_node_output", &(&id, &node_id, ×, &output)) + .await + } + + async fn push_node_error( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + error: &str, + ) -> crate::Result<()> { + self.send("push_node_error", &(&id, &node_id, ×, &error)) + .await + } + + async fn set_node_finish( + &self, + id: &FlowRunId, + node_id: &NodeId, + times: &i32, + time: &DateTime, + ) -> crate::Result<()> { + self.send("set_node_finish", &(&id, &node_id, ×, &time)) + .await + } + + async fn new_signature_request( + &self, + pubkey: &[u8; 32], + message: &[u8], + flow_run_id: Option<&FlowRunId>, + signatures: Option<&[Presigner]>, + ) -> crate::Result { + self.send( + "new_signature_request", + &( + &Value::from(*pubkey), + &Value::from(message), + flow_run_id, + signatures, + ), + ) + .await + } + + async fn save_signature( + &self, + id: &i64, + signature: &[u8; 64], + new_message: Option<&Bytes>, + ) -> crate::Result<()> { + self.send( + "save_signature", + &( + &id, + &Value::from(*signature), + &new_message.map(base64::encode), + ), + ) + .await + } + + async fn read_item(&self, store: &str, key: &str) -> crate::Result> { + self.send("read_item", &(&store, &key)).await + } + + async fn export_user_data(&mut self) -> crate::Result { + self.send("export_user_data", &()).await + } +} + +impl UserConnection { + pub async fn process_rpc(&mut self, req_json: &str) -> Result, BoxError> { + let req: RpcRequest<'_, &'_ RawValue> = serde_json::from_str(req_json)?; + match req.method { + "share_flow_run" => { + let (id, user) = serde_json::from_str(req.params.get())?; + let res = self.share_flow_run(id, user).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + + "get_flow_info" => { + let (id,) = serde_json::from_str(req.params.get())?; + let res = self.get_flow_info(id).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "get_wallets" => { + let res = self.get_wallets().await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "clone_flow" => { + let (flow_id,) = serde_json::from_str(req.params.get())?; + let res = self.clone_flow(flow_id).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "new_flow_run" => { + let (config, inputs) = serde_json::from_str(req.params.get())?; + let res = self.new_flow_run(&config, &inputs).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "get_previous_values" => { + let (nodes,) = serde_json::from_str(req.params.get())?; + let res = self.get_previous_values(&nodes).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "get_flow_config" => { + let (id,) = serde_json::from_str(req.params.get())?; + let res = self.get_flow_config(id).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "set_start_time" => { + let (id, time) = serde_json::from_str(req.params.get())?; + let res = self.set_start_time(&id, &time).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "push_flow_error" => { + let (id, error): (_, String) = serde_json::from_str(req.params.get())?; + let res = self.push_flow_error(&id, &error).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "set_run_result" => { + let (id, time, not_run, output): (_, _, Vec, _) = + serde_json::from_str(req.params.get())?; + let res = self.set_run_result(&id, &time, ¬_run, &output).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "new_node_run" => { + let (id, node_id, times, time, input) = serde_json::from_str(req.params.get())?; + let res = self + .new_node_run(&id, &node_id, ×, &time, &input) + .await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "save_node_output" => { + let (id, node_id, times, output) = serde_json::from_str(req.params.get())?; + let res = self + .save_node_output(&id, &node_id, ×, &output) + .await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "push_node_error" => { + let (id, node_id, times, error): (_, _, _, String) = + serde_json::from_str(req.params.get())?; + let res = self.push_node_error(&id, &node_id, ×, &error).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "set_node_finish" => { + let (id, node_id, times, time) = serde_json::from_str(req.params.get())?; + let res = self.set_node_finish(&id, &node_id, ×, &time).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "new_signature_request" => { + let (pubkey, message, flow_run_id, signatures): ( + Value, + Value, + Option, + Option>, + ) = serde_json::from_str(req.params.get())?; + let pubkey = value::from_value::>(pubkey)?.0; + let message = value::from_value::(message)?.into_vec(); + let res = self + .new_signature_request( + &pubkey, + &message, + flow_run_id.as_ref(), + signatures.as_deref(), + ) + .await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "save_signature" => { + let (id, signature, new_message): (_, Value, Option) = + serde_json::from_str(req.params.get())?; + let signature = value::from_value::>(signature)?.0; + let new_message = new_message + .map(base64::decode) + .transpose()? + .map(Bytes::from); + let res = self + .save_signature(&id, &signature, new_message.as_ref()) + .await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "read_item" => { + let (store, key): (String, String) = serde_json::from_str(req.params.get())?; + let res = self.read_item(&store, &key).await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + "export_user_data" => { + let res = self.export_user_data().await?; + Ok(serde_json::value::to_raw_value(&res)?) + } + name => Err(format!("unknown method: {}", name).into()), + } + } +} diff --git a/crates/db/src/error.rs b/crates/db/src/error.rs new file mode 100644 index 00000000..36dbb767 --- /dev/null +++ b/crates/db/src/error.rs @@ -0,0 +1,297 @@ +use crate::{connection::proxied_user_conn, StorageError}; +use actix_web::{http::StatusCode, ResponseError}; +use serde::Serialize; +use std::{ + fmt::{Debug, Display}, + panic::Location, +}; +use thiserror::Error as ThisError; + +#[derive(Serialize, Debug)] +pub struct ErrorBody { + pub error: String, +} + +impl ErrorBody { + pub fn build(e: &E) -> actix_web::HttpResponse { + actix_web::HttpResponse::build(e.status_code()).json(ErrorBody { + error: e.to_string(), + }) + } +} + +impl ResponseError for Error { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + Error::Unauthorized => StatusCode::NOT_FOUND, + Error::SpawnError(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::EncryptionError => StatusCode::INTERNAL_SERVER_ERROR, + Error::NoEncryptionKey => StatusCode::INTERNAL_SERVER_ERROR, + Error::NotSupported => StatusCode::INTERNAL_SERVER_ERROR, + Error::Timeout => StatusCode::INTERNAL_SERVER_ERROR, + Error::CreatePool(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::GetDbConnection(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::InitDb(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Execute { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Error::Data { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Error::Json { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Error::Parsing { .. } => StatusCode::INTERNAL_SERVER_ERROR, + Error::ResourceNotFound { .. } => StatusCode::NOT_FOUND, + Error::Io(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::NoCert => StatusCode::INTERNAL_SERVER_ERROR, + Error::AddCert(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Deserialize(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Storage(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Bcrypt => StatusCode::INTERNAL_SERVER_ERROR, + Error::Base58 => StatusCode::INTERNAL_SERVER_ERROR, + Error::LogicError(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::LocalStorage { .. } => todo!(), + Error::ProxyError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +#[derive(Debug, ThisError)] +pub enum Error { + #[error("unauthorized")] + Unauthorized, + #[error("spawn error: {}", .0)] + SpawnError(tokio::task::JoinError), + #[error("encryption error")] + EncryptionError, + #[error("no encryption key")] + NoEncryptionKey, + #[error("not supported")] + NotSupported, + #[error("time-out")] + Timeout, + #[error("failed to create database connection pool:\n{0}")] + CreatePool(deadpool_postgres::ConfigError), + #[error("failed to get a database connection from pool:\n{0}")] + GetDbConnection(deadpool_postgres::PoolError), + #[error("failed to initialize database tables:\n{0}")] + InitDb(tokio_postgres::Error), + #[error("failed to execute statement: {error}, context {context:?}, at {location}")] + Execute { + #[source] + error: tokio_postgres::Error, + context: &'static str, + location: &'static Location<'static>, + }, + #[error("failed to parse data: {error}, context {context:?}, at {location}")] + Data { + #[source] + error: tokio_postgres::Error, + context: &'static str, + location: &'static Location<'static>, + }, + #[error("failed to parse data: {error}, context {context:?}, at {location}")] + Json { + #[source] + error: serde_json::Error, + context: &'static str, + location: &'static Location<'static>, + }, + #[error("parsing error: {error}, context {context:?}, at {location}")] + Parsing { + #[source] + error: anyhow::Error, + context: &'static str, + location: &'static Location<'static>, + }, + #[error("{kind} not found: {id}, at {location}")] + ResourceNotFound { + kind: &'static str, + id: String, + location: &'static Location<'static>, + }, + #[error("{0}")] + Io(#[from] std::io::Error), + #[error("no certificate in PEM file")] + NoCert, + #[error("failed to add cert to root-ca: {0}")] + AddCert(String), + #[error(transparent)] + Deserialize(serde_json::Error), + #[error(transparent)] + Storage(#[from] StorageError), + #[error("bcrypt failed")] + Bcrypt, + #[error("invalid base58")] + Base58, + #[error("{0}")] + LogicError(E), + #[error("sled error: {error}, context {context:?}, at {location}")] + LocalStorage { + #[source] + error: kv::Error, + context: &'static str, + location: &'static Location<'static>, + }, + #[error(transparent)] + ProxyError(#[from] proxied_user_conn::Error), +} + +pub type Result = std::result::Result; + +impl From for Error { + fn from(error: tokio::task::JoinError) -> Self { + Self::SpawnError(error) + } +} + +impl From for Error { + fn from(_: chacha20poly1305::Error) -> Self { + Self::EncryptionError + } +} + +impl> Error { + pub fn erase_type(self) -> Error { + match self { + Error::Unauthorized => Error::Unauthorized, + Error::SpawnError(e) => Error::SpawnError(e), + Error::EncryptionError => Error::EncryptionError, + Error::NoEncryptionKey => Error::NoEncryptionKey, + Error::Timeout => Error::Timeout, + Error::NotSupported => Error::NotSupported, + Error::LogicError(e) => Error::LogicError(anyhow::anyhow!(e)), + Error::CreatePool(e) => Error::CreatePool(e), + Error::GetDbConnection(e) => Error::GetDbConnection(e), + Error::InitDb(e) => Error::InitDb(e), + Error::Execute { + error, + context, + location, + } => Error::Execute { + error, + context, + location, + }, + Error::Data { + error, + context, + location, + } => Error::Data { + error, + context, + location, + }, + Error::Json { + error, + context, + location, + } => Error::Json { + error, + context, + location, + }, + Error::ResourceNotFound { kind, id, location } => { + Error::ResourceNotFound { kind, id, location } + } + Error::Io(e) => Error::Io(e), + Error::NoCert => Error::NoCert, + Error::AddCert(e) => Error::AddCert(e), + Error::Deserialize(e) => Error::Deserialize(e), + Error::Storage(e) => Error::Storage(e), + Error::Bcrypt => Error::Bcrypt, + Error::Base58 => Error::Base58, + Error::LocalStorage { + error, + context, + location, + } => Error::LocalStorage { + error, + context, + location, + }, + Error::ProxyError(e) => Error::ProxyError(e), + Error::Parsing { + error, + context, + location, + } => Error::Parsing { + error, + context, + location, + }, + } + } + + /// Local storage (sled) error + #[track_caller] + pub fn local(context: &'static str) -> impl FnOnce(kv::Error) -> Self { + let location = std::panic::Location::caller(); + + move |error: kv::Error| Error::LocalStorage { + context, + location, + error, + } + } + + /// Error when executing a PG statement. + #[track_caller] + pub fn exec(context: &'static str) -> impl FnOnce(tokio_postgres::Error) -> Self { + let location = std::panic::Location::caller(); + + move |error: tokio_postgres::Error| Error::Execute { + context, + location, + error, + } + } + + /// Error when parsing data from the database, usually for JSON deserialize error. + #[track_caller] + pub fn data(context: &'static str) -> impl FnOnce(tokio_postgres::Error) -> Self { + let location = std::panic::Location::caller(); + + move |error: tokio_postgres::Error| Error::Data { + context, + location, + error, + } + } + + /// Error when parsing JSON data from the database. + #[track_caller] + pub fn json(context: &'static str) -> impl FnOnce(serde_json::Error) -> Self { + let location = std::panic::Location::caller(); + + move |error: serde_json::Error| Error::Json { + context, + location, + error, + } + } + + /// Error when parsing data from the database. + #[track_caller] + pub fn parsing( + context: &'static str, + ) -> impl FnOnce(E1) -> Self { + let location = std::panic::Location::caller(); + + move |error: E1| Error::Parsing { + context, + location, + error: error.into(), + } + } + + #[track_caller] + pub fn not_found(kind: &'static str, id: I) -> Self { + let location = std::panic::Location::caller(); + + Error::ResourceNotFound { + kind, + location, + id: id.to_string(), + } + } +} diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs new file mode 100644 index 00000000..574f713f --- /dev/null +++ b/crates/db/src/lib.rs @@ -0,0 +1,68 @@ +use chrono::{DateTime, Utc}; +use config::Encrypted; +use flow_lib::{FlowRunId, NodeId, UserId}; +use serde::{Deserialize, Serialize}; + +pub mod apikey; +pub mod config; +pub mod connection; +pub mod error; +pub mod local_storage; +pub mod pool; +pub mod wasm_storage; + +pub use deadpool_postgres::Client as DeadPoolClient; +pub use error::{Error, Result}; +pub use local_storage::LocalStorage; +pub use tokio_postgres::error::SqlState; +pub use wasm_storage::{StorageError, WasmStorage}; + +#[derive(Serialize, Deserialize)] +pub struct NodeRunRow { + pub user_id: UserId, + pub flow_run_id: FlowRunId, + pub node_id: NodeId, + pub times: i32, + #[serde(with = "chrono::serde::ts_milliseconds")] + pub start_time: DateTime, + #[serde(with = "chrono::serde::ts_milliseconds")] + pub end_time: DateTime, + pub input: Option, + pub output: Option, + pub errors: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct FlowRunLogsRow { + pub user_id: UserId, + pub flow_run_id: FlowRunId, + pub log_index: i32, + pub node_id: Option, + pub times: Option, + #[serde(with = "chrono::serde::ts_milliseconds")] + pub time: DateTime, + pub log_level: String, + pub content: String, + pub module: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Wallet { + pub id: i64, + #[serde(with = "utils::serde_bs58")] + pub pubkey: [u8; 32], + #[serde( + default, + with = "utils::serde_bs58::opt", + // skip_serializing_if = "Option::is_none" + )] + pub keypair: Option<[u8; 64]>, +} + +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct EncryptedWallet { + pub(crate) id: i64, + #[serde(with = "utils::serde_bs58")] + pub(crate) pubkey: [u8; 32], + pub(crate) encrypted_keypair: Option, +} diff --git a/crates/db/src/local_storage/mod.rs b/crates/db/src/local_storage/mod.rs new file mode 100644 index 00000000..ea517299 --- /dev/null +++ b/crates/db/src/local_storage/mod.rs @@ -0,0 +1,181 @@ +use crate::Error; +use chrono::{DateTime, Utc}; +use flow_lib::UserId; +use kv::{Bucket, Store}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{path::Path, time::Duration}; + +pub trait CacheBucket { + type Key: ?Sized; + type EncodedKey: for<'a> kv::Key<'a>; + + type Object: Serialize + DeserializeOwned; + fn name() -> &'static str; + fn encode_key(key: &Self::Key) -> Self::EncodedKey; + fn cache_time() -> Duration; + fn can_read(obj: &Self::Object, user_id: &UserId) -> bool; +} + +#[derive(Clone)] +pub struct LocalStorage { + db: kv::Store, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Jwt { + pub access_token: String, + pub refresh_token: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub expires_at: DateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Password { + pub password: String, + pub encrypted_password: String, +} + +fn user_key(key: &UserId) -> &[u8] { + key.as_bytes() +} + +#[derive(Serialize, Deserialize)] +struct CacheValue { + expires_at: i64, + value: V, +} + +impl LocalStorage { + pub fn new>(path: P) -> crate::Result { + tracing::info!("openning sled storage: {}", path.as_ref().display()); + let db = Store::new(kv::Config::new(path)).map_err(Error::local("open"))?; + Ok(Self { db }) + } + + pub fn set_cache(&self, key: &C::Key, value: C::Object) -> crate::Result<()> + where + C: CacheBucket, + { + let bucket = C::name(); + tracing::debug!("set_cache {}", bucket); + self.db + .bucket::>>(Some(bucket)) + .map_err(Error::local("open cache bucket"))? + .set( + &C::encode_key(key), + &kv::Json(CacheValue { + expires_at: Utc::now().timestamp() + C::cache_time().as_secs() as i64, + value, + }), + ) + .map_err(Error::local(bucket))?; + Ok(()) + } + + pub fn get_cache(&self, user_id: &UserId, key: &C::Key) -> Option + where + C: CacheBucket, + { + let bucket = C::name(); + tracing::trace!("get_cache {}", bucket); + let result = self + .db + .bucket::<_, kv::Json>>(Some(bucket)) + .inspect_err(|error| { + tracing::error!("get_cache error: {}", error); + }) + .ok()? + .transaction::<_, kv::Error, _>(|tx| { + let now = Utc::now().timestamp(); + let key = C::encode_key(key); + if let Some(obj) = tx.get(&key)? { + if obj.0.expires_at <= now { + tx.remove(&key)?; + Ok(None) + } else if C::can_read(&obj.0.value, user_id) { + tracing::debug!("cache hit {}", bucket); + Ok(Some(obj.0.value)) + } else { + Ok(None) + } + } else { + Ok(None) + } + }); + match result { + Ok(result) => result, + Err(error) => { + tracing::error!("get_cache error: {}", error); + None + } + } + } + + fn jwt_bucket(&self) -> crate::Result>> { + self.db + .bucket(Some("JWTs")) + .map_err(Error::local("open JWTs bucket")) + } + + pub fn get_jwt(&self, user_id: &UserId) -> crate::Result> { + tracing::debug!("get JWTs, user_id={}", user_id); + Ok(self + .jwt_bucket()? + .get(&user_key(user_id)) + .map_err(Error::local("get JWTs"))? + .map(|j| j.0)) + } + + pub fn set_jwt(&self, user_id: &UserId, jwt: &Jwt) -> crate::Result<()> { + tracing::debug!("set JWTs, user_id={}", user_id); + self.jwt_bucket()? + .set(&user_key(user_id), &kv::Json(jwt.clone())) + .map_err(Error::local("set JWTs"))?; + Ok(()) + } + + pub fn remove_jwt(&self, user_id: &UserId) -> crate::Result<()> { + tracing::debug!("remove JWTs, user_id={}", user_id); + self.jwt_bucket()? + .remove(&user_key(user_id)) + .map_err(Error::local("remove JWTs"))?; + Ok(()) + } + + fn password_bucket(&self) -> crate::Result>> { + self.db + .bucket(Some("Passwords")) + .map_err(Error::local("open Passwords")) + } + + pub fn set_password(&self, user_id: &UserId, password: Password) -> crate::Result<()> { + self.password_bucket()? + .set(&user_key(user_id), &kv::Bincode(password)) + .map_err(Error::local("set Passwords"))?; + Ok(()) + } + + pub fn get_or_generate_password(&self, user_id: &UserId) -> crate::Result { + tracing::debug!("get password {}", user_id); + self.password_bucket()? + .transaction::<_, kv::Error, _>(|tx| { + if let Some(p) = tx.get(&user_key(user_id))? { + Ok(p.0) + } else { + let password = rand_password(); + let password = Password { + encrypted_password: bcrypt::hash(&password, 10).unwrap(), + password, + }; + tx.set(&user_key(user_id), &kv::Bincode(password.clone()))?; + Ok(password) + } + }) + .map_err(Error::local("get or generate password")) + } +} + +fn rand_password() -> String { + use rand::distributions::DistString; + rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 24) +} diff --git a/crates/db/src/pool.rs b/crates/db/src/pool.rs new file mode 100644 index 00000000..bf908ef2 --- /dev/null +++ b/crates/db/src/pool.rs @@ -0,0 +1,278 @@ +use crate::{ + config::{DbConfig, Encrypted, EncryptionKey, ProxiedDbConfig}, + connection::{ + proxied_user_conn::{self, ProxiedUserConn}, + AdminConn, UserConnection, UserConnectionTrait, + }, + Error, LocalStorage, WasmStorage, +}; +use deadpool_postgres::{ClientWrapper, Hook, HookError, Metrics, Pool, PoolConfig, SslMode}; +use flow_lib::{context::get_jwt, solana::Keypair, UserId}; +use futures_util::FutureExt; +use hashbrown::HashMap; +use std::time::{Duration, Instant}; + +pub use deadpool_postgres::Object as Connection; + +#[derive(Clone)] +pub enum DbPool { + Real(RealDbPool), + Proxied(ProxiedDbPool), +} + +impl DbPool { + pub async fn get_user_conn( + &self, + user_id: UserId, + ) -> crate::Result> { + match self { + DbPool::Real(pool) => Ok(Box::new(pool.get_user_conn(user_id).await?)), + DbPool::Proxied(pool) => Ok(Box::new(pool.get_user_conn(user_id).await?)), + } + } + + pub fn get_local(&self) -> &LocalStorage { + match self { + DbPool::Real(pool) => pool.get_local(), + DbPool::Proxied(pool) => pool.get_local(), + } + } +} + +#[derive(Clone)] +pub struct RealDbPool { + encryption_key: Option, + pg: Pool, + wasm: WasmStorage, + local: LocalStorage, +} + +fn read_cert(path: &std::path::Path) -> crate::Result { + let cert = std::fs::read(path)?; + let mut buf = cert.as_slice(); + let items = rustls_pemfile::read_all(&mut buf)?; + + let cert = items + .iter() + .find_map(|i| { + if let rustls_pemfile::Item::X509Certificate(c) = i { + Some(rustls::Certificate(c.clone())) + } else { + None + } + }) + .ok_or(Error::NoCert)?; + + Ok(cert) +} + +async fn conn_healthcheck( + conn: &mut ClientWrapper, + metric: &Metrics, +) -> Result<(), deadpool_postgres::HookError> { + if metric.last_used() <= Duration::from_secs(10) { + Ok(()) + } else { + conn.simple_query("").await.map_err(HookError::Backend)?; + Ok(()) + } +} + +impl RealDbPool { + pub async fn new( + cfg: &DbConfig, + wasm: WasmStorage, + local: LocalStorage, + ) -> crate::Result { + use deadpool_postgres::{Config, Runtime}; + + let pool_cfg = Config { + user: Some(cfg.user.clone()), + password: Some(cfg.password.clone()), + dbname: Some(cfg.dbname.clone()), + host: Some(cfg.host.clone()), + port: Some(cfg.port), + ssl_mode: Some(if cfg.ssl.enabled { + SslMode::Require + } else { + SslMode::Disable + }), + pool: cfg.max_pool_size.map(|size| PoolConfig { + max_size: size, + ..Default::default() + }), + ..Config::default() + }; + tracing::info!("SSL enabled: {}", cfg.ssl.enabled); + let encryption_key = cfg.encryption_key.clone(); + + let builder = if cfg.ssl.enabled { + let mut roots = rustls::RootCertStore::empty(); + if let Some(path) = cfg.ssl.cert.as_ref() { + tracing::info!("adding certificate: {}", path.display()); + let cert = read_cert(path)?; + roots + .add(&cert) + .map_err(|e| Error::AddCert(e.to_string()))?; + } + let certs = rustls_native_certs::load_native_certs() + .map_err(|e| Error::AddCert(e.to_string()))?; + for cert in certs { + roots + .add(&rustls::Certificate(cert.0)) + .map_err(|e| Error::AddCert(e.to_string()))?; + } + let config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + let tls = tokio_postgres_rustls::MakeRustlsConnect::new(config); + pool_cfg.builder(tls).map_err(Error::CreatePool)? + } else { + pool_cfg + .builder(tokio_postgres::NoTls) + .map_err(Error::CreatePool)? + }; + + let pg = builder + .pre_recycle(Hook::async_fn(|c, m| conn_healthcheck(c, m).boxed())) + .runtime(Runtime::Tokio1) + .build() + .expect("shouldn't fail"); + + // Test to see if we can connect + let conn = pg.get().await.map_err(Error::GetDbConnection)?; + match ping(&conn).await { + Ok((mean, std)) => { + tracing::info!("connection ping: {:.2}±{:.2}ms", mean, std); + } + Err(error) => { + tracing::error!("{}", error); + } + } + + { + let pg = pg.clone(); + let max_age = Duration::from_secs(30); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(45)).await; + if pg.is_closed() { + break; + } + + // close connections if they are unused for 30 secs + pg.retain(|_, metrics| metrics.last_used() < max_age); + } + }); + }; + + Ok(Self { + pg, + wasm, + local, + encryption_key, + }) + } + + pub(crate) fn encryption_key(&self) -> crate::Result<&EncryptionKey> { + self.encryption_key + .as_ref() + .ok_or(crate::Error::NoEncryptionKey) + } + + pub fn encrypt_keypair(&self, keypair: &Keypair) -> crate::Result { + Ok(self.encryption_key()?.encrypt_keypair(keypair)) + } + + pub async fn get_conn(&self) -> crate::Result { + // let conn = tokio::time::timeout(Duration::from_secs(240), self.pg.get()) + // .await + // .map_err(|_| Error::Timeout)? + // .map_err(Error::GetDbConnection)?; + // Ok(conn) + self.pg.get().await.map_err(Error::GetDbConnection) + } + + pub async fn get_user_conn(&self, user_id: UserId) -> crate::Result { + Ok(UserConnection::new( + self.clone(), + self.wasm.clone(), + user_id, + self.local.clone(), + )) + } + + pub async fn get_admin_conn(&self) -> crate::Result { + Ok(AdminConn::new(self.clone(), self.local.clone())) + } + + pub fn get_local(&self) -> &LocalStorage { + &self.local + } +} + +async fn ping(conn: &Connection) -> crate::Result<(f64, f64)> { + let stmt = conn + .prepare_cached("SELECT gen_random_uuid()") + .await + .map_err(Error::exec("prepare"))?; + + let mut time = Vec::new(); + + for _ in 0..10 { + let now = Instant::now(); + conn.query_one(&stmt, &[]) + .await + .map_err(Error::exec("select"))?; + let elapsed = now.elapsed(); + time.push(elapsed.as_secs_f64() * 1000.0); + } + + let mean = time.iter().sum::() / time.len() as f64; + let std = + (time.iter().map(|x| (x - mean) * (x - mean)).sum::() / time.len() as f64).sqrt(); + + Ok((mean, std)) +} + +#[derive(Clone)] +pub struct ProxiedDbPool { + pub config: ProxiedDbConfig, + pub client: reqwest::Client, + pub local: LocalStorage, + pub services: HashMap, +} + +impl ProxiedDbPool { + pub fn new( + config: ProxiedDbConfig, + local: LocalStorage, + services: HashMap, + ) -> crate::Result { + Ok(Self { + config, + client: reqwest::Client::new(), + local, + services, + }) + } + + pub fn get_local(&self) -> &LocalStorage { + &self.local + } + + pub async fn get_user_conn(&self, user_id: UserId) -> crate::Result { + Ok(ProxiedUserConn { + user_id, + client: self.client.clone(), + rpc_url: self.config.upstream_url.clone() + "/proxy/db_rpc", + push_logs_url: self.config.upstream_url.clone() + "/proxy/db_push_logs", + jwt_svc: self + .services + .get(&user_id) + .ok_or(proxied_user_conn::Error::Jwt(get_jwt::Error::NotAllowed))? + .clone(), + }) + } +} diff --git a/crates/db/src/wasm_storage.rs b/crates/db/src/wasm_storage.rs new file mode 100644 index 00000000..0ebef95c --- /dev/null +++ b/crates/db/src/wasm_storage.rs @@ -0,0 +1,63 @@ +use bytes::Bytes; +use reqwest::{ + header::{HeaderValue, AUTHORIZATION}, + Url, +}; +use thiserror::Error as ThisError; + +#[derive(Clone)] +pub struct WasmStorage { + client: reqwest::Client, + base_url: Url, +} + +#[derive(ThisError, Debug)] +pub enum StorageError { + #[error("invalid anon key")] + InvalidAnonKey, + #[error("failed to build URL: {0}")] + BuildUrl(url::ParseError), + #[error("failed to build URL: {0}")] + BuildClient(reqwest::Error), + #[error(transparent)] + Network(#[from] reqwest::Error), + #[error("{:?} {}", code, body)] + Api { + code: reqwest::StatusCode, + body: String, + }, +} + +impl WasmStorage { + pub fn new( + supabase_endpoint: Url, + anon_key: &str, + wasm_bucket: &str, + ) -> Result { + let anon_key = HeaderValue::from_str(&format!("Bearer {}", anon_key)) + .map_err(|_| StorageError::InvalidAnonKey)?; + let client = reqwest::Client::builder() + .default_headers([(AUTHORIZATION, anon_key)].into_iter().collect()) + .build() + .map_err(StorageError::BuildClient)?; + let base_url = supabase_endpoint + .join(&format!("storage/v1/object/{}/", wasm_bucket)) + .map_err(StorageError::BuildUrl)?; + + Ok(Self { client, base_url }) + } + + pub async fn download(&self, path: &str) -> Result { + let url = self.base_url.join(path).map_err(StorageError::BuildUrl)?; + let resp = self.client.get(url).send().await?; + if resp.status() == reqwest::StatusCode::OK { + Ok(resp.bytes().await?) + } else { + Err(StorageError::Api { + code: resp.status(), + body: String::from_utf8(resp.bytes().await?.into()) + .unwrap_or_else(|_| "".to_owned()), + }) + } + } +} diff --git a/crates/flow-server/Cargo.toml b/crates/flow-server/Cargo.toml new file mode 100644 index 00000000..fe784d64 --- /dev/null +++ b/crates/flow-server/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "flow-server" +version = "0.0.0" +edition = "2021" + +[features] +default = ["import"] +import = [] + +[dependencies] +db = { workspace = true } +flow = { workspace = true } +flow-lib = { workspace = true } +value = { workspace = true } +space-wasm = { workspace = true } +utils = { workspace = true } +cmds-pdg = { workspace = true } +cmds-std = { workspace = true } +cmds-solana = { workspace = true } + +actix = "0.13" +actix-web = "4" +actix-web-actors = "4" +actix-cors = "0.6" +reqwest = { version = "0.12", features = ["rustls-tls"] } +tokio = "1" +tower = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_with = "3" +toml = "0.5" +chrono = "0.4" +thiserror = "1" +uuid = { version = "1", features = ["v4", "serde"] } +futures-util = "0.3" +futures-channel = "0.3" +bytes = "1" +hmac = "0.12" +sha2 = "0.10" +base64 = "0.13" +rand = "0.8" +bincode = "=2.0.0-rc.2" +blake3 = "1.3" +ed25519-dalek = "2" +bs58 = "0.4" +hex = "0.4" +hashbrown = "0.14" +anyhow = "1" +tokio-util = "0.7" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +solana-sdk = { version = "1", default-features = false } +solana-client = "1" +either = { version = "1.9", features = ["serde"] } +regex = "1.8.3" +once_cell = "1.17.2" +rhai-script.workspace = true +tracing-log = "0.2.0" +url = { version = "2.5.0", features = ["serde"] } +async-trait = "0.1.80" +five8 = { version = "0.2.1", features = ["std"] } +getset = "0.1.3" + +[dev-dependencies] +criterion = "0.5" +inventory = "0.3" + +[[bench]] +name = "crypto" +harness = false diff --git a/crates/flow-server/Dockerfile b/crates/flow-server/Dockerfile new file mode 100644 index 00000000..8bf1265e --- /dev/null +++ b/crates/flow-server/Dockerfile @@ -0,0 +1,47 @@ +FROM docker.io/library/rust AS rustc +RUN apt-get update && apt-get install -y clang lld && rm -rf /var/lib/apt/lists/* +ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld" +RUN cargo install cargo-chef --quiet +WORKDIR /build/ + +# Step 1: Compute a recipe file +FROM rustc AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + + +# Step 2: Cache project dependencies +FROM rustc AS cacher +ARG PROFILE=release +COPY --from=planner /build/recipe.json recipe.json +RUN cargo chef cook --profile=$PROFILE --recipe-path recipe.json --bin flow-server 2>/dev/null + +# Step 3: Build the binary +FROM rustc AS builder +COPY . . +# Copy over the cached dependencies from above +COPY --from=cacher /build/target target +COPY --from=cacher /usr/local/cargo /usr/local/cargo +ARG PROFILE=release +RUN cargo build --profile=$PROFILE --bin flow-server --quiet + +FROM docker.io/denoland/deno:debian AS deno + +# Step 4: +# Create a tiny output image. +# It only contains our final binaries. +FROM docker.io/library/debian:stable-slim AS runtime +COPY ./certs/supabase-prod-ca-2021.crt /usr/local/share/ca-certificates/ +COPY ./crates/flow-server/entrypoint.bash /space-operator/ +RUN apt-get update && \ + apt-get install -y libssl3 ca-certificates wget && \ + wget https://github.com/supabase/cli/releases/download/v1.167.4/supabase_1.167.4_linux_amd64.deb && \ + apt-get install -y ./supabase_1.167.4_linux_amd64.deb && \ + apt-get remove -y wget && \ + rm -rf /var/lib/apt/lists/* +COPY --from=deno /usr/bin/deno /usr/local/bin +RUN deno --version +WORKDIR /space-operator/ +COPY --from=builder /build/target/release/flow-server /usr/local/bin +RUN bash -c "ldd /usr/local/bin/* | (! grep 'not found')" +ENTRYPOINT ["./entrypoint.bash"] diff --git a/crates/flow-server/benches/crypto.rs b/crates/flow-server/benches/crypto.rs new file mode 100644 index 00000000..11d473b3 --- /dev/null +++ b/crates/flow-server/benches/crypto.rs @@ -0,0 +1,32 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use ed25519_dalek::Signer; +use flow_server::user::*; + +fn init_login(m: SignatureAuth, pk: [u8; 32]) { + m.init_login(0, &pk); +} + +fn confirm(m: SignatureAuth, s: &str) { + let _ = m.confirm(0, s); +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let sk = rand::random::<[u8; 32]>(); + let kp = ed25519_dalek::SigningKey::from_bytes(&sk); + let m = SignatureAuth::new(rand::random()); + let pk = *kp.verifying_key().as_bytes(); + c.bench_function("init_login", |b| { + b.iter(|| init_login(black_box(m), black_box(pk))) + }); + + let msg = m.init_login(0, &pk); + let signature = bs58::encode(kp.sign(msg.as_bytes()).to_bytes()).into_string(); + let text = format!("{msg}.{signature}"); + let s = text.as_str(); + c.bench_function("confirm", |b| { + b.iter(|| confirm(black_box(m), black_box(s))) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/flow-server/entrypoint.bash b/crates/flow-server/entrypoint.bash new file mode 100755 index 00000000..64dcc4e1 --- /dev/null +++ b/crates/flow-server/entrypoint.bash @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +supabase migration up --db-url $MIGRATION_DB_URL +unset MIGRATION_DB_URL +flow-server $CONFIG_FILE diff --git a/crates/flow-server/src/api/apikey_info.rs b/crates/flow-server/src/api/apikey_info.rs new file mode 100644 index 00000000..92b0180b --- /dev/null +++ b/crates/flow-server/src/api/apikey_info.rs @@ -0,0 +1,24 @@ +use super::prelude::*; + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub user_id: UserId, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/info") + .wrap(config.cors()) + .route(web::get().to(key_info)) +} + +async fn key_info( + db: web::Data, + apikey: web::Header, +) -> Result, Error> { + let user_id = db + .get_admin_conn() + .await? + .get_user_id_from_apikey(&apikey.into_inner().into_inner()) + .await?; + Ok(web::Json(Output { user_id })) +} diff --git a/crates/flow-server/src/api/auth_proxy.rs b/crates/flow-server/src/api/auth_proxy.rs new file mode 100644 index 00000000..4b5d32a2 --- /dev/null +++ b/crates/flow-server/src/api/auth_proxy.rs @@ -0,0 +1,26 @@ +use super::prelude::{ + auth::{JWTPayload, Token}, + *, +}; + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub payload: JWTPayload, + pub token: Token, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/auth") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(auth)) +} + +async fn auth( + payload: web::ReqData, + token: web::ReqData, +) -> Result, Error> { + let payload = payload.into_inner(); + let token = token.into_inner(); + Ok(web::Json(Output { payload, token })) +} diff --git a/crates/flow-server/src/api/claim_token.rs b/crates/flow-server/src/api/claim_token.rs new file mode 100644 index 00000000..a666c777 --- /dev/null +++ b/crates/flow-server/src/api/claim_token.rs @@ -0,0 +1,52 @@ +use super::prelude::*; +use crate::db_worker::token_worker::LoginWithAdminCred; +use chrono::{DateTime, Utc}; +use db::local_storage::Jwt; +use flow_lib::config::Endpoints; + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub user_id: UserId, + pub access_token: String, + pub refresh_token: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub expires_at: DateTime, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/claim_token") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .app_data(web::Data::new(config.endpoints())) + .route(web::post().to(claim_token)) +} + +async fn claim_token( + user: web::ReqData, + db: web::Data, + endpoints: web::Data, +) -> Result, Error> { + let user = user.into_inner(); + let result = LoginWithAdminCred { + client: reqwest::Client::new(), + user_id: user.user_id, + db: (**db).clone(), + endpoints: (**endpoints).clone(), + } + .claim() + .await; + + match result { + Ok(Jwt { + access_token, + refresh_token, + expires_at, + }) => Ok(web::Json(Output { + user_id: user.user_id, + access_token, + refresh_token, + expires_at, + })), + Err(error) => Err(Error::custom(StatusCode::INTERNAL_SERVER_ERROR, error)), + } +} diff --git a/crates/flow-server/src/api/clone_flow.rs b/crates/flow-server/src/api/clone_flow.rs new file mode 100644 index 00000000..fdd25e5b --- /dev/null +++ b/crates/flow-server/src/api/clone_flow.rs @@ -0,0 +1,44 @@ +use super::prelude::*; +use crate::db_worker::{user_worker::CloneFlow, GetUserWorker}; +use db::pool::DbPool; +use hashbrown::HashMap; + +#[derive(Serialize)] +pub struct Output { + pub flow_id: FlowId, + pub id_map: HashMap, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/clone/{id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(clone_flow)) +} + +async fn clone_flow( + flow_id: web::Path, + user: web::ReqData, + db_worker: web::Data>, +) -> Result, Error> { + let flow_id = flow_id.into_inner(); + let user = user.into_inner(); + + let id_map = db_worker + .send(GetUserWorker { + user_id: user.user_id, + }) + .await? + .send(CloneFlow { + user_id: user.user_id, + flow_id, + }) + .await??; + + Ok(web::Json(Output { + flow_id: *id_map + .get(&flow_id) + .ok_or_else(|| Error::custom(StatusCode::INTERNAL_SERVER_ERROR, "bug in clone_flow"))?, + id_map, + })) +} diff --git a/crates/flow-server/src/api/confirm_auth.rs b/crates/flow-server/src/api/confirm_auth.rs new file mode 100644 index 00000000..0b6e7cc8 --- /dev/null +++ b/crates/flow-server/src/api/confirm_auth.rs @@ -0,0 +1,34 @@ +use super::prelude::*; +use crate::user::{SignatureAuth, SupabaseAuth}; +use serde_json::value::RawValue; + +#[derive(Deserialize)] +pub struct Params { + token: String, +} + +#[derive(Serialize)] +pub struct Output { + /// Response from Supabase's login API + session: Box, + /// True if this is a new user + new_user: bool, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/confirm") + .wrap(config.anon_key()) + .wrap(config.cors()) + .route(web::post().to(confirm_auth)) +} + +async fn confirm_auth( + params: web::Json, + sig: web::Data, + sup: web::Data, +) -> Result, Error> { + let Params { token } = params.into_inner(); + let payload = sig.confirm(chrono::Utc::now().timestamp(), &token)?; + let (session, new_user) = sup.login(&payload).await?; + Ok(web::Json(Output { session, new_user })) +} diff --git a/crates/flow-server/src/api/create_apikey.rs b/crates/flow-server/src/api/create_apikey.rs new file mode 100644 index 00000000..550e5505 --- /dev/null +++ b/crates/flow-server/src/api/create_apikey.rs @@ -0,0 +1,42 @@ +use super::prelude::*; +use db::{ + apikey::{self, NameConflict}, + Error as DbError, +}; + +#[derive(Deserialize)] +pub struct Params { + name: String, +} + +#[derive(Serialize)] +pub struct Output { + pub full_key: String, + #[serde(flatten)] + pub key: apikey::APIKey, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/create") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(create_key)) +} + +async fn create_key( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let user_id = user.into_inner().user_id; + let Params { name } = params.into_inner(); + let r = db.get_user_conn(user_id).await?.create_apikey(&name).await; + let (key, full_key) = match r { + Ok(r) => r, + Err(DbError::LogicError(NameConflict)) => { + return Err(Error::custom(StatusCode::BAD_REQUEST, "NameConflict")) + } + Err(error) => return Err(error.erase_type().into()), + }; + Ok(web::Json(Output { full_key, key })) +} diff --git a/crates/flow-server/src/api/data_export.rs b/crates/flow-server/src/api/data_export.rs new file mode 100644 index 00000000..321b129f --- /dev/null +++ b/crates/flow-server/src/api/data_export.rs @@ -0,0 +1,18 @@ +use super::prelude::*; +use db::connection::ExportedUserData; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/export") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(export)) +} + +async fn export( + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let user_id = user.into_inner().user_id; + let data = db.get_user_conn(user_id).await?.export_user_data().await?; + Ok(web::Json(data)) +} diff --git a/crates/flow-server/src/api/data_import.rs b/crates/flow-server/src/api/data_import.rs new file mode 100644 index 00000000..5c36252c --- /dev/null +++ b/crates/flow-server/src/api/data_import.rs @@ -0,0 +1,20 @@ +use super::prelude::*; +use db::connection::ExportedUserData; + +pub fn service(config: &Config) -> Option { + Some( + web::resource("/import") + .wrap(config.service_key()?) + .wrap(config.cors()) + .route(web::post().to(import)), + ) +} + +async fn import( + db: web::Data, + data: web::Json, +) -> Result, Error> { + let data = data.into_inner(); + db.get_admin_conn().await?.import_data(data).await?; + Ok(web::Json(Success)) +} diff --git a/crates/flow-server/src/api/db_push_logs.rs b/crates/flow-server/src/api/db_push_logs.rs new file mode 100644 index 00000000..745d14c5 --- /dev/null +++ b/crates/flow-server/src/api/db_push_logs.rs @@ -0,0 +1,25 @@ +use super::prelude::*; +use crate::db_worker::CopyIn; +use db::FlowRunLogsRow; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/db_push_logs") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(db_push_logs)) +} + +async fn db_push_logs( + params: web::Json>, + user: web::ReqData, + db_worker: web::Data>, +) -> Result, Error> { + let user_id = user.into_inner().user_id; + let rows = params + .into_inner() + .into_iter() + .filter(|row| row.user_id == user_id) + .collect::>(); + db_worker.send(CopyIn(rows)).await?; + Ok(web::Json(Success)) +} diff --git a/crates/flow-server/src/api/db_rpc.rs b/crates/flow-server/src/api/db_rpc.rs new file mode 100644 index 00000000..4e71cc3c --- /dev/null +++ b/crates/flow-server/src/api/db_rpc.rs @@ -0,0 +1,24 @@ +use super::prelude::*; +use serde_json::value::RawValue; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/db_rpc") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(db_rpc)) +} + +async fn db_rpc( + params: web::Json>, + user: web::ReqData, + db: web::Data, +) -> Result, String>>, Error> { + let user_id = user.into_inner().user_id; + let result = db + .get_user_conn(user_id) + .await? + .process_rpc(params.0.get()) + .await + .map_err(|e| e.to_string()); + Ok(web::Json(result)) +} diff --git a/crates/flow-server/src/api/delete_apikey.rs b/crates/flow-server/src/api/delete_apikey.rs new file mode 100644 index 00000000..8087b506 --- /dev/null +++ b/crates/flow-server/src/api/delete_apikey.rs @@ -0,0 +1,30 @@ +use super::prelude::*; + +#[derive(Deserialize)] +pub struct Params { + key_hash: String, +} + +#[derive(Serialize)] +pub struct Output {} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/delete") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(delete_key)) +} + +async fn delete_key( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let user_id = user.into_inner().user_id; + let Params { key_hash } = params.into_inner(); + db.get_user_conn(user_id) + .await? + .delete_apikey(&key_hash) + .await?; + Ok(web::Json(Output {})) +} diff --git a/crates/flow-server/src/api/deploy_flow.rs b/crates/flow-server/src/api/deploy_flow.rs new file mode 100644 index 00000000..95b1988b --- /dev/null +++ b/crates/flow-server/src/api/deploy_flow.rs @@ -0,0 +1,29 @@ +use crate::middleware::auth_v1::{Auth, AuthenticatedUser}; + +use super::prelude::*; +use db::pool::DbPool; +use flow::flow_set::{DeploymentId, FlowDeployment}; + +#[derive(Serialize)] +pub struct Output { + pub deployment_id: DeploymentId, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/deploy/{id}") + .wrap(config.cors()) + .route(web::post().to(deploy_flow)) +} + +async fn deploy_flow( + flow_id: web::Path, + user: Auth, + db: web::Data, +) -> Result, Error> { + let mut conn = db.get_user_conn(*user.user_id()).await?; + let deployment = FlowDeployment::from_entrypoint(flow_id.into_inner(), &mut conn) + .await + .map_err(flow::Error::from)?; + let deployment_id = conn.insert_deployment(&deployment).await?; + Ok(web::Json(Output { deployment_id })) +} diff --git a/crates/flow-server/src/api/flow_run/mod.rs b/crates/flow-server/src/api/flow_run/mod.rs new file mode 100644 index 00000000..9989927b --- /dev/null +++ b/crates/flow-server/src/api/flow_run/mod.rs @@ -0,0 +1,80 @@ +use super::prelude::*; +use crate::db_worker::{DBWorker, FlowRunEvent, SubscribeEvents}; +use actix::AsyncContext; +use actix_web_actors::ws; +use flow::event; +use value::json_repr::JsonRepr; + +pub fn service(config: &Config, _db: DbPool) -> impl HttpServiceFactory { + web::scope("/run/{flow_run_id}") + .wrap(config.cors()) + .service(web::resource("/events").route(web::get().to(events_stream))) +} + +async fn events_stream( + flow_run_id: web::Path, + ctx: web::Data, + req: actix_web::HttpRequest, + stream: web::Payload, +) -> Result { + tracing::info!("{}", req.path()); + let resp = ws::start( + EventsWs { + flow_run_id: flow_run_id.into_inner(), + db_worker: ctx.db_worker.clone(), + }, + &req, + stream, + )?; + Ok(resp) +} + +pub struct EventsWs { + flow_run_id: FlowRunId, + db_worker: actix::Addr, +} + +impl actix::Actor for EventsWs { + type Context = ws::WebsocketContext; + + fn started(&mut self, ctx: &mut Self::Context) { + self.db_worker.do_send(SubscribeEvents { + flow_run_id: self.flow_run_id, + ws: ctx.address(), + }); + } +} + +/// Handler for ws::Message message +impl actix::StreamHandler> for EventsWs { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => ctx.text(text), + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + _ => (), + } + } +} + +impl actix::Handler for EventsWs { + type Result = (); + fn handle(&mut self, msg: FlowRunEvent, ctx: &mut Self::Context) -> Self::Result { + let event = msg.0; + match event.event { + event::EventType::FlowStart => {} + event::EventType::FlowFinish { output, .. } => { + let json = + serde_json::to_string(&JsonRepr::new(&value::Value::Map(output))).unwrap(); + ctx.text(json); + } + event::EventType::FlowError { .. } => {} + event::EventType::NodeStart { .. } => {} + event::EventType::NodeOutput { .. } => {} + event::EventType::NodeFinish { .. } => {} + event::EventType::NodeError { .. } => {} + event::EventType::NodeLog { .. } => {} + event::EventType::FlowLog { .. } => {} + } + } +} diff --git a/crates/flow-server/src/api/get_flow_output.rs b/crates/flow-server/src/api/get_flow_output.rs new file mode 100644 index 00000000..495f6f1c --- /dev/null +++ b/crates/flow-server/src/api/get_flow_output.rs @@ -0,0 +1,41 @@ +use super::prelude::*; +use crate::db_worker::{ + flow_run_worker::{FlowRunWorker, WaitFinish}, + FindActor, +}; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/output/{run_id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::get().to(get_flow_output)) +} + +async fn get_flow_output( + run_id: web::Path, + auth: web::ReqData, + db: web::Data, + db_worker: web::Data>, +) -> Result, Error> { + let run_id = run_id.into_inner(); + let conn = db.get_admin_conn().await?; + let run_info = conn.get_flow_run_info(run_id).await?; + let auth = auth.into_inner(); + if !(auth.flow_run_id() == Some(run_id) + || auth.user_id().is_some_and(|user_id| { + run_info.user_id == user_id || run_info.shared_with.contains(&user_id) + })) + { + return Err(Error::custom(StatusCode::NOT_FOUND, "unauthorized")); + } + if let Some(addr) = db_worker + .send(FindActor::::new(run_id)) + .await? + { + addr.send(WaitFinish) + .await? + .map_err(|_| Error::custom(StatusCode::INTERNAL_SERVER_ERROR, "channel closed"))?; + } + let output = conn.get_flow_run_output(run_id).await?; + Ok(web::Json(output)) +} diff --git a/crates/flow-server/src/api/get_info.rs b/crates/flow-server/src/api/get_info.rs new file mode 100644 index 00000000..a772a788 --- /dev/null +++ b/crates/flow-server/src/api/get_info.rs @@ -0,0 +1,24 @@ +use super::prelude::*; +use actix_web::{http::header::ContentType, HttpResponseBuilder}; +use url::Url; + +#[derive(Serialize)] +struct Output { + supabase_url: Url, + anon_key: String, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + let output = Output { + supabase_url: config.supabase.endpoint.url.clone(), + anon_key: config.supabase.anon_key.clone(), + }; + let json: bytes::Bytes = serde_json::to_vec(&output).unwrap().into(); + web::resource("/info").route(web::get().to(move || { + std::future::ready( + HttpResponseBuilder::new(StatusCode::OK) + .insert_header(ContentType::json()) + .body(json.clone()), + ) + })) +} diff --git a/crates/flow-server/src/api/get_signature_request.rs b/crates/flow-server/src/api/get_signature_request.rs new file mode 100644 index 00000000..22f9d0f1 --- /dev/null +++ b/crates/flow-server/src/api/get_signature_request.rs @@ -0,0 +1,89 @@ +use super::prelude::*; +use crate::db_worker::{ + flow_run_worker::{FlowRunWorker, SubscribeEvents}, + user_worker::SigReqExists, + FindActor, UserWorker, +}; +use db::connection::FlowRunInfo; +use flow::flow_run_events::Event; +use flow_lib::context::signer::SignatureRequest; +use futures_util::StreamExt; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/signature_request/{run_id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::get().to(get_signature_request)) +} + +async fn exists_in_user( + db_worker: &actix::Addr, + req_id: i64, + user_id: UserId, +) -> Result { + let user = db_worker + .send(FindActor::::new(user_id)) + .await?; + Ok(match user { + Some(user) => user.send(SigReqExists { id: req_id }).await?, + None => false, + }) +} + +#[allow(dead_code)] +async fn exists( + db_worker: &actix::Addr, + req_id: i64, + run_info: &FlowRunInfo, +) -> Result { + if exists_in_user(db_worker, req_id, run_info.user_id).await? { + return Ok(true); + } + for user_id in &run_info.shared_with { + if exists_in_user(db_worker, req_id, *user_id).await? { + return Ok(true); + } + } + Ok(false) +} + +async fn get_signature_request( + run_id: web::Path, + auth: web::ReqData, + db: web::Data, + db_worker: web::Data>, +) -> Result, Error> { + let run_id = run_id.into_inner(); + let conn = db.get_admin_conn().await?; + let run_info = conn.get_flow_run_info(run_id).await?; + let auth = auth.into_inner(); + if !(auth.flow_run_id() == Some(run_id) + || auth.user_id().is_some_and(|user_id| { + run_info.user_id == user_id || run_info.shared_with.contains(&user_id) + })) + { + return Err(Error::custom(StatusCode::NOT_FOUND, "unauthorized")); + } + if let Some(flow_run) = db_worker + .send(FindActor::::new(run_id)) + .await? + { + let (_, mut events) = flow_run + .send(SubscribeEvents { + tokens: <_>::from([auth]), + }) + .await? + .map_err(|_| Error::custom(StatusCode::INTERNAL_SERVER_ERROR, "channel closed"))?; + while let Some(event) = events.next().await { + if let Event::SignatureRequest(req) = event { + if let Some(req_id) = req.id { + tracing::debug!("{}", req_id); + if exists(&db_worker, req_id, &run_info).await? { + return Ok(web::Json(req)); + } + } + } + } + } + Err(Error::custom(StatusCode::NOT_FOUND, "not found")) +} diff --git a/crates/flow-server/src/api/init_auth.rs b/crates/flow-server/src/api/init_auth.rs new file mode 100644 index 00000000..e2eb6eb8 --- /dev/null +++ b/crates/flow-server/src/api/init_auth.rs @@ -0,0 +1,29 @@ +use super::prelude::*; +use crate::user::SignatureAuth; + +#[derive(Deserialize)] +pub struct Params { + #[serde(with = "utils::serde_bs58")] + pub pubkey: [u8; 32], +} + +#[derive(Serialize)] +pub struct Output { + pub msg: String, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/init") + .wrap(config.anon_key()) + .wrap(config.cors()) + .route(web::post().to(init_auth)) +} + +async fn init_auth( + params: web::Json, + sig: web::Data, +) -> Result, Error> { + let Params { pubkey } = params.into_inner(); + let msg = sig.init_login(chrono::Utc::now().timestamp(), &pubkey); + Ok(web::Json(Output { msg })) +} diff --git a/crates/flow-server/src/api/kvstore/create_store.rs b/crates/flow-server/src/api/kvstore/create_store.rs new file mode 100644 index 00000000..a939874b --- /dev/null +++ b/crates/flow-server/src/api/kvstore/create_store.rs @@ -0,0 +1,71 @@ +use super::super::prelude::*; +use db::SqlState; +use once_cell::sync::Lazy; +use regex::Regex; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/create_store") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(create_store)) +} + +#[derive(Deserialize)] +struct Params { + store: String, +} + +#[derive(ThisError, Debug)] +enum StoreNameError { + #[error("max store name length is {}", .max)] + MaxLength { max: usize }, + #[error("store name must match the regex: '{}'", .regex)] + WrongFormat { regex: &'static str }, +} + +fn check_store_name(s: &str) -> Result<(), StoreNameError> { + const MAX_LEN: usize = 120; + if s.len() > MAX_LEN { + return Err(StoreNameError::MaxLength { max: MAX_LEN }); + } + + const RE_STR: &str = r#"^[a-zA-Z][a-zA-Z0-9_-]*$"#; + static RE: Lazy = Lazy::new(|| Regex::new(RE_STR).unwrap()); + if !RE.is_match(s) { + return Err(StoreNameError::WrongFormat { regex: RE_STR }); + } + Ok(()) +} + +fn process_error(e: DbError) -> Error { + if let DbError::Execute { error, context, .. } = &e { + if *context == "insert kvstore_metadata" + && error.code() == Some(&SqlState::UNIQUE_VIOLATION) + { + return Error::custom(StatusCode::PRECONDITION_FAILED, "database already exists"); + } + + if *context == "update user_quotas" + && error.to_string().contains("unexpected number of rows") + { + return Error::custom(StatusCode::FORBIDDEN, "user's storage limit exceeded"); + } + } + + e.into() +} + +async fn create_store( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let params = params.into_inner(); + check_store_name(¶ms.store).map_err(|e| Error::custom(StatusCode::BAD_REQUEST, e))?; + db.get_admin_conn() + .await? + .create_store(&user.user_id, ¶ms.store) + .await + .map_err(process_error)?; + Ok(web::Json(Success)) +} diff --git a/crates/flow-server/src/api/kvstore/delete_item.rs b/crates/flow-server/src/api/kvstore/delete_item.rs new file mode 100644 index 00000000..405bbeae --- /dev/null +++ b/crates/flow-server/src/api/kvstore/delete_item.rs @@ -0,0 +1,33 @@ +use super::super::prelude::*; +use value::Value; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/delete_item") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(write_item)) +} + +#[derive(Deserialize)] +struct Params { + store: String, + key: String, +} + +#[derive(Serialize)] +struct Output { + old_value: Value, +} + +async fn write_item( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let old_value = db + .get_admin_conn() + .await? + .remove_item(&user.user_id, ¶ms.store, ¶ms.key) + .await?; + Ok(web::Json(Output { old_value })) +} diff --git a/crates/flow-server/src/api/kvstore/delete_store.rs b/crates/flow-server/src/api/kvstore/delete_store.rs new file mode 100644 index 00000000..6e42bb5e --- /dev/null +++ b/crates/flow-server/src/api/kvstore/delete_store.rs @@ -0,0 +1,31 @@ +use super::super::prelude::*; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/delete_store") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(delete_store)) +} + +#[derive(Deserialize)] +struct Params { + store: String, +} + +async fn delete_store( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let params = params.into_inner(); + let success = db + .get_admin_conn() + .await? + .delete_store(&user.user_id, ¶ms.store) + .await?; + if success { + Ok(web::Json(Success)) + } else { + Err(Error::custom(StatusCode::NOT_FOUND, "store not found")) + } +} diff --git a/crates/flow-server/src/api/kvstore/mod.rs b/crates/flow-server/src/api/kvstore/mod.rs new file mode 100644 index 00000000..2a354d37 --- /dev/null +++ b/crates/flow-server/src/api/kvstore/mod.rs @@ -0,0 +1,6 @@ +pub mod create_store; +pub mod delete_store; + +pub mod delete_item; +pub mod read_item; +pub mod write_item; diff --git a/crates/flow-server/src/api/kvstore/read_item.rs b/crates/flow-server/src/api/kvstore/read_item.rs new file mode 100644 index 00000000..5c847a1a --- /dev/null +++ b/crates/flow-server/src/api/kvstore/read_item.rs @@ -0,0 +1,36 @@ +use super::super::prelude::*; +use value::Value; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/read_item") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(read_item)) +} + +#[derive(Deserialize)] +struct Params { + store: String, + key: String, +} + +#[derive(Serialize)] +struct Output { + value: Value, +} + +async fn read_item( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let opt = db + .get_user_conn(user.user_id) + .await? + .read_item(¶ms.store, ¶ms.key) + .await?; + match opt { + Some(value) => Ok(web::Json(Output { value })), + None => Err(Error::custom(StatusCode::NOT_FOUND, "not found")), + } +} diff --git a/crates/flow-server/src/api/kvstore/write_item.rs b/crates/flow-server/src/api/kvstore/write_item.rs new file mode 100644 index 00000000..b31d42ed --- /dev/null +++ b/crates/flow-server/src/api/kvstore/write_item.rs @@ -0,0 +1,49 @@ +use super::super::prelude::*; +use value::Value; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/write_item") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(write_item)) +} + +#[derive(Deserialize)] +struct Params { + store: String, + key: String, + value: Value, +} + +#[derive(Serialize)] +struct Output { + old_value: Option, +} + +fn parse_error(e: DbError) -> Error { + if let DbError::Execute { error, context, .. } = &e { + let name = error.as_db_error().and_then(|e| e.constraint()); + if name == Some("kvstore_user_id_store_name_fkey") { + return Error::custom(StatusCode::NOT_FOUND, "store not found"); + } + + if *context == "update user_quotas" { + return Error::custom(StatusCode::FORBIDDEN, "user's storage limit exceeded"); + } + } + e.into() +} + +async fn write_item( + params: web::Json, + user: web::ReqData, + db: web::Data, +) -> Result, Error> { + let old_value = db + .get_admin_conn() + .await? + .insert_or_replace_item(&user.user_id, ¶ms.store, ¶ms.key, ¶ms.value) + .await + .map_err(parse_error)?; + Ok(web::Json(Output { old_value })) +} diff --git a/crates/flow-server/src/api/mod.rs b/crates/flow-server/src/api/mod.rs new file mode 100644 index 00000000..cf6ac840 --- /dev/null +++ b/crates/flow-server/src/api/mod.rs @@ -0,0 +1,63 @@ +pub mod claim_token; +pub mod confirm_auth; +pub mod init_auth; + +pub mod upsert_wallet; + +pub mod get_flow_output; +pub mod get_signature_request; + +pub mod start_flow; +pub mod start_flow_shared; +pub mod start_flow_unverified; +pub mod stop_flow; + +pub mod clone_flow; + +pub mod submit_signature; + +pub mod apikey_info; +pub mod create_apikey; +pub mod delete_apikey; + +pub mod kvstore; + +pub mod auth_proxy; +pub mod db_push_logs; +pub mod db_rpc; +pub mod ws_auth_proxy; + +pub mod data_export; +pub mod data_import; + +pub mod get_info; + +pub mod deploy_flow; +pub mod start_deployment; + +pub mod prelude { + pub use crate::{db_worker::DBWorker, error::Error, middleware::auth, Config}; + pub use actix_web::{dev::HttpServiceFactory, http::StatusCode, web}; + pub use db::{ + connection::{UserConnection, UserConnectionTrait}, + pool::{DbPool, RealDbPool}, + Error as DbError, + }; + pub use flow_lib::{FlowId, FlowRunId, UserId, ValueSet}; + pub use serde::{Deserialize, Serialize}; + pub use thiserror::Error as ThisError; + + pub struct Success; + + impl Serialize for Success { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut s = s.serialize_struct("Success", 1)?; + s.serialize_field("success", &true)?; + s.end() + } + } +} diff --git a/crates/flow-server/src/api/start_deployment.rs b/crates/flow-server/src/api/start_deployment.rs new file mode 100644 index 00000000..8d848f4a --- /dev/null +++ b/crates/flow-server/src/api/start_deployment.rs @@ -0,0 +1,99 @@ +use super::prelude::*; +use crate::{ + db_worker::{user_worker::StartDeployment, GetUserWorker}, + middleware::{ + auth_v1::{Auth2, AuthenticatedUser, Unverified}, + optional, + }, + user::{SignatureAuth, SupabaseAuth}, +}; +use flow::flow_set::{DeploymentId, FlowStarter, StartFlowDeploymentOptions}; +use flow_lib::solana::Pubkey; + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub enum Query { + FlowTag { flow: FlowId, tag: String }, + Id { id: DeploymentId }, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Params { + inputs: Option, +} + +#[derive(Serialize)] +pub struct Output { + pub flow_run_id: FlowRunId, + pub token: String, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/start") + .wrap(config.cors()) + .route(web::post().to(start_deployment)) +} + +async fn start_deployment( + query: web::Query, + params: actix_web::Result>, + user: Auth2, + db: web::Data, + db_worker: web::Data>, + sup: web::Data, + sig: web::Data, +) -> actix_web::Result> { + let params = optional(params)?; + let mut starter = match &user { + Auth2::One(user) => FlowStarter { + user_id: *user.user_id(), + pubkey: Pubkey::new_from_array(*user.pubkey()), + authenticated: true, + }, + Auth2::Two(unverified) => FlowStarter { + user_id: UserId::nil(), + pubkey: Pubkey::new_from_array(*unverified.pubkey()), + authenticated: false, + }, + }; + let conn = db.get_user_conn(starter.user_id).await?; + let id = match query.into_inner() { + Query::FlowTag { flow, tag } => conn.get_deployment_id_from_tag(&flow, &tag).await?, + Query::Id { id } => id, + }; + let mut deployment = conn.get_deployment(&id).await?; + + let conn = db.get_user_conn(deployment.user_id).await?; + deployment.flows = conn.get_deployment_flows(&id).await?; + deployment.wallets_id = conn.get_deployment_wallets(&id).await?; + + if starter.user_id.is_nil() { + starter.user_id = sup.get_or_create_user(&starter.pubkey.to_bytes()).await?.0; + } + + let owner = deployment.user_id; + let owner_worker = db_worker + .send(GetUserWorker { user_id: owner }) + .await + .map_err(Error::from)?; + let options = StartFlowDeploymentOptions { + inputs: params + .map(|p| p.0.inputs.unwrap_or_default()) + .unwrap_or_default(), + starter, + }; + tracing::debug!("{:?}", options); + let flow_run_id = owner_worker + .send(StartDeployment { + deployment, + options, + }) + .await + .map_err(Error::from)??; + + Ok(web::Json(Output { + flow_run_id, + token: sig.flow_run_token(flow_run_id), + })) +} diff --git a/crates/flow-server/src/api/start_flow.rs b/crates/flow-server/src/api/start_flow.rs new file mode 100644 index 00000000..ef1df8db --- /dev/null +++ b/crates/flow-server/src/api/start_flow.rs @@ -0,0 +1,74 @@ +use super::prelude::*; +use crate::db_worker::{user_worker::StartFlowFresh, GetUserWorker}; +use db::pool::DbPool; +use flow_lib::config::client::PartialConfig; +use hashbrown::HashMap; +use value::Value; + +#[derive(Deserialize)] +pub struct Params { + #[serde(default)] + pub inputs: HashMap, + pub partial_config: Option, + #[serde(default)] + pub environment: HashMap, + #[serde(default)] + pub output_instructions: bool, +} + +#[derive(Serialize)] +pub struct Output { + pub flow_run_id: FlowRunId, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/start/{id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(start_flow)) +} + +async fn start_flow( + flow_id: web::Path, + params: Option>, + user: web::ReqData, + db_worker: web::Data>, +) -> Result, Error> { + let flow_id = flow_id.into_inner(); + let user = user.into_inner(); + let (inputs, partial_config, environment, output_instructions) = params + .map( + |web::Json(Params { + inputs, + partial_config, + environment, + output_instructions, + })| (inputs, partial_config, environment, output_instructions), + ) + .unwrap_or_default(); + let inputs = inputs.into_iter().collect::(); + + if let Some(partial_config) = &partial_config { + tracing::debug!("partial config: {:?}", partial_config); + } + + let flow_run_id = db_worker + .send(GetUserWorker { + user_id: user.user_id, + }) + .await? + .send(StartFlowFresh { + user: flow_lib::User { id: user.user_id }, + flow_id, + input: inputs, + partial_config, + environment, + output_instructions, + action_identity: None, + action_config: None, + fees: Vec::new(), + }) + .await??; + + Ok(web::Json(Output { flow_run_id })) +} diff --git a/crates/flow-server/src/api/start_flow_shared.rs b/crates/flow-server/src/api/start_flow_shared.rs new file mode 100644 index 00000000..65e991da --- /dev/null +++ b/crates/flow-server/src/api/start_flow_shared.rs @@ -0,0 +1,79 @@ +use super::prelude::*; +use crate::db_worker::{user_worker::StartFlowShared, GetUserWorker}; +use db::pool::DbPool; +use hashbrown::HashMap; +use value::Value; + +#[derive(Deserialize)] +pub struct Params { + #[serde(default)] + pub inputs: HashMap, + #[serde(default)] + pub output_instructions: bool, +} + +#[derive(Serialize)] +pub struct Output { + pub flow_run_id: FlowRunId, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/start_shared/{id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(start_flow_shared)) +} + +async fn start_flow_shared( + flow_id: web::Path, + params: Option>, + user: web::ReqData, + db_worker: web::Data>, + db: web::Data, +) -> Result, Error> { + let flow_id = flow_id.into_inner(); + let user = user.into_inner(); + let (inputs, output_instructions) = params + .map( + |web::Json(Params { + inputs, + output_instructions, + })| (inputs, output_instructions), + ) + .unwrap_or_default(); + let inputs = inputs.into_iter().collect::(); + + let flow = db + .get_user_conn(user.user_id) + .await? + .get_flow_info(flow_id) + .await?; + if !flow.start_shared { + return Err(Error::custom(StatusCode::FORBIDDEN, "not allowed")); + } + + let starter = db_worker + .send(GetUserWorker { + user_id: user.user_id, + }) + .await?; + let owner = db_worker + .send(GetUserWorker { + user_id: flow.user_id, + }) + .await?; + + let flow_run_id = owner + .send(StartFlowShared { + flow_id, + input: inputs, + output_instructions, + action_identity: None, + action_config: None, + fees: Vec::new(), + started_by: (user.user_id, starter), + }) + .await??; + + Ok(web::Json(Output { flow_run_id })) +} diff --git a/crates/flow-server/src/api/start_flow_unverified.rs b/crates/flow-server/src/api/start_flow_unverified.rs new file mode 100644 index 00000000..66f8960e --- /dev/null +++ b/crates/flow-server/src/api/start_flow_unverified.rs @@ -0,0 +1,93 @@ +use super::prelude::*; +use crate::{ + db_worker::{user_worker::StartFlowShared, GetUserWorker}, + user::{SignatureAuth, SupabaseAuth}, +}; +use db::pool::DbPool; +use flow_lib::solana::{Pubkey, SolanaActionConfig}; +use hashbrown::HashMap; +use serde_with::{serde_as, DisplayFromStr}; +use value::Value; + +#[serde_as] +#[derive(Default, Deserialize, Debug)] +pub struct Params { + #[serde(default)] + pub inputs: HashMap, + #[serde(default)] + pub output_instructions: bool, + #[serde(default, with = "value::pubkey::opt")] + pub action_identity: Option, + pub action_config: Option, + #[serde(default)] + #[serde_as(as = "Vec<(DisplayFromStr, _)>")] + pub fees: Vec<(Pubkey, u64)>, +} + +#[derive(Serialize)] +pub struct Output { + pub flow_run_id: FlowRunId, + pub token: String, +} + +pub fn service( + config: &Config, + db: DbPool, + sup: web::Data, +) -> impl HttpServiceFactory { + web::resource("/start_unverified/{id}") + .app_data(sup) + .app_data(web::Data::new(config.signature_auth())) + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(start_flow_unverified)) +} + +async fn start_flow_unverified( + flow_id: web::Path, + params: Option>, + user: web::ReqData, + db_worker: web::Data>, + sup: web::Data, + db: web::Data, + sig: web::Data, +) -> Result, Error> { + let flow_id = flow_id.into_inner(); + let user = user.into_inner(); + let params = params.map(|params| params.0).unwrap_or_default(); + let inputs = params.inputs.into_iter().collect::(); + + let flow = db + .get_user_conn(UserId::nil()) + .await? + .get_flow_info(flow_id) + .await?; + if !flow.start_shared && !flow.start_unverified { + return Err(Error::custom(StatusCode::FORBIDDEN, "not allowed")); + } + + let user_id = sup.get_or_create_user(&user.pubkey).await?.0; + + let starter = db_worker.send(GetUserWorker { user_id }).await?; + let owner = db_worker + .send(GetUserWorker { + user_id: flow.user_id, + }) + .await?; + + let flow_run_id = owner + .send(StartFlowShared { + flow_id, + input: inputs, + output_instructions: params.output_instructions, + action_identity: params.action_identity, + action_config: params.action_config, + fees: params.fees, + started_by: (user_id, starter), + }) + .await??; + + let token = sig.flow_run_token(flow_run_id); + + Ok(web::Json(Output { flow_run_id, token })) +} diff --git a/crates/flow-server/src/api/stop_flow.rs b/crates/flow-server/src/api/stop_flow.rs new file mode 100644 index 00000000..e3c75062 --- /dev/null +++ b/crates/flow-server/src/api/stop_flow.rs @@ -0,0 +1,46 @@ +use super::prelude::*; +use crate::db_worker::{ + flow_run_worker::{FlowRunWorker, StopError, StopFlow}, + FindActor, +}; + +#[derive(Deserialize)] +pub struct Params { + #[serde(default)] + pub timeout_millies: u32, + pub reason: Option, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/stop/{id}") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(stop_flow)) +} + +async fn stop_flow( + id: web::Path, + params: Option>, + user: web::ReqData, + db_worker: web::Data>, +) -> Result, StopError> { + let id = id.into_inner(); + let user = user.into_inner(); + let (timeout_millies, reason) = params + .map(|p| (p.0.timeout_millies, p.0.reason)) + .unwrap_or_default(); + + db_worker + .send(FindActor::::new(id)) + .await? + .ok_or(StopError::NotFound)? + .send(StopFlow { + user_id: user.user_id, + run_id: id, + timeout_millies, + reason, + }) + .await??; + + Ok(web::Json(Success)) +} diff --git a/crates/flow-server/src/api/submit_signature.rs b/crates/flow-server/src/api/submit_signature.rs new file mode 100644 index 00000000..5463e1b4 --- /dev/null +++ b/crates/flow-server/src/api/submit_signature.rs @@ -0,0 +1,38 @@ +use super::prelude::*; +use crate::db_worker::user_worker::{SubmitError, SubmitSignature}; +use bytes::Bytes; +use serde_with::{base64::Base64, serde_as}; + +#[serde_as] +#[derive(Deserialize)] +pub struct Params { + id: i64, + #[serde(with = "utils::serde_bs58")] + signature: [u8; 64], + #[serde_as(as = "Option")] + new_msg: Option, +} + +pub fn service(config: &Config) -> impl HttpServiceFactory { + web::resource("/submit") + .wrap(config.cors()) + .route(web::post().to(submit_signature)) +} + +async fn submit_signature( + params: web::Json, + db_worker: web::Data>, +) -> Result, SubmitError> { + let params = params.into_inner(); + + db_worker + .send(SubmitSignature { + id: params.id, + user_id: UserId::nil(), + signature: params.signature, + new_msg: params.new_msg, + }) + .await??; + + Ok(web::Json(Success)) +} diff --git a/crates/flow-server/src/api/upsert_wallet.rs b/crates/flow-server/src/api/upsert_wallet.rs new file mode 100644 index 00000000..46fd4950 --- /dev/null +++ b/crates/flow-server/src/api/upsert_wallet.rs @@ -0,0 +1,27 @@ +use super::prelude::*; +use crate::user::{SupabaseAuth, UpsertWalletBody}; +use serde_json::value::RawValue; + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + web::resource("/upsert") + .wrap(config.all_auth(db)) + .wrap(config.cors()) + .route(web::post().to(upsert_wallet)) +} + +async fn upsert_wallet( + params: web::Json, + token: web::ReqData, + sup: web::Data, +) -> Result<(web::Json>, StatusCode), Error> { + let jwt = token + .jwt + .clone() + .ok_or_else(|| Error::custom(StatusCode::BAD_REQUEST, "must be called with user's JWT"))?; + let (status, result) = sup + .upsert_wallet(&jwt, params.0) + .await + .map_err(|error| Error::custom(StatusCode::INTERNAL_SERVER_ERROR, error))?; + let status = actix_web::http::StatusCode::from_u16(status.as_u16()).unwrap(); + Ok((web::Json(result), status)) +} diff --git a/crates/flow-server/src/api/ws_auth_proxy.rs b/crates/flow-server/src/api/ws_auth_proxy.rs new file mode 100644 index 00000000..e0f37fb3 --- /dev/null +++ b/crates/flow-server/src/api/ws_auth_proxy.rs @@ -0,0 +1,32 @@ +use super::prelude::*; +use crate::{auth::ApiAuth, middleware::auth::TokenType}; + +#[derive(Serialize, Deserialize)] +pub struct Params { + pub token: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub payload: TokenType, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + let auth = web::Data::new(config.all_auth(db)); + web::resource("/ws_auth") + .app_data(auth) + .wrap(config.cors()) + .route(web::post().to(ws_auth)) +} + +async fn ws_auth( + params: web::Json, + auth: web::Data, +) -> Result, Error> { + let payload = (*auth) + .clone() + .ws_authenticate(params.into_inner().token) + .await + .map_err(|error| Error::custom(StatusCode::UNAUTHORIZED, error))?; + Ok(web::Json(Output { payload })) +} diff --git a/crates/flow-server/src/db_worker/flow_run_worker.rs b/crates/flow-server/src/db_worker/flow_run_worker.rs new file mode 100644 index 00000000..bb168d12 --- /dev/null +++ b/crates/flow-server/src/db_worker/flow_run_worker.rs @@ -0,0 +1,434 @@ +use super::{ + messages::{SubscribeError, SubscriptionID}, + user_worker::SigReqEvent, + CopyIn, Counter, DBWorker, SystemShutdown, +}; +use crate::{api::prelude::auth::TokenType, error::ErrorBody}; +use actix::{ + fut::wrap_future, Actor, ActorContext, ActorFutureExt, AsyncContext, ResponseActFuture, + ResponseFuture, StreamHandler, WrapFuture, +}; +use actix_web::http::StatusCode; +use db::{pool::DbPool, FlowRunLogsRow}; +use flow::{ + flow_graph::StopSignal, + flow_run_events::{ + self, Event, FlowError, FlowFinish, FlowLog, FlowStart, NodeError, NodeFinish, NodeLog, + NodeOutput, NodeStart, + }, +}; +use flow_lib::{FlowRunId, UserId}; +use futures_channel::mpsc; +use futures_util::{stream::BoxStream, FutureExt, StreamExt}; +use hashbrown::{HashMap, HashSet}; +use thiserror::Error as ThisError; +use tokio::sync::broadcast::{self, error::RecvError}; +use utils::address_book::ManagableActor; +use value::Value; + +pub struct FlowRunWorker { + user_id: UserId, + shared_with: Vec, + run_id: FlowRunId, + stop_signal: StopSignal, + stop_shared_signal: StopSignal, + counter: Counter, + tx: mpsc::UnboundedSender, + subs: HashMap, + all_events: Vec, + done_tx: broadcast::Sender<()>, +} + +impl Actor for FlowRunWorker { + type Context = actix::Context; + + fn started(&mut self, _: &mut Self::Context) { + tracing::debug!("started FlowRunWorker {}", self.run_id); + } + + fn stopped(&mut self, _: &mut Self::Context) { + tracing::debug!("stopped FlowRunWorker {}", self.run_id); + self.stop_signal + .stop(0, Some("stopping FlowRunWorker".to_owned())); + } +} + +impl ManagableActor for FlowRunWorker { + type ID = FlowRunId; + + fn id(&self) -> Self::ID { + self.run_id + } +} + +pub struct WaitFinish; + +impl actix::Message for WaitFinish { + type Result = Result<(), RecvError>; +} + +impl actix::Handler for FlowRunWorker { + type Result = ResponseFuture<::Result>; + fn handle(&mut self, _: WaitFinish, _: &mut Self::Context) -> Self::Result { + let mut rx = self.done_tx.subscribe(); + async move { rx.recv().await }.boxed() + } +} + +impl actix::Handler for FlowRunWorker { + type Result = (); + fn handle(&mut self, msg: SigReqEvent, ctx: &mut Self::Context) -> Self::Result { + StreamHandler::handle(self, Event::SignatureRequest(msg.0), ctx) + } +} + +impl actix::Handler for FlowRunWorker { + type Result = ResponseActFuture::Result>; + fn handle(&mut self, msg: SystemShutdown, _: &mut Self::Context) -> Self::Result { + let mut rx = self.done_tx.subscribe(); + let stop_signal = self.stop_signal.clone(); + let id = self.run_id; + Box::pin( + async move { + let res = tokio::time::timeout(msg.timeout, rx.recv()).await; + if res.is_err() { + tracing::warn!("force stopping FlowRunWorker {}", id); + stop_signal.stop(0, Some("restarting server".to_owned())); + rx.recv().await.ok(); + } + } + .into_actor(&*self) + .map(|_, _, ctx| ctx.stop()), + ) + } +} + +struct Subscription { + tx: mpsc::UnboundedSender, +} + +pub struct SubscribeEvents { + pub tokens: HashSet, +} + +impl actix::Message for SubscribeEvents { + type Result = Result<(SubscriptionID, mpsc::UnboundedReceiver), SubscribeError>; +} + +impl actix::Handler for FlowRunWorker { + type Result = ::Result; + + fn handle(&mut self, msg: SubscribeEvents, _: &mut Self::Context) -> Self::Result { + let can_read = msg + .tokens + .iter() + .any(|token| token.is_user(self.user_id) || token.is_flow_run(self.run_id)); + if !can_read { + return Err(SubscribeError::Unauthorized); + } + + let stream_id = self.counter.next(); + let (tx, rx) = mpsc::unbounded(); + for item in self.all_events.iter().cloned() { + tx.unbounded_send(item).unwrap(); + } + self.subs.insert(stream_id, Subscription { tx }); + + Ok((stream_id, rx)) + } +} + +pub struct StopFlow { + pub user_id: UserId, + pub run_id: FlowRunId, + pub timeout_millies: u32, + pub reason: Option, +} + +impl actix::Message for StopFlow { + type Result = Result<(), StopError>; +} + +#[derive(ThisError, Debug)] +pub enum StopError { + #[error("unauthorized: {}", user_id)] + Unauthorized { user_id: UserId }, + #[error("not found")] + NotFound, + #[error(transparent)] + Mailbox(#[from] actix::MailboxError), + #[error(transparent)] + Worker(#[from] flow_lib::BoxError), +} + +impl actix_web::ResponseError for StopError { + fn status_code(&self) -> StatusCode { + match self { + StopError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, + StopError::NotFound => StatusCode::NOT_FOUND, + StopError::Mailbox(_) => StatusCode::INTERNAL_SERVER_ERROR, + StopError::Worker(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +impl actix::Handler for FlowRunWorker { + type Result = Result<(), StopError>; + + fn handle(&mut self, msg: StopFlow, _: &mut Self::Context) -> Self::Result { + if self.user_id != msg.user_id { + if self.shared_with.contains(&msg.user_id) { + self.stop_shared_signal + .stop(msg.timeout_millies, msg.reason); + return Ok(()); + } + return Err(StopError::Unauthorized { + user_id: msg.user_id, + }); + } + if self.run_id != msg.run_id { + return Err(StopError::NotFound); + } + self.stop_signal.stop(msg.timeout_millies, msg.reason); + Ok(()) + } +} + +impl FlowRunWorker { + #[allow(clippy::too_many_arguments)] + pub fn new( + run_id: FlowRunId, + user_id: UserId, + shared_with: Vec, + counter: Counter, + stream: BoxStream<'static, flow_run_events::Event>, + db: DbPool, + root: actix::Addr, + stop_signal: StopSignal, + stop_shared_signal: StopSignal, + ctx: &mut actix::Context, + ) -> Self { + let (tx, rx) = mpsc::unbounded(); + let fut = save_to_db(user_id, run_id, rx, db, root.recipient()); + ctx.spawn(wrap_future::<_, Self>(fut).map(move |_, act, _| { + act.done_tx.send(()).ok(); + })); + ctx.add_stream(stream); + + FlowRunWorker { + user_id, + shared_with, + run_id, + stop_signal, + stop_shared_signal, + counter, + tx, + done_tx: broadcast::channel::<()>(1).0, + subs: HashMap::new(), + all_events: Vec::new(), + } + } + + pub fn stop_signal(&self) -> StopSignal { + self.stop_signal.clone() + } + + pub fn stop_shared_signal(&self) -> StopSignal { + self.stop_shared_signal.clone() + } +} + +impl StreamHandler for FlowRunWorker { + fn handle(&mut self, item: Event, _: &mut Self::Context) { + let is_finished = matches!(&item, Event::FlowFinish(_)); + + self.tx.unbounded_send(item.clone()).ok(); + if is_finished { + self.tx.close_channel(); + } + + self.subs.retain(|_, sub| { + let retain = sub.tx.unbounded_send(item.clone()).is_ok() && !is_finished; + if is_finished { + sub.tx.close_channel(); + } + retain + }); + self.all_events.push(item); + } + + fn finished(&mut self, _: &mut Self::Context) { + self.tx.close_channel(); + } +} + +fn log_error(error: E) { + tracing::error!("{}, dropping event.", error); +} + +/// Max 16 KB for each fields +const MAX_SIZE: usize = 32 * 1024; + +/// Strip long values to save data +fn strip(value: Value) -> Value { + match value { + Value::String(s) if s.len() > MAX_SIZE => "VALUE TOO LARGE".into(), + Value::Bytes(s) if s.len() > MAX_SIZE => "VALUE TOO LARGE".into(), + Value::Array(mut s) => { + for v in &mut s { + *v = strip(std::mem::take(v)); + } + Value::Array(s) + } + Value::Map(mut s) => { + for v in s.values_mut() { + *v = strip(std::mem::take(v)); + } + Value::Map(s) + } + _ => value, + } +} + +async fn save_to_db( + user_id: UserId, + run_id: FlowRunId, + rx: mpsc::UnboundedReceiver, + db: DbPool, + tx: actix::Recipient>>, +) { + let mut log_index = 0i32; + const CHUNK_SIZE: usize = 16; + let mut chunks = rx.ready_chunks(CHUNK_SIZE); + while let Some(events) = chunks.next().await { + let mut logs: Vec = Vec::new(); + let conn = match db.get_user_conn(user_id).await { + Ok(conn) => conn, + Err(error) => { + tracing::error!( + "could not get DB connection, dropping events. detail: {}", + error + ); + continue; + } + }; + for event in events { + match event { + Event::FlowStart(FlowStart { time }) => { + conn.set_start_time(&run_id, &time) + .await + .map_err(log_error) + .ok(); + } + Event::FlowError(FlowError { error, .. }) => { + conn.push_flow_error(&run_id, error.as_str()) + .await + .map_err(log_error) + .ok(); + } + Event::FlowFinish(FlowFinish { + time, + not_run, + output, + }) => { + conn.set_run_result(&run_id, &time, ¬_run, &output) + .await + .map_err(log_error) + .ok(); + } + Event::NodeStart(NodeStart { + time, + node_id, + times, + input, + }) => { + conn.new_node_run(&run_id, &node_id, &(times as i32), &time, &strip(input)) + .await + .map_err(log_error) + .ok(); + } + Event::NodeOutput(NodeOutput { + node_id, + times, + output, + .. + }) => { + conn.save_node_output(&run_id, &node_id, &(times as i32), &strip(output)) + .await + .map_err(log_error) + .ok(); + } + Event::NodeError(NodeError { + node_id, + times, + error, + .. + }) => { + conn.push_node_error(&run_id, &node_id, &(times as i32), &error) + .await + .map_err(log_error) + .ok(); + } + Event::NodeFinish(NodeFinish { + time, + node_id, + times, + }) => { + conn.set_node_finish(&run_id, &node_id, &(times as i32), &time) + .await + .map_err(log_error) + .ok(); + } + Event::NodeLog(NodeLog { + time, + node_id, + times, + level, + module, + content, + }) => { + logs.push(FlowRunLogsRow { + user_id, + flow_run_id: run_id, + log_index, + node_id: Some(node_id), + times: Some(times as i32), + time, + log_level: level.to_string(), + content, + module, + }); + log_index += 1; + } + Event::FlowLog(FlowLog { + time, + level, + module, + content, + }) => { + logs.push(FlowRunLogsRow { + user_id, + flow_run_id: run_id, + log_index, + node_id: None, + times: None, + time, + log_level: level.to_string(), + content, + module, + }); + log_index += 1; + } + Event::SignatureRequest(_) => {} + } + } + drop(conn); + if !logs.is_empty() && tx.send(CopyIn(logs)).await.is_err() { + tracing::error!("failed to send to DBWorker, dropping event.") + } + } +} diff --git a/crates/flow-server/src/db_worker/messages.rs b/crates/flow-server/src/db_worker/messages.rs new file mode 100644 index 00000000..9bec3bbd --- /dev/null +++ b/crates/flow-server/src/db_worker/messages.rs @@ -0,0 +1,43 @@ +use flow_lib::{config::client::ClientConfig, FlowId, FlowRunId, ValueSet}; +use thiserror::Error as ThisError; +use uuid::Uuid; + +pub struct GetFlowConfig { + pub user_id: Uuid, + pub flow_id: FlowId, +} + +impl actix::Message for GetFlowConfig { + type Result = Result; +} + +pub struct StartFlow { + pub user_id: Uuid, + pub flow_id: FlowId, + pub input: ValueSet, +} + +impl actix::Message for StartFlow { + type Result = Result; +} + +pub type SubscriptionID = u64; + +#[derive(ThisError, Debug)] +pub enum SubscribeError { + #[error("unauthorized")] + Unauthorized, + #[error("not found")] + NotFound, + #[error(transparent)] + MailBox(#[from] actix::MailboxError), +} + +/// Sent after a stream finished +pub struct Finished { + pub stream_id: SubscriptionID, +} + +impl actix::Message for Finished { + type Result = (); +} diff --git a/crates/flow-server/src/db_worker/mod.rs b/crates/flow-server/src/db_worker/mod.rs new file mode 100644 index 00000000..7021bfcf --- /dev/null +++ b/crates/flow-server/src/db_worker/mod.rs @@ -0,0 +1,385 @@ +use crate::{flow_logs, Config}; +use actix::{ + fut::wrap_future, Actor, ActorContext, ActorFutureExt, Arbiter, AsyncContext, Context, + ResponseActFuture, ResponseFuture, WrapFuture, +}; +use db::{ + pool::{DbPool, ProxiedDbPool, RealDbPool}, + FlowRunLogsRow, +}; +use flow::flow_run_events::{EventSender, DEFAULT_LOG_FILTER, FLOW_SPAN_NAME}; +use flow_lib::{config::Endpoints, context::get_jwt, BoxError, FlowRunId, UserId}; +use futures_channel::mpsc; +use futures_util::{FutureExt, StreamExt}; +use std::{ + sync::{atomic::AtomicU64, Arc}, + time::Duration, +}; +use tokio::sync::broadcast; +use tracing::{level_filters::LevelFilter, Span}; +use tracing_subscriber::EnvFilter; +use utils::address_book::{AddressBook, AlreadyStarted, ManagableActor}; + +pub mod flow_run_worker; +pub mod messages; +pub mod signer; +pub mod token_worker; +pub mod user_worker; + +pub use flow_run_worker::FlowRunWorker; +pub use user_worker::UserWorker; + +use self::{ + token_worker::{LoginWithAdminCred, TokenWorker}, + user_worker::{SubmitError, SubmitSignature}, +}; + +#[derive(Clone, Default)] +pub struct Counter { + inner: Arc, +} + +impl Counter { + pub fn next(&self) -> u64 { + self.inner + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } +} + +pub struct DBWorker { + db: DbPool, + endpoints: Endpoints, + /// All actors in the system + actors: AddressBook, + counter: Counter, + tracing_data: flow_logs::Map, + tx: mpsc::UnboundedSender>, + done_tx: broadcast::Sender<()>, +} + +impl DBWorker { + pub fn new( + db: DbPool, + config: &Config, + actors: AddressBook, + tracing_data: flow_logs::Map, + ctx: &mut actix::Context, + ) -> Self { + let (tx, rx) = mpsc::unbounded(); + match &db { + DbPool::Real(db) => ctx.spawn(wrap_future::<_, Self>(db_copy_in(rx, db.clone())).map( + |_, act, _| { + act.done_tx.send(()).ok(); + }, + )), + DbPool::Proxied(db) => ctx.spawn( + wrap_future::<_, Self>(db_copy_in_proxied(rx, db.clone())).map(|_, act, _| { + act.done_tx.send(()).ok(); + }), + ), + }; + + Self { + db, + endpoints: config.endpoints(), + actors, + counter: Counter::default(), + tx, + tracing_data, + done_tx: broadcast::channel(1).0, + } + } +} + +impl Actor for DBWorker { + type Context = actix::Context; + + fn started(&mut self, _: &mut Self::Context) { + tracing::info!("started DBWorker"); + } + + fn stopped(&mut self, _: &mut Self::Context) { + tracing::info!("stopped DBWorker"); + } +} + +#[derive(Debug, Clone, Copy)] +pub struct SystemShutdown { + pub timeout: Duration, +} + +impl actix::Message for SystemShutdown { + type Result = (); +} + +impl actix::Handler for DBWorker { + type Result = ResponseActFuture::Result>; + fn handle(&mut self, msg: SystemShutdown, _: &mut Self::Context) -> Self::Result { + let wait = self + .actors + .iter::() + .map(|(_, addr)| addr.send(msg)) + .collect::>(); + Box::pin( + futures_util::future::join_all(wait) + .into_actor(&*self) + .then(|_, act, _| { + act.tx = mpsc::unbounded().0; + let mut rx = act.done_tx.subscribe(); + async move { + rx.recv().await.ok(); + } + .into_actor(&*act) + }) + .map(|_, _, ctx| ctx.stop()), + ) + } +} + +impl actix::Handler for DBWorker { + type Result = ResponseFuture>; + fn handle(&mut self, msg: SubmitSignature, _: &mut Self::Context) -> Self::Result { + let users = self.actors.iter::().collect::>(); + async move { + for (user_id, addr) in users { + let res = addr + .send(SubmitSignature { + user_id, + ..msg.clone() + }) + .await; + match res { + Err(_) => continue, + Ok(Err(SubmitError::NotFound)) => continue, + Ok(Ok(())) => return Ok(()), + Ok(Err(error)) => return Err(error), + } + } + Ok(()) + } + .boxed_local() + } +} + +pub struct GetUserWorker { + pub user_id: UserId, +} + +impl actix::Message for GetUserWorker { + type Result = actix::Addr; +} + +impl actix::Handler for DBWorker { + type Result = actix::Addr; + fn handle(&mut self, msg: GetUserWorker, ctx: &mut Self::Context) -> Self::Result { + let id = msg.user_id; + self.actors.get_or_start(id, { + let counter = self.counter.clone(); + let db = self.db.clone(); + let root = ctx.address(); + let endpoints = self.endpoints.clone(); + move || { + UserWorker::start_in_arbiter(&Arbiter::current(), move |_| { + UserWorker::new(id, endpoints, db, counter, root) + }) + } + }) + } +} + +pub struct GetTokenWorker { + pub user_id: UserId, +} + +impl actix::Message for GetTokenWorker { + type Result = Result, get_jwt::Error>; +} + +impl actix::Handler for DBWorker { + type Result = Result, get_jwt::Error>; + fn handle(&mut self, msg: GetTokenWorker, _: &mut Self::Context) -> Self::Result { + let id = msg.user_id; + match &self.db { + DbPool::Real(db) => { + let addr = self.actors.get_or_start(id, { + let user_id = msg.user_id; + let local_db = self.db.get_local().clone(); + let endpoints = self.endpoints.clone(); + let claim = LoginWithAdminCred { + client: reqwest::Client::new(), + user_id, + db: db.clone(), + endpoints: endpoints.clone(), + }; + move || { + TokenWorker::start_in_arbiter(&Arbiter::current(), move |_| { + TokenWorker::new(user_id, local_db, endpoints, claim) + }) + } + }); + Ok(addr) + } + DbPool::Proxied(_) => self + .actors + .get::(id) + .ok_or(get_jwt::Error::NotAllowed)? + .upgrade() + .ok_or(get_jwt::Error::other("TokenWorker stopped")), + } + } +} + +pub struct RegisterLogs { + pub flow_run_id: FlowRunId, + pub tx: EventSender, + pub filter: Option, +} + +impl actix::Message for RegisterLogs { + type Result = Result; +} + +impl actix::Handler for DBWorker { + type Result = ::Result; + fn handle(&mut self, msg: RegisterLogs, _: &mut Self::Context) -> Self::Result { + let span = tracing::error_span!(FLOW_SPAN_NAME, flow_run_id = msg.flow_run_id.to_string()); + let id = span.id().ok_or("span ID is None")?; + let filter = EnvFilter::builder() + .with_default_directive(LevelFilter::ERROR.into()) + .parse_lossy(msg.filter.as_deref().unwrap_or(DEFAULT_LOG_FILTER)); + let mut map = self.tracing_data.write().unwrap(); + map.insert(id, flow_logs::Data { tx: msg.tx, filter }); + Ok(span) + } +} + +pub struct StartFlowRunWorker +where + F: FnOnce(&mut Context) -> FlowRunWorker + Send + 'static, +{ + pub id: ::ID, + pub make_actor: F, +} + +impl actix::Message for StartFlowRunWorker +where + F: FnOnce(&mut Context) -> FlowRunWorker + Send + 'static, +{ + type Result = Result, AlreadyStarted>; +} + +impl actix::Handler> for DBWorker +where + F: FnOnce(&mut Context) -> FlowRunWorker + Send + 'static, +{ + type Result = Result, AlreadyStarted>; + + fn handle(&mut self, msg: StartFlowRunWorker, _: &mut Self::Context) -> Self::Result { + self.actors + .try_start_with_context(msg.id, msg.make_actor, Arbiter::current()) + } +} + +pub struct FindActor { + pub id: A::ID, +} + +impl actix::Message for FindActor { + type Result = Option>; +} + +impl FindActor { + pub fn new(id: A::ID) -> Self { + Self { id } + } +} + +impl actix::Handler> for DBWorker { + type Result = Option>; + + fn handle(&mut self, msg: FindActor, _: &mut Self::Context) -> Self::Result { + self.actors + .get::(msg.id) + .and_then(|weak| weak.upgrade()) + } +} + +impl actix::Handler> for DBWorker { + type Result = Option>; + + fn handle(&mut self, msg: FindActor, _: &mut Self::Context) -> Self::Result { + self.actors + .get::(msg.id) + .and_then(|weak| weak.upgrade()) + } +} + +pub struct CopyIn(pub T); + +impl actix::Message for CopyIn { + type Result = (); +} + +impl actix::Handler>> for DBWorker { + type Result = (); + + fn handle(&mut self, msg: CopyIn>, _: &mut Self::Context) -> Self::Result { + self.tx.unbounded_send(msg.0).ok(); + } +} + +async fn db_copy_in(rx: mpsc::UnboundedReceiver>, db: RealDbPool) { + const CHUNK_SIZE: usize = 16; + let mut chunks = rx.ready_chunks(CHUNK_SIZE); + + while let Some(events) = chunks.next().await { + let conn = match db.get_admin_conn().await { + Ok(conn) => conn, + Err(error) => { + tracing::error!( + "could not get DB connection, dropping events. detail: {}", + error + ); + continue; + } + }; + let res = conn + .copy_in_flow_run_logs(events.iter().flat_map(|vec| vec.iter())) + .await; + match res { + Ok(count) => tracing::debug!("inserted {} rows", count), + Err(error) => tracing::error!("{}, dropping events.", error), + } + } +} + +async fn db_copy_in_proxied(rx: mpsc::UnboundedReceiver>, db: ProxiedDbPool) { + const CHUNK_SIZE: usize = 16; + let mut chunks = rx.ready_chunks(CHUNK_SIZE); + while let Some(events) = chunks.next().await { + let rows = events + .into_iter() + .flat_map(|vec| vec.into_iter()) + .collect::>(); + if rows.is_empty() { + continue; + } + let user_id = rows[0].user_id; + let conn = match db.get_user_conn(user_id).await { + Ok(conn) => conn, + Err(error) => { + tracing::error!( + "could not get DB connection, dropping events. detail: {}", + error + ); + continue; + } + }; + let count = rows.len(); + let res = conn.push_logs(&rows).await; + match res { + Ok(_) => tracing::debug!("inserted {} rows", count), + Err(error) => tracing::error!("{}, dropping events.", error), + } + } +} diff --git a/crates/flow-server/src/db_worker/signer.rs b/crates/flow-server/src/db_worker/signer.rs new file mode 100644 index 00000000..55f4705d --- /dev/null +++ b/crates/flow-server/src/db_worker/signer.rs @@ -0,0 +1,209 @@ +use actix::{Actor, ResponseFuture}; +use db::{pool::DbPool, Error as DbError, Wallet}; +use flow_lib::{ + context::signer::{self, SignatureRequest}, + UserId, +}; +use futures_util::FutureExt; +use hashbrown::{hash_map::Entry, HashMap}; +use serde_json::Value as JsonValue; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; +use std::future::ready; +use thiserror::Error as ThisError; + +pub enum SignerType { + Keypair(Box), + UserWallet { + // Forward to UserWorker + user_id: UserId, + sender: actix::Recipient, + }, +} + +impl std::fmt::Debug for SignerType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Keypair(k) => f + .debug_tuple("SignerType::Keypair") + .field(&k.pubkey()) + .finish(), + Self::UserWallet { user_id, .. } => f + .debug_tuple("SignerType::UserWallet") + .field(&user_id) + .finish(), + } + } +} + +pub struct SignerWorker { + pub signers: HashMap, +} + +impl Actor for SignerWorker { + type Context = actix::Context; +} + +impl actix::Handler for SignerWorker { + type Result = ResponseFuture<::Result>; + + fn handle(&mut self, msg: SignatureRequest, _: &mut Self::Context) -> Self::Result { + match self.signers.get(&msg.pubkey) { + None => ready(Err(signer::Error::Pubkey(msg.pubkey.to_string()))).boxed(), + Some(SignerType::Keypair(keypair)) => ready(Ok(signer::SignatureResponse { + signature: keypair.sign_message(&msg.message), + new_message: None, + })) + .boxed(), + Some(SignerType::UserWallet { sender, .. }) => { + let fut = sender.send(msg); + async move { fut.await? }.boxed() + } + } + } +} + +#[derive(ThisError, Debug)] +pub enum AddWalletError { + #[error("pubkey is not on curve")] + NotOnCurve, + #[error("invalid keypair")] + InvalidKeypair, +} + +impl SignerWorker { + pub async fn fetch<'a, I>(db: DbPool, users: I) -> Result + where + I: IntoIterator)>, + { + let mut signers = HashMap::new(); + + for (user_id, sender) in users { + let conn = db.get_user_conn(*user_id).await?; + let wallets = conn.get_wallets().await?; + for w in wallets { + let pk = Pubkey::new_from_array(w.pubkey); + if !pk.is_on_curve() { + tracing::warn!("invalid wallet: pubkey is not on curve; id={}", w.id); + continue; + } + let s = match w.keypair { + None => SignerType::UserWallet { + user_id: *user_id, + sender: sender.clone(), + }, + Some(keypair) => { + // check to prevent https://github.com/advisories/GHSA-w5vr-6qhr-36cc + if ed25519_dalek::SigningKey::from_keypair_bytes(&keypair).is_err() { + tracing::warn!("invalid keypair: mismatch; id={}", w.id); + continue; + } + let keypair = match Keypair::from_bytes(&keypair) { + Ok(keypair) => keypair, + Err(error) => { + tracing::warn!("invalid keypair: {}; id={}", error, w.id); + continue; + } + }; + SignerType::Keypair(Box::new(keypair)) + } + }; + match signers.entry(pk) { + Entry::Vacant(slot) => { + slot.insert(s); + } + Entry::Occupied(mut slot) => { + if matches!( + (slot.get(), &s), + (SignerType::UserWallet { .. }, SignerType::Keypair(_)) + ) { + tracing::warn!("replacing wallet {}", pk); + slot.insert(s); + } + } + } + } + } + + Ok(Self { signers }) + } + + pub async fn fetch_all_and_start<'a, I>( + db: DbPool, + users: I, + ) -> Result<(actix::Addr, JsonValue), DbError> + where + I: IntoIterator)>, + { + let signer = Self::fetch(db, users).await?; + let signers_info = signer + .signers + .iter() + .map(|(pk, w)| { + ( + pk.to_string(), + match w { + SignerType::Keypair(_) => "HARDCODED".to_owned(), + SignerType::UserWallet { user_id, .. } => user_id.to_string(), + }, + ) + }) + .collect::(); + Ok((signer.start(), signers_info)) + } + + pub fn add_wallet( + &mut self, + user_id: &UserId, + sender: &actix::Recipient, + w: Wallet, + ) -> Result<(), AddWalletError> { + let pk = Pubkey::new_from_array(w.pubkey); + if !pk.is_on_curve() { + return Err(AddWalletError::NotOnCurve); + } + let s = if let Some(keypair) = w.keypair { + let keypair = + Keypair::from_bytes(&keypair).map_err(|_| AddWalletError::InvalidKeypair)?; + SignerType::Keypair(Box::new(keypair)) + } else { + SignerType::UserWallet { + user_id: *user_id, + sender: sender.clone(), + } + }; + match self.signers.entry(pk) { + Entry::Vacant(slot) => { + slot.insert(s); + } + Entry::Occupied(mut slot) => { + if matches!( + (slot.get(), &s), + (SignerType::UserWallet { .. }, SignerType::Keypair(_)) + ) { + slot.insert(s); + } + } + } + + Ok(()) + } + + pub async fn fetch_wallets_from_ids( + db: &DbPool, + user_id: UserId, + sender: actix::Recipient, + ids: &[i64], + ) -> Result { + let mut signer = Self { + signers: HashMap::new(), + }; + let conn = db.get_user_conn(user_id).await?; + let wallets = conn.get_some_wallets(&ids).await?; + for wallet in wallets { + if let Err(error) = signer.add_wallet(&user_id, &sender, wallet) { + tracing::warn!("could not add wallet: {}", error); + } + } + Ok(signer) + } +} diff --git a/crates/flow-server/src/db_worker/token_worker.rs b/crates/flow-server/src/db_worker/token_worker.rs new file mode 100644 index 00000000..3d2610b6 --- /dev/null +++ b/crates/flow-server/src/db_worker/token_worker.rs @@ -0,0 +1,441 @@ +use crate::{ + api::{apikey_info, claim_token}, + auth::X_API_KEY, + user::PasswordLogin, +}; +use actix::{Actor, ActorFutureExt, Addr, AsyncContext, ResponseFuture, WrapFuture}; +use chrono::{Duration, Utc}; +use db::{local_storage::Jwt, pool::RealDbPool, LocalStorage}; +use flow_lib::{config::Endpoints, context::get_jwt, UserId}; +use futures_channel::oneshot; +use futures_util::{future::BoxFuture, FutureExt}; +use hashbrown::HashMap; +use reqwest::{header::HeaderName, StatusCode}; +use serde::{Deserialize, Serialize}; +use std::future::ready; +use utils::{actix_service::ActixService, address_book::ManagableActor}; + +pub trait ClaimToken: Unpin + 'static { + fn claim(&self) -> BoxFuture<'static, Result>; +} + +#[derive(Clone)] +pub struct LoginWithAdminCred { + pub client: reqwest::Client, + pub user_id: UserId, + pub db: RealDbPool, + pub endpoints: Endpoints, +} + +impl LoginWithAdminCred { + pub async fn claim(self) -> Result { + let r = self + .db + .get_admin_conn() + .await + .map_err(get_jwt::Error::other)? + .get_login_credential(self.user_id) + .await + .map_err(get_jwt::Error::other)?; + + let resp = self + .client + .post(format!( + "{}/auth/v1/token?grant_type=password", + self.endpoints.supabase + )) + .header( + HeaderName::from_static("apikey"), + &self.endpoints.supabase_anon_key, + ) + .json(&PasswordLogin { + email: r.email, + password: r.password, + }) + .send() + .await + .map_err(get_jwt::Error::other)?; + + if resp.status() != StatusCode::OK { + return Err(supabase_error(resp).await); + } + + Ok(Jwt::from( + resp.json::() + .await + .map_err(get_jwt::Error::other)?, + )) + } +} + +impl ClaimToken for LoginWithAdminCred { + fn claim(&self) -> BoxFuture<'static, Result> { + Box::pin(self.clone().claim()) + } +} + +#[derive(Clone)] +pub struct ClaimWithApiKey { + pub client: reqwest::Client, + pub user_id: UserId, + pub api_key: String, + pub upstream_url: String, +} + +impl ClaimWithApiKey { + async fn claim(self) -> Result { + let claim_token::Output { + access_token, + refresh_token, + expires_at, + .. + } = self + .client + .post(format!("{}/auth/claim_token", self.upstream_url)) + .header(X_API_KEY.as_str(), self.api_key) + .send() + .await + .map_err(get_jwt::Error::other)? + .error_for_status() + .map_err(get_jwt::Error::other)? + .json::() + .await + .map_err(get_jwt::Error::other)?; + Ok(Jwt { + access_token, + refresh_token, + expires_at, + }) + } +} + +impl ClaimToken for ClaimWithApiKey { + fn claim(&self) -> BoxFuture<'static, Result> { + Box::pin(self.clone().claim()) + } +} + +pub struct TokenWorker { + claim: Box, + user_id: UserId, + local_db: LocalStorage, + state: TokenState, + endpoints: Endpoints, +} + +impl ManagableActor for TokenWorker { + type ID = UserId; + fn id(&self) -> Self::ID { + self.user_id + } +} + +impl TokenWorker { + pub fn new( + user_id: UserId, + local_db: LocalStorage, + endpoints: Endpoints, + claim: T, + ) -> Self { + Self { + claim: Box::new(claim), + user_id, + local_db, + endpoints, + state: TokenState::None, + } + } +} + +enum TokenState { + None, + Available(Jwt), + Fetching(Vec::Result>>), +} + +impl Actor for TokenWorker { + type Context = actix::Context; + + fn started(&mut self, _: &mut Self::Context) { + tracing::debug!("started TokenWorker {}", self.user_id); + let token = self + .local_db + .get_jwt(&self.user_id) + .map_err(|e| { + tracing::error!("{}", e); + self.local_db + .remove_jwt(&self.user_id) + .map_err(|e| tracing::error!("{}", e)) + .ok(); + }) + .ok() + .flatten() + .map(TokenState::Available) + .unwrap_or(TokenState::None); + self.state = token; + } + + fn stopped(&mut self, _: &mut Self::Context) { + tracing::debug!("stopped TokenWorker {}", self.user_id); + } +} + +#[derive(Deserialize, Debug)] +struct GoTrueError { + error: String, + error_description: String, +} + +async fn supabase_error(resp: reqwest::Response) -> get_jwt::Error { + let bytes = match resp.bytes().await { + Ok(bytes) => bytes, + Err(error) => return get_jwt::Error::other(error), + }; + match serde_json::from_slice::(&bytes) { + Ok(GoTrueError { + error, + error_description, + }) => get_jwt::Error::Supabase { + error, + error_description, + }, + Err(_) => get_jwt::Error::other(String::from_utf8_lossy(&bytes)), + } +} + +impl From for Jwt { + fn from(resp: TokenResponse) -> Self { + Self { + access_token: resp.access_token, + refresh_token: resp.refresh_token, + expires_at: Utc::now() + Duration::try_seconds(resp.expires_in as i64).unwrap(), + } + } +} + +#[derive(Deserialize, Debug)] +struct TokenResponse { + access_token: String, + // token_type: String, + expires_in: u32, + refresh_token: String, +} + +async fn refresh(refresh_token: String, endpoints: Endpoints) -> Result { + #[derive(Serialize, Debug)] + struct RefreshToken { + refresh_token: String, + } + + let resp = reqwest::Client::new() + .post(format!( + "{}/auth/v1/token?grant_type=refresh_token", + endpoints.supabase + )) + .header( + HeaderName::from_static("apikey"), + &endpoints.supabase_anon_key, + ) + .json(&RefreshToken { refresh_token }) + .send() + .await + .map_err(get_jwt::Error::other)?; + if resp.status() != StatusCode::OK { + return Err(supabase_error(resp).await); + } + Ok(Jwt::from( + resp.json::() + .await + .map_err(get_jwt::Error::other)?, + )) +} + +impl TokenState { + fn process_result( + &mut self, + res: Result, + local: &LocalStorage, + user_id: &UserId, + ) { + *self = match std::mem::replace(self, TokenState::None) { + TokenState::None => unreachable!(), + TokenState::Available(_) => unreachable!(), + TokenState::Fetching(vec) => { + let (result, state) = match res { + Ok(jwt) => { + local + .set_jwt(user_id, &jwt) + .map_err(|e| tracing::error!("{}", e)) + .ok(); + ( + Ok(get_jwt::Response { + access_token: jwt.access_token.clone(), + }), + TokenState::Available(jwt), + ) + } + Err(error) => { + local + .remove_jwt(user_id) + .map_err(|e| tracing::error!("{}", e)) + .ok(); + (Err(error), TokenState::None) + } + }; + for tx in vec { + tx.send(result.clone()).ok(); + } + state + } + } + } +} + +impl actix::Handler for TokenWorker { + type Result = ResponseFuture<::Result>; + fn handle(&mut self, msg: get_jwt::Request, ctx: &mut Self::Context) -> Self::Result { + if self.user_id != msg.user_id { + return Box::pin(ready(Err(get_jwt::Error::WrongRecipient))); + } + + let result: Self::Result; + let from_rx = |rx: oneshot::Receiver<_>| { + Box::pin(async move { rx.await.map_err(|_| get_jwt::Error::other("canceled"))? }) + }; + self.state = + match std::mem::replace(&mut self.state, TokenState::None) { + TokenState::None => { + tracing::info!("claim new JWT token, user_id={}", self.user_id); + let task = self.claim.claim().into_actor(&*self).map(|res, act, _| { + act.state.process_result(res, &act.local_db, &act.user_id) + }); + ctx.spawn(task); + + let (tx, rx) = oneshot::channel(); + result = from_rx(rx); + TokenState::Fetching([tx].into()) + } + TokenState::Available(jwt) => { + if jwt.expires_at - Utc::now() < Duration::try_minutes(5).unwrap() { + let refresh_token = jwt.refresh_token; + let endpoints = self.endpoints.clone(); + tracing::info!("refresh JWT token, user_id={}", self.user_id); + let task = refresh(refresh_token, endpoints).into_actor(&*self).map( + |res, act, _| { + act.state.process_result(res, &act.local_db, &act.user_id) + }, + ); + ctx.spawn(task); + + let (tx, rx) = oneshot::channel(); + result = from_rx(rx); + TokenState::Fetching(vec![tx]) + } else { + result = Box::pin(ready(Ok(get_jwt::Response { + access_token: jwt.access_token.clone(), + }))); + TokenState::Available(jwt) + } + } + TokenState::Fetching(mut vec) => { + let (tx, rx) = oneshot::channel(); + vec.push(tx); + result = from_rx(rx); + TokenState::Fetching(vec) + } + }; + + result + } +} + +pub async fn token_from_apikeys( + keys: Vec, + local_db: LocalStorage, + endpoints: Endpoints, + upstream_url: String, +) -> ( + HashMap, + HashMap>, +) { + let client = reqwest::Client::new(); + let tasks = keys.into_iter().map(|k| { + async fn send( + client: &reqwest::Client, + url: &str, + k: String, + ) -> Result { + let resp = client + .get(format!("{}/apikey/info", url)) + .header(X_API_KEY.as_str(), k) + .send() + .await?; + let output = resp + .error_for_status()? + .json::() + .await?; + Ok(output.user_id) + } + + send(&client, &upstream_url, k.clone()).map(|r| (k, r)) + }); + let actors = futures_util::future::join_all(tasks) + .await + .into_iter() + .filter_map(|(k, r)| match r { + Ok(id) => Some((id, k)), + Err(error) => { + tracing::error!("failed to get key info {:?}: {}", k, error); + None + } + }) + .map(|(user_id, api_key)| { + let worker = TokenWorker::new( + user_id, + local_db.clone(), + endpoints.clone(), + ClaimWithApiKey { + client: client.clone(), + user_id, + api_key, + upstream_url: upstream_url.clone(), + }, + ); + let addr = worker.start(); + (user_id, addr) + }) + .collect::>>(); + + let services = actors + .iter() + .map(|(user_id, addr)| { + let svc = get_jwt::Svc::from_service( + ActixService::from(addr.clone().recipient()), + get_jwt::Error::worker, + 16, + ); + (*user_id, svc) + }) + .collect::>(); + (services, actors) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn need_key_refresh() { + let error = refresh( + "Hello".to_owned(), + Endpoints { + flow_server: String::new(), + supabase: "https://base.spaceoperator.com".to_owned(), + supabase_anon_key: std::env::var("ANON_KEY").unwrap(), + }, + ) + .await + .unwrap_err(); + dbg!(error); + } +} diff --git a/crates/flow-server/src/db_worker/user_worker.rs b/crates/flow-server/src/db_worker/user_worker.rs new file mode 100644 index 00000000..0ba95d70 --- /dev/null +++ b/crates/flow-server/src/db_worker/user_worker.rs @@ -0,0 +1,868 @@ +use super::{ + flow_run_worker::FlowRunWorker, + messages::SubscribeError, + signer::{AddWalletError, SignerWorker}, + Counter, DBWorker, FindActor, GetTokenWorker, GetUserWorker, RegisterLogs, StartFlowRunWorker, +}; +use crate::error::ErrorBody; +use actix::{ + fut::wrap_future, Actor, ActorFutureExt, ActorTryFutureExt, AsyncContext, Response, + ResponseActFuture, ResponseFuture, WrapFuture, +}; +use actix_web::{http::StatusCode, ResponseError}; +use bytes::Bytes; +use db::{pool::DbPool, Error as DbError}; +use flow::{ + flow_graph::StopSignal, + flow_registry::{get_flow, get_previous_values, new_flow_run, FlowRegistry, StartFlowOptions}, + flow_set::{FlowDeployment, FlowSet, FlowSetContext, StartFlowDeploymentOptions}, +}; +use flow_lib::{ + config::{ + client::{FlowRunOrigin, PartialConfig}, + Endpoints, + }, + context::{ + env::RUST_LOG, + get_jwt, + signer::{self, SignatureRequest}, + }, + solana::{is_same_message_logic, Pubkey, SolanaActionConfig}, + utils::TowerClient, + FlowId, FlowRunId, User, UserId, +}; +use futures_channel::{mpsc, oneshot}; +use futures_util::{future::BoxFuture, TryFutureExt}; +use hashbrown::HashMap; +use solana_sdk::signature::Signature; +use std::future::{ready, Future}; +use thiserror::Error as ThisError; +use utils::{actix_service::ActixService, address_book::ManagableActor}; + +pub struct UserWorker { + root: actix::Addr, + db: DbPool, + counter: Counter, + user_id: UserId, + endpoints: Endpoints, + sigreg: HashMap, + subs: HashMap, +} + +pub struct SubscribeSigReq {} + +impl actix::Message for SubscribeSigReq { + type Result = Result<(u64, mpsc::UnboundedReceiver), SubscribeError>; +} + +impl actix::Handler for UserWorker { + type Result = ::Result; + + fn handle(&mut self, _msg: SubscribeSigReq, _: &mut Self::Context) -> Self::Result { + let stream_id = self.counter.next(); + let (tx, rx) = mpsc::unbounded(); + self.subs.insert(stream_id, Subscription { tx }); + Ok((stream_id, rx)) + } +} + +struct Subscription { + tx: mpsc::UnboundedSender, +} + +#[derive(Clone)] +pub struct SigReqEvent(pub SignatureRequest); + +impl actix::Message for SigReqEvent { + type Result = (); +} + +#[derive(Debug)] +struct SigReq { + resp: oneshot::Sender>, + req: signer::SignatureRequest, +} + +impl ManagableActor for UserWorker { + type ID = UserId; + + fn id(&self) -> Self::ID { + self.user_id + } +} + +impl Actor for UserWorker { + type Context = actix::Context; + + fn started(&mut self, _: &mut Self::Context) { + tracing::debug!("started UserWorker {}", self.user_id); + } + + fn stopping(&mut self, _: &mut Self::Context) -> actix::Running { + self.subs.retain(|_, v| !v.tx.is_closed()); + if self.subs.is_empty() { + actix::Running::Stop + } else { + actix::Running::Continue + } + } + + fn stopped(&mut self, _: &mut Self::Context) { + tracing::debug!("stopped UserWorker {}", self.user_id); + } +} + +#[derive(ThisError, Debug)] +pub enum MakeFlowSetContextError { + #[error(transparent)] + MakeTokenWorker(#[from] get_jwt::Error), + #[error(transparent)] + Db(#[from] DbError), + #[error(transparent)] + Mailbox(#[from] actix::MailboxError), + #[error(transparent)] + AddWallet(#[from] AddWalletError), +} + +impl UserWorker { + pub fn new( + user_id: UserId, + endpoints: Endpoints, + db: DbPool, + counter: Counter, + root: actix::Addr, + ) -> Self { + Self { + user_id, + endpoints, + db, + counter, + root, + sigreg: <_>::default(), + subs: <_>::default(), + } + } + + fn make_flow_set_context( + &mut self, + d: &FlowDeployment, + options: &StartFlowDeploymentOptions, + ctx: &mut actix::Context, + ) -> impl Future> + 'static { + let new_flow_run = TowerClient::from_service( + ActixService::from(ctx.address().recipient()), + new_flow_run::Error::Worker, + 16, + ); + + let root = self.root.clone(); + let db = self.db.clone(); + let user_id = self.user_id; + let addr = ctx.address().recipient(); + let wallets_id = d.wallets_id.clone(); + let endpoints = self.endpoints.clone(); + let starter = options.starter; + async move { + let get_jwt = root.send(GetTokenWorker { user_id }).await??; + let get_jwt = TowerClient::from_service( + ActixService::from(get_jwt.recipient()), + get_jwt::Error::worker, + 16, + ); + + let mut signer = + SignerWorker::fetch_wallets_from_ids(&db, user_id, addr, &wallets_id).await?; + if starter.user_id != user_id { + let addr = root + .send(GetUserWorker { + user_id: starter.user_id, + }) + .await?; + let conn = db.get_user_conn(starter.user_id).await?; + let wallet = conn + .get_wallet_by_pubkey(&starter.pubkey.to_bytes()) + .await?; + signer.add_wallet(&starter.user_id, &addr.recipient(), wallet)?; + } + let signer = signer.start(); + let signer = TowerClient::from_service( + ActixService::from(signer.recipient()), + signer::Error::Worker, + 16, + ); + + Ok(FlowSetContext::builder() + .depth(0) + .endpoints(endpoints) + .get_jwt(get_jwt) + .new_flow_run(new_flow_run) + .signer(signer) + .build()) + } + } + + fn process_sigreq( + &mut self, + result: Result<(i64, signer::SignatureRequest), DbError>, + ctx: &mut actix::Context, + ) -> BoxFuture<'static, Result> { + match result { + Ok((id, mut req)) => { + req.id = Some(id); + let (tx, rx) = oneshot::channel(); + let timeout = req.timeout; + self.sigreg + .try_insert( + id, + SigReq { + resp: tx, + req: req.clone(), + }, + ) + .expect("DB's ID is unique"); + self.subs + .retain(|_, sub| sub.tx.unbounded_send(req.clone()).is_ok()); + if let Some(flow_run_id) = req.flow_run_id { + actix::spawn( + self.root + .send(FindActor::::new(flow_run_id)) + .map_ok(move |res| { + if let Some(addr) = res { + addr.do_send(SigReqEvent(req.clone())); + } + }) + .inspect_err(move |_| { + tracing::error!("FlowRunWorker not found {}", flow_run_id); + }), + ); + } + ctx.run_later(timeout, move |act, _| { + if let Some(SigReq { resp, .. }) = act.sigreg.remove(&id) { + resp.send(Err(signer::Error::Timeout)).ok(); + } + }); + Box::pin(async move { + rx.await + .map_err(|_| signer::Error::Other("tx dropped".into()))? + }) + } + Err(error) => Box::pin(ready(Err(signer::Error::Other(error.into())))), + } + } +} + +pub struct SigReqExists { + pub id: i64, +} + +impl actix::Message for SigReqExists { + type Result = bool; +} + +impl actix::Handler for UserWorker { + type Result = Response<::Result>; + + fn handle(&mut self, msg: SigReqExists, _: &mut Self::Context) -> Self::Result { + Response::reply(self.sigreg.contains_key(&msg.id)) + } +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture>; + + fn handle(&mut self, msg: get_previous_values::Request, _: &mut Self::Context) -> Self::Result { + let user_id = self.user_id; + let db = self.db.clone(); + let fut = async move { + if user_id != msg.user_id { + return Err(get_previous_values::Error::Unauthorized); + } + + let values = db + .get_user_conn(user_id) + .await + .map_err(|e| get_previous_values::Error::Other(e.into()))? + .get_previous_values(&msg.nodes) + .await + .map_err(|e| get_previous_values::Error::Other(e.into()))?; + + Ok(get_previous_values::Response { values }) + }; + + Box::pin(fut) + } +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture>; + + fn handle(&mut self, msg: get_flow::Request, _: &mut Self::Context) -> Self::Result { + let user_id = self.user_id; + let db = self.db.clone(); + let fut = async move { + if user_id != msg.user_id { + return Err(get_flow::Error::Unauthorized); + } + + let config = db + .get_user_conn(user_id) + .await + .map_err(|e| get_flow::Error::Other(e.into()))? + .get_flow_config(msg.flow_id) + .await + .map_err(|e| match e { + DbError::ResourceNotFound { .. } => get_flow::Error::NotFound, + _ => get_flow::Error::Other(e.into()), + })?; + + Ok(get_flow::Response { config }) + }; + + Box::pin(fut) + } +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture>; + + fn handle(&mut self, msg: new_flow_run::Request, _: &mut Self::Context) -> Self::Result { + let user_id = self.user_id; + let db = self.db.clone(); + let root = self.root.clone(); + let counter = self.counter.clone(); + Box::pin(async move { + if user_id != msg.user_id { + return Err(new_flow_run::Error::Unauthorized); + } + + let conn = db + .get_user_conn(user_id) + .await + .map_err(new_flow_run::Error::other)?; + let run_id = conn + .new_flow_run(&msg.config, &msg.inputs) + .await + .map_err(new_flow_run::Error::other)?; + + for id in &msg.shared_with { + if *id != user_id { + conn.share_flow_run(run_id, *id) + .await + .map_err(new_flow_run::Error::other)?; + } + } + + let stop_signal = StopSignal::new(); + let stop_shared_signal = StopSignal::new(); + + root.send(StartFlowRunWorker { + id: run_id, + make_actor: { + let stop_signal = stop_signal.clone(); + let stop_shared_signal = stop_shared_signal.clone(); + let root = root.clone(); + move |ctx| { + FlowRunWorker::new( + run_id, + user_id, + msg.shared_with, + counter, + msg.stream, + db, + root.clone(), + stop_signal.clone(), + stop_shared_signal.clone(), + ctx, + ) + } + }, + }) + .await? + .map_err(|_| new_flow_run::Error::Other("could not start worker".into()))?; + + let span = root + .send(RegisterLogs { + flow_run_id: run_id, + tx: msg.tx, + filter: msg.config.environment.get(RUST_LOG).cloned(), + }) + .await? + .map_err(new_flow_run::Error::Other)?; + + Ok(new_flow_run::Response { + flow_run_id: run_id, + stop_signal, + stop_shared_signal, + span, + }) + }) + } +} + +impl actix::Handler for UserWorker { + type Result = ResponseActFuture>; + + fn handle(&mut self, msg: signer::SignatureRequest, _: &mut Self::Context) -> Self::Result { + let db = self.db.clone(); + let user_id = self.user_id; + async move { + let id = db + .get_user_conn(user_id) + .await? + .new_signature_request( + &msg.pubkey.to_bytes(), + &msg.message, + msg.flow_run_id.as_ref(), + msg.signatures.as_deref(), + ) + .await?; + Ok((id, msg)) + } + .into_actor(&*self) + .then(move |result, act, ctx| act.process_sigreq(result, ctx).into_actor(act)) + .boxed_local() + } +} + +#[derive(Clone, Debug)] +pub struct SubmitSignature { + pub user_id: UserId, + pub id: i64, + pub signature: [u8; 64], + pub new_msg: Option, +} + +impl actix::Message for SubmitSignature { + type Result = Result<(), SubmitError>; +} + +#[derive(ThisError, Debug)] +pub enum SubmitError { + #[error("unauthorized")] + Unauthorized, + #[error("not found")] + NotFound, + #[error("could not update tx because it will invalidate existing signature")] + NotAllowChangeTx, + #[error("transaction changed: {}", .0)] + TxChanged(anyhow::Error), + #[error("signature verification failed")] + WrongSignature, + #[error(transparent)] + Db(#[from] DbError), + #[error(transparent)] + Mailbox(#[from] actix::MailboxError), +} + +impl ResponseError for SubmitError { + fn status_code(&self) -> StatusCode { + match self { + SubmitError::Unauthorized => StatusCode::UNAUTHORIZED, + SubmitError::NotFound => StatusCode::NOT_FOUND, + SubmitError::NotAllowChangeTx => StatusCode::BAD_REQUEST, + SubmitError::WrongSignature => StatusCode::BAD_REQUEST, + SubmitError::TxChanged(_) => StatusCode::BAD_REQUEST, + SubmitError::Db(_) => StatusCode::INTERNAL_SERVER_ERROR, + SubmitError::Mailbox(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture>; + + fn handle(&mut self, mut msg: SubmitSignature, _: &mut Self::Context) -> Self::Result { + if self.user_id != msg.user_id { + return Box::pin(ready(Err(SubmitError::Unauthorized))); + } + if !self.sigreg.contains_key(&msg.id) { + return Box::pin(ready(Err(SubmitError::NotFound))); + } + let req = self.sigreg.remove(&msg.id).unwrap(); + let mut message = req.req.message.clone(); + if let Some(new) = msg.new_msg { + if new == req.req.message { + msg.new_msg = None; + } else { + if !req + .req + .signatures + .as_ref() + .map(|s| s.is_empty()) + .unwrap_or(true) + { + return Box::pin(ready(Err(SubmitError::NotAllowChangeTx))); + } + if let Err(error) = is_same_message_logic(&req.req.message, &new) { + self.sigreg.insert(msg.id, req); + return Box::pin(ready(Err(SubmitError::TxChanged(error)))); + } + msg.new_msg = Some(new.clone()); + message = new; + } + } + if !Signature::from(msg.signature).verify(&req.req.pubkey.to_bytes(), &message) { + self.sigreg.insert(msg.id, req); + return Box::pin(ready(Err(SubmitError::WrongSignature))); + } + let db = self.db.clone(); + let user_id = self.user_id; + req.resp + .send(Ok(signer::SignatureResponse { + signature: Signature::from(msg.signature), + new_message: msg.new_msg.clone(), + })) + .ok(); + Box::pin(async move { + db.get_user_conn(user_id) + .await? + .save_signature(&msg.id, &msg.signature, msg.new_msg.as_ref()) + .await?; + + Ok(()) + }) + } +} + +pub struct StartFlowFresh { + pub user: User, + pub flow_id: FlowId, + pub input: value::Map, + pub output_instructions: bool, + pub action_identity: Option, + pub action_config: Option, + pub fees: Vec<(Pubkey, u64)>, + pub partial_config: Option, + pub environment: HashMap, +} + +#[derive(ThisError, Debug)] +pub enum StartError { + #[error("unauthorized")] + Unauthorized, + #[error(transparent)] + Flow(#[from] flow::Error), + #[error(transparent)] + NewFlowRun(#[from] new_flow_run::Error), + #[error(transparent)] + Jwt(#[from] get_jwt::Error), + #[error(transparent)] + Mailbox(#[from] actix::MailboxError), + #[error(transparent)] + Db(#[from] db::Error), + #[error(transparent)] + FlowSet(#[from] MakeFlowSetContextError), +} + +impl ResponseError for StartError { + fn status_code(&self) -> StatusCode { + match self { + StartError::Unauthorized => StatusCode::NOT_FOUND, + StartError::Flow(e) => match e { + flow::Error::Any(_) => StatusCode::INTERNAL_SERVER_ERROR, + flow::Error::Canceled(_) => StatusCode::INTERNAL_SERVER_ERROR, + flow::Error::ValueNotFound(_) => StatusCode::INTERNAL_SERVER_ERROR, + flow::Error::CreateCmd(_) => StatusCode::INTERNAL_SERVER_ERROR, + flow::Error::BuildGraphError(_) => StatusCode::BAD_REQUEST, + flow::Error::GetFlow(e) => match e { + get_flow::Error::NotFound => StatusCode::NOT_FOUND, + get_flow::Error::Unauthorized => StatusCode::UNAUTHORIZED, + get_flow::Error::InvalidInferflow { .. } + | get_flow::Error::Worker(_) + | get_flow::Error::MailBox(_) + | get_flow::Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, + }, + flow::Error::GetFlowRow(e) => match e { + flow::flow_set::get_flow_row::Error::NotFound => StatusCode::NOT_FOUND, + flow::flow_set::get_flow_row::Error::Unauthorized => StatusCode::UNAUTHORIZED, + flow::flow_set::get_flow_row::Error::Worker(_) + | flow::flow_set::get_flow_row::Error::MailBox(_) + | flow::flow_set::get_flow_row::Error::Other(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + }, + flow::Error::MakeSigner(e) => match e { + flow::flow_set::make_signer::Error::Worker(_) + | flow::flow_set::make_signer::Error::MailBox(_) + | flow::flow_set::make_signer::Error::Other(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + }, + flow::Error::Cycle => StatusCode::BAD_REQUEST, + flow::Error::NeedOneTx => StatusCode::BAD_REQUEST, + flow::Error::NeedOneSignatureOutput => StatusCode::BAD_REQUEST, + }, + StartError::NewFlowRun(e) => match e { + new_flow_run::Error::BuildFlow(_) | new_flow_run::Error::GetPreviousValues(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + new_flow_run::Error::NotFound => StatusCode::NOT_FOUND, + new_flow_run::Error::Unauthorized => StatusCode::UNAUTHORIZED, + new_flow_run::Error::MaxDepthReached => StatusCode::BAD_REQUEST, + new_flow_run::Error::Worker(_) + | new_flow_run::Error::MailBox(_) + | new_flow_run::Error::Other(_) => StatusCode::UNAUTHORIZED, + }, + StartError::Jwt(e) => match e { + get_jwt::Error::NotAllowed | get_jwt::Error::UserNotFound => { + StatusCode::UNAUTHORIZED + } + get_jwt::Error::WrongRecipient + | get_jwt::Error::Worker(_) + | get_jwt::Error::MailBox(_) + | get_jwt::Error::Supabase { .. } + | get_jwt::Error::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, + }, + StartError::Mailbox(_) => StatusCode::INTERNAL_SERVER_ERROR, + StartError::Db(_) => StatusCode::INTERNAL_SERVER_ERROR, + StartError::FlowSet(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +impl actix::Message for StartFlowFresh { + type Result = Result; +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture>; + + fn handle(&mut self, msg: StartFlowFresh, ctx: &mut Self::Context) -> Self::Result { + let user_id = self.user_id; + let addr = ctx.address(); + let endpoints = self.endpoints.clone(); + let root = self.root.clone(); + let db = self.db.clone(); + Box::pin(async move { + if msg.user.id != user_id { + return Err(StartError::Unauthorized); + } + + let wrk = root.send(GetTokenWorker { user_id }).await??; + + let (signer, signers_info) = + SignerWorker::fetch_all_and_start(db, &[(user_id, addr.clone().recipient())]) + .await?; + + let r = FlowRegistry::from_actix( + msg.user, + msg.user, + Vec::new(), + msg.flow_id, + (signer.recipient(), signers_info), + addr.clone().recipient(), + addr.clone().recipient(), + addr.clone().recipient(), + wrk.recipient(), + msg.environment, + endpoints, + ) + .await?; + + let run_id = r + .start( + msg.flow_id, + msg.input, + StartFlowOptions { + partial_config: msg.partial_config, + collect_instructions: msg.output_instructions, + action_identity: msg.action_identity, + action_config: msg.action_config, + fees: msg.fees, + origin: FlowRunOrigin::Start {}, + ..Default::default() + }, + ) + .await? + .0; + + Ok(run_id) + }) + } +} + +pub struct StartFlowShared { + pub flow_id: FlowId, + pub input: value::Map, + pub output_instructions: bool, + pub action_identity: Option, + pub action_config: Option, + pub fees: Vec<(Pubkey, u64)>, + pub started_by: (UserId, actix::Addr), +} + +impl actix::Message for StartFlowShared { + type Result = Result; +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture<::Result>; + + fn handle(&mut self, msg: StartFlowShared, ctx: &mut Self::Context) -> Self::Result { + if msg.started_by.0 == self.user_id { + return self.handle( + StartFlowFresh { + user: User { id: self.user_id }, + flow_id: msg.flow_id, + input: msg.input, + output_instructions: msg.output_instructions, + action_identity: msg.action_identity, + action_config: msg.action_config, + fees: msg.fees, + partial_config: None, + environment: <_>::default(), + }, + ctx, + ); + } + + let user_id = self.user_id; + let addr = ctx.address(); + let endpoints = self.endpoints.clone(); + let root = self.root.clone(); + let db = self.db.clone(); + Box::pin(async move { + let wrk = root.send(GetTokenWorker { user_id }).await??; + + let (signer, signers_info) = SignerWorker::fetch_all_and_start( + db, + &[ + (msg.started_by.0, msg.started_by.1.recipient()), + (user_id, addr.clone().recipient()), + ], + ) + .await?; + + let r = FlowRegistry::from_actix( + User { id: user_id }, + User { + id: msg.started_by.0, + }, + [msg.started_by.0].into(), + msg.flow_id, + (signer.recipient(), signers_info), + addr.clone().recipient(), + addr.clone().recipient(), + addr.clone().recipient(), + wrk.recipient(), + <_>::default(), + endpoints, + ) + .await?; + + let run_id = r + .start( + msg.flow_id, + msg.input, + StartFlowOptions { + collect_instructions: msg.output_instructions, + action_identity: msg.action_identity, + action_config: msg.action_config, + origin: FlowRunOrigin::StartShared { + started_by: msg.started_by.0, + }, + fees: msg.fees, + ..Default::default() + }, + ) + .await? + .0; + + Ok(run_id) + }) + } +} + +#[derive(Clone, Copy)] +pub struct CloneFlow { + pub user_id: UserId, + pub flow_id: FlowId, +} + +#[derive(ThisError, Debug)] +pub enum CloneFlowError { + #[error("unauthorized")] + Unauthorized, + #[error(transparent)] + Db(#[from] DbError), +} + +impl ResponseError for CloneFlowError { + fn status_code(&self) -> StatusCode { + match self { + CloneFlowError::Unauthorized => StatusCode::NOT_FOUND, + CloneFlowError::Db(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +impl actix::Message for CloneFlow { + type Result = Result, CloneFlowError>; +} + +impl actix::Handler for UserWorker { + type Result = ResponseFuture, CloneFlowError>>; + + fn handle(&mut self, msg: CloneFlow, _: &mut Self::Context) -> Self::Result { + let db = self.db.clone(); + let user_id = self.user_id; + + let fut = async move { + if user_id != msg.user_id { + return Err(CloneFlowError::Unauthorized); + } + + Ok(db + .get_user_conn(user_id) + .await? + .clone_flow(msg.flow_id) + .await?) + }; + + Box::pin(fut) + } +} + +pub struct StartDeployment { + pub deployment: FlowDeployment, + pub options: StartFlowDeploymentOptions, +} + +impl actix::Message for StartDeployment { + type Result = Result; +} + +impl actix::Handler for UserWorker { + type Result = ResponseActFuture::Result>; + + fn handle(&mut self, msg: StartDeployment, ctx: &mut Self::Context) -> Self::Result { + self.make_flow_set_context(&msg.deployment, &msg.options, ctx) + .map_err(StartError::from) + .into_actor(&*self) + .and_then(move |context, _, _| { + wrap_future::<_, Self>(async move { + let id = FlowSet::builder() + .deployment(msg.deployment) + .context(context) + .build() + .start(msg.options) + .await? + .0; + Ok(id) + }) + }) + .boxed_local() + } +} diff --git a/crates/flow-server/src/error.rs b/crates/flow-server/src/error.rs new file mode 100644 index 00000000..4c0fa451 --- /dev/null +++ b/crates/flow-server/src/error.rs @@ -0,0 +1,87 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use serde::Serialize; +use thiserror::Error as ThisError; + +use crate::db_worker::user_worker; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error(transparent)] + Flow(#[from] flow::Error), + #[error(transparent)] + Db(#[from] db::Error), + #[error(transparent)] + SignatureAuth(#[from] crate::user::Invalid), + #[error(transparent)] + Login(#[from] crate::user::LoginError), + #[error("not found")] + NotFound, + #[error("{}", msg)] + Custom { status: StatusCode, msg: String }, + #[error(transparent)] + Actix(#[from] actix_web::Error), + #[error(transparent)] + Mailbox(#[from] actix::MailboxError), + #[error(transparent)] + Start(#[from] user_worker::StartError), + #[error(transparent)] + CloneFlow(#[from] user_worker::CloneFlowError), +} + +impl Error { + pub fn custom(status: StatusCode, msg: T) -> Self { + Error::Custom { + status, + msg: msg.to_string(), + } + } +} + +pub type Result = std::result::Result; + +#[derive(Serialize, Debug)] +pub struct ErrorBody { + pub error: String, +} + +impl ErrorBody { + pub fn build(e: &E) -> HttpResponse { + HttpResponse::build(e.status_code()).json(ErrorBody { + error: e.to_string(), + }) + } +} + +impl ResponseError for Error { + fn status_code(&self) -> StatusCode { + match self { + Error::Start(e) => e.status_code(), + Error::SignatureAuth(_) | Error::Login(_) => StatusCode::UNAUTHORIZED, + Error::NotFound => StatusCode::NOT_FOUND, + Error::Custom { status, .. } => *status, + Error::Actix(e) => e.as_response_error().status_code(), + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + match self { + Error::Start(e) => e.error_response(), + _ => ErrorBody::build(self), + } + } +} + +#[derive(ThisError, Debug)] +#[error("missing apikey")] +pub struct ApiKey; + +impl ResponseError for ApiKey { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } + + fn error_response(&self) -> HttpResponse { + ErrorBody::build(self) + } +} diff --git a/crates/flow-server/src/flow_logs.rs b/crates/flow-server/src/flow_logs.rs new file mode 100644 index 00000000..53afa4ff --- /dev/null +++ b/crates/flow-server/src/flow_logs.rs @@ -0,0 +1,244 @@ +use chrono::Utc; +use flow::flow_run_events::{Event, EventSender, FlowLog, NodeLog, FLOW_SPAN_NAME, NODE_SPAN_NAME}; +use flow_lib::NodeId; +use hashbrown::HashMap; +use std::sync::{Arc, RwLock}; +use tracing::{span, Subscriber}; +use tracing_log::NormalizeEvent; +use tracing_subscriber::{layer::Filter, registry::LookupSpan, EnvFilter, Layer}; + +#[derive(Debug)] +pub struct Data { + pub tx: EventSender, + pub filter: EnvFilter, +} + +pub type Map = Arc>>; + +pub struct FlowLogs { + map: Map, +} + +impl FlowLogs { + pub fn new() -> (Self, Map) { + let map = Map::default(); + (Self { map: map.clone() }, map) + } +} + +#[derive(Default, Clone)] +struct LogMessage { + message: Option, +} + +impl tracing::field::Visit for LogMessage { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.message = Some(format!("{:?}", value)); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.message = Some(value.to_owned()); + } + } +} + +fn get_message(event: &tracing::Event<'_>) -> Option { + let mut msg = LogMessage::default(); + event.record(&mut msg); + msg.message +} + +#[derive(Clone)] +struct NodeLogSpan { + node_id: NodeId, + times: u32, +} + +#[derive(Default, Clone, Debug)] +struct NodeLogSpanVisitor { + node_id: Option, + times: Option, +} + +impl NodeLogSpanVisitor { + fn finish(self) -> Option { + Some(NodeLogSpan { + node_id: self.node_id?, + times: self.times?, + }) + } +} + +impl NodeLogSpanVisitor { + fn record_times>(&mut self, field: &tracing::field::Field, value: T) { + if field.name() == "times" { + if let Ok(u) = value.try_into() { + self.times = Some(u); + } + } + } +} + +impl tracing::field::Visit for NodeLogSpanVisitor { + fn record_f64(&mut self, _field: &tracing::field::Field, _value: f64) {} + + fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { + self.record_times(field, value); + } + + fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { + self.record_times(field, value); + } + + fn record_i128(&mut self, field: &tracing::field::Field, value: i128) { + self.record_times(field, value); + } + + fn record_u128(&mut self, field: &tracing::field::Field, value: u128) { + self.record_times(field, value); + } + + fn record_bool(&mut self, _field: &tracing::field::Field, _value: bool) {} + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "node_id" { + if let Ok(id) = value.parse::() { + self.node_id = Some(id); + } + } + } + + fn record_error( + &mut self, + field: &tracing::field::Field, + value: &(dyn std::error::Error + 'static), + ) { + self.record_str(field, &value.to_string()); + } + + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + self.record_str(field, &format!("{:?}", value)); + } +} + +impl Layer for FlowLogs +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_new_span( + &self, + attrs: &span::Attributes<'_>, + id: &span::Id, + cx: tracing_subscriber::layer::Context<'_, S>, + ) { + if attrs.metadata().name() == NODE_SPAN_NAME { + let span = match cx.span(id) { + None => return, + Some(span) => span, + }; + let mut ext = span.extensions_mut(); + if ext.get_mut::().is_none() { + let mut fields = NodeLogSpanVisitor::default(); + attrs.record(&mut fields); + if let Some(value) = fields.finish() { + ext.insert(value); + } + } + } + } + + fn on_close(&self, id: span::Id, cx: tracing_subscriber::layer::Context<'_, S>) { + if let Some(span) = cx.span(&id) { + if span.name() == FLOW_SPAN_NAME { + self.map.write().unwrap().remove(&id); + } + } + } + + fn on_event(&self, event: &tracing::Event<'_>, cx: tracing_subscriber::layer::Context<'_, S>) { + let map = self.map.read().unwrap(); + let result = cx + .event_scope(event) + .and_then(|mut iter| iter.find_map(|span| map.get_key_value(&span.id()))); + if let Some((id, data)) = result { + let id = id.clone(); + if Filter::enabled(&data.filter, event.metadata(), &cx) { + let content = match get_message(event) { + Some(s) => s, + None => return, + }; + + let normalized_metadata = event.normalized_metadata(); + let meta = normalized_metadata + .as_ref() + .unwrap_or_else(|| event.metadata()); + + let level = *meta.level(); + let module = meta.module_path().map(<_>::to_owned); + + let node_log = cx.event_scope(event).and_then(|mut iter| { + iter.find_map(|span| span.extensions().get::().cloned()) + }); + + let time = Utc::now(); + let log = match node_log { + None => Event::FlowLog(FlowLog { + time, + level: level.into(), + module, + content, + }), + Some(NodeLogSpan { node_id, times }) => Event::NodeLog(NodeLog { + time, + node_id, + times, + level: level.into(), + module, + content, + }), + }; + + if data.tx.unbounded_send(log).is_err() { + drop(map); + + self.map.write().unwrap().remove(&id); + } + } + } + } +} + +pub struct IgnoreFlowLogs { + map: Map, +} + +impl IgnoreFlowLogs { + pub fn new(map: Map) -> Self { + Self { map } + } +} + +impl Filter for IgnoreFlowLogs +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn enabled( + &self, + meta: &tracing::Metadata<'_>, + cx: &tracing_subscriber::layer::Context<'_, S>, + ) -> bool { + if meta.is_span() { + true + } else { + let map = self.map.read().unwrap(); + let data = cx.lookup_current().and_then(|span| { + cx.span_scope(&span.id()) + .and_then(|mut iter| iter.find_map(|span| map.get(&span.id()))) + }); + data.is_none() + } + } +} diff --git a/crates/flow-server/src/lib.rs b/crates/flow-server/src/lib.rs new file mode 100644 index 00000000..5a6230da --- /dev/null +++ b/crates/flow-server/src/lib.rs @@ -0,0 +1,566 @@ +use actix_web::http::header::{HeaderName, HeaderValue, AUTHORIZATION}; +use anyhow::Context; +use db::{ + config::{DbConfig, ProxiedDbConfig}, + pool::DbPool, +}; +use either::Either; +use flow_lib::config::Endpoints; +use middleware::{ + auth, + req_fn::{self, Function, ReqFn}, +}; +use serde::Deserialize; +use solana_client::nonblocking::rpc_client::RpcClient; +use std::{path::PathBuf, rc::Rc}; +use url::Url; +use user::SignatureAuth; + +pub mod api; +pub mod db_worker; +pub mod error; +pub mod flow_logs; +pub mod middleware; +pub mod user; +pub mod ws; + +pub fn match_wildcard(pat: &str, origin: &HeaderValue) -> bool { + let Ok(mut origin_str) = origin.to_str() else { + return false; + }; + + let mut segments = pat.split('*'); + + let Some(first) = segments.next() else { + return false; + }; + origin_str = match origin_str.strip_prefix(first) { + Some(s) => s, + None => return false, + }; + + for s in segments { + if s.is_empty() { + continue; + } + match origin_str.find(s) { + Some(pos) => { + let wildcard = &origin_str[..pos]; + if !wildcard.chars().all(|c| c.is_ascii_alphanumeric()) { + return false; + } + origin_str = &origin_str[pos..]; + } + None => { + return false; + } + } + } + + true +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum EndpointConfigUnchecked { + ProjectId { project_id: String }, + Endpoint { endpoint: Url }, +} + +#[derive(Deserialize, Clone)] +#[serde(try_from = "EndpointConfigUnchecked")] +pub struct EndpointConfig { + url: Url, +} + +impl TryFrom for EndpointConfig { + type Error = url::ParseError; + fn try_from(value: EndpointConfigUnchecked) -> Result { + Ok(match value { + EndpointConfigUnchecked::Endpoint { endpoint } => Self { url: endpoint }, + EndpointConfigUnchecked::ProjectId { project_id } => Self { + url: format!("https://{}.supabase.co", project_id).parse()?, + }, + }) + } +} + +impl Default for EndpointConfig { + fn default() -> Self { + Self { + // default location of Supabase CLI local development + url: "http://localhost:54321".parse().unwrap(), + } + } +} + +#[derive(Deserialize, Clone)] +pub struct SupabaseConfig { + #[serde(flatten)] + pub endpoint: EndpointConfig, + pub jwt_key: Option, + pub anon_key: String, + pub service_key: Option, + #[serde(default = "SupabaseConfig::default_bucket")] + pub wasm_bucket: String, + #[serde(default = "SupabaseConfig::default_open_whitelists")] + pub open_whitelists: bool, +} + +impl SupabaseConfig { + pub fn default_bucket() -> String { + "node-files".to_owned() + } + + pub fn default_open_whitelists() -> bool { + false + } + + pub fn get_endpoint(&self) -> Url { + self.endpoint.url.clone() + } +} + +impl Default for SupabaseConfig { + fn default() -> Self { + Self { + endpoint: <_>::default(), + jwt_key: None, + anon_key: String::new(), + service_key: None, + wasm_bucket: Self::default_bucket(), + open_whitelists: Self::default_open_whitelists(), + } + } +} + +fn default_db_config() -> Either { + either::Right(ProxiedDbConfig { + upstream_url: "https://dev-api.spaceoperator.com".parse().unwrap(), + api_keys: Vec::new(), + }) +} + +#[derive(Deserialize)] +pub struct Config { + #[serde(default = "Config::default_host")] + pub host: String, + #[serde(default = "Config::default_port")] + pub port: u16, + #[serde(default = "default_db_config", with = "either::serde_untagged")] + pub db: Either, + #[serde(default)] + pub cors_origins: Vec, + pub supabase: SupabaseConfig, + #[serde(default = "Config::default_local_storage")] + pub local_storage: PathBuf, + #[serde(default = "Config::default_shutdown_timeout_secs")] + pub shutdown_timeout_secs: u16, + pub helius_api_key: Option, + pub solana: Option, + + #[serde(skip)] + blake3_key: [u8; blake3::KEY_LEN], +} + +#[derive(Deserialize, Default)] +pub struct SolanaConfig { + pub mainnet_url: Option, + pub devnet_url: Option, + pub testnet_url: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + host: Self::default_host(), + port: Self::default_port(), + db: default_db_config(), + cors_origins: Vec::new(), + supabase: SupabaseConfig::default(), + local_storage: Self::default_local_storage(), + shutdown_timeout_secs: Self::default_shutdown_timeout_secs(), + blake3_key: rand::random(), + solana: None, + helius_api_key: None, + } + } +} + +impl Config { + pub fn default_host() -> String { + "127.0.0.1".to_owned() + } + + pub const fn default_port() -> u16 { + 8080 + } + + pub fn default_local_storage() -> PathBuf { + PathBuf::from("./local_storage") + } + + pub const fn default_shutdown_timeout_secs() -> u16 { + 1 + } + + pub fn get_config() -> Self { + match std::env::args().nth(1) { + Some(s) => if s == "-" { + use std::io::Read; + let mut buf = String::new(); + std::io::stdin() + .read_to_string(&mut buf) + .map_err(|error| { + tracing::error!("Error reading STDIN: {}", error); + }) + .map(move |_| buf) + } else { + std::fs::read_to_string(s).map_err(|error| { + tracing::error!("Error reading config: {}", error); + }) + } + .and_then(|s| { + toml::from_str(&s).map_err(|error| { + tracing::error!("Error parsing config: {}", error); + }) + }) + .map_err(|_| { + tracing::warn!("Invalid config file, using default"); + }) + .unwrap_or_default(), + None => { + tracing::info!("No config specified, using default"); + Config::default() + } + } + } + + pub async fn healthcheck(&self) -> Result<(), Vec> { + let mut errors = Vec::new(); + if let Some(key) = &self.helius_api_key { + let client = RpcClient::new(format!("https://mainnet.helius-rpc.com/?api-key={}", key)); + client + .get_version() + .await + .context("Helius mainnet failed") + .map_err(|error| errors.push(error)) + .ok(); + let client = RpcClient::new(format!("https://devnet.helius-rpc.com/?api-key={}", key)); + client + .get_version() + .await + .context("Helius devnet failed") + .map_err(|error| errors.push(error)) + .ok(); + } + if let Some(url) = self.solana.as_ref().and_then(|s| s.mainnet_url.as_ref()) { + let client = RpcClient::new(url.to_string()); + client + .get_version() + .await + .context("Solana mainnet failed") + .map_err(|error| errors.push(error)) + .ok(); + } + if let Some(url) = self.solana.as_ref().and_then(|s| s.devnet_url.as_ref()) { + let client = RpcClient::new(url.to_string()); + client + .get_version() + .await + .context("Solana devnet failed") + .map_err(|error| errors.push(error)) + .ok(); + } + if let Some(url) = self.solana.as_ref().and_then(|s| s.testnet_url.as_ref()) { + let client = RpcClient::new(url.to_string()); + client + .get_version() + .await + .context("Solana testnet failed") + .map_err(|error| errors.push(error)) + .ok(); + } + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } + + pub fn endpoints(&self) -> Endpoints { + Endpoints { + flow_server: match &self.db { + Either::Right(cfg) => cfg.upstream_url.to_string(), + _ => format!("http://localhost:{}", self.port), + }, + supabase: self.supabase_endpoint(), + supabase_anon_key: self.supabase.anon_key.clone(), + } + } + + fn supabase_endpoint(&self) -> String { + // TODO: strip / to avoid breaking changes + let mut s = self.supabase.get_endpoint().to_string(); + if s.ends_with('/') { + s.pop(); + } + s + } + + /// Build a CORS middleware. + pub fn cors(&self) -> actix_cors::Cors { + let mut cors = actix_cors::Cors::default() + .allow_any_header() + .allow_any_method() + .supports_credentials(); + if self.cors_origins.iter().any(|v| v == "*") { + cors = cors.allow_any_origin(); + } else { + for origin in &self.cors_origins { + if origin.contains('*') { + let pattern = origin.clone(); + cors = + cors.allowed_origin_fn(move |origin, _| match_wildcard(&pattern, origin)); + } else { + cors = cors.allowed_origin(origin); + } + } + } + cors + } + + pub fn signature_auth(&self) -> SignatureAuth { + SignatureAuth::new(self.blake3_key) + } + + /// Build a middleware to validate `Authorization` header + /// with Supabase's JWT secret and API key. + pub fn all_auth(&self, pool: DbPool) -> auth::ApiAuth { + match (self.supabase.jwt_key.as_ref(), pool) { + (Some(key), DbPool::Real(pool)) => auth::ApiAuth::real( + key.as_bytes(), + self.supabase.anon_key.clone(), + pool, + self.signature_auth(), + ), + (None, DbPool::Real(pool)) => { + // TODO: print error + auth::ApiAuth::real( + &[], + self.supabase.anon_key.clone(), + pool, + self.signature_auth(), + ) + } + (_, DbPool::Proxied(pool)) => auth::ApiAuth::proxied(pool, self.signature_auth()), + } + } + + /// Build a middleware to validate `apikey` header + /// with Supabase's anon key. + pub fn anon_key(&self) -> ReqFn> { + let key = self.supabase.anon_key.clone(); + let name = HeaderName::from_static("apikey"); + req_fn::rc_fn_ref(move |r| match r.headers().get(&name) { + Some(v) if key.as_bytes() == v => Ok(()), + _ => Err(error::ApiKey), + }) + } + + /// Build a middleware to validate `Authorization` header + /// with Supabase's service-role key. + pub fn service_key(&self) -> Option>> { + let key = self.supabase.service_key.clone()?; + Some(req_fn::rc_fn_ref(move |r| { + match r.headers().get(&AUTHORIZATION) { + Some(v) => { + if v.as_bytes().strip_prefix(b"Bearer ") == Some(key.as_bytes()) { + Ok(()) + } else { + Err(error::ApiKey) + } + } + _ => Err(error::ApiKey), + } + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use flow::{flow_run_events::event_channel, FlowGraph}; + use flow_lib::{command::CommandDescription, config::client::ClientConfig, FlowConfig}; + use value::Value; + + use cmds_solana as _; + use cmds_std as _; + + #[derive(Deserialize)] + struct TestFile { + flow: ClientConfig, + } + + #[test] + fn cors_wildcard() { + assert!(match_wildcard( + "https://flow-git-*-space-operator.vercel.app", + &HeaderValue::from_static("https://flow-git-master-space-operator.vercel.app"), + )); + assert!(match_wildcard( + "https://flow-git-*-space-operator.vercel.app", + &HeaderValue::from_static("https://flow-git-flows-space-operator.vercel.app"), + )); + assert!(match_wildcard( + "https://flow-*-space-operator.vercel.app", + &HeaderValue::from_static("https://flow-qv9tx6vxs-space-operator.vercel.app"), + )); + assert!(!match_wildcard( + "https://flow-*-space-operator.vercel.app", + &HeaderValue::from_static("https://flow-qv9tx6vxs-fake-space-operator.vercel.app"), + )); + } + + #[tokio::test] + async fn test_generate_keypair() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!("../../../test_files/generate_keypair.json"); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + dbg!(&res.output); + dbg!(&res.node_errors); + assert_eq!( + res.output["key"], + Value::new_keypair_bs58("3LUpzbebV5SCftt8CPmicbKxNtQhtJegEz4n8s6LBf3b1s4yfjLapgJhbMERhP73xLmWEP2XJ2Rz7Y3TFiYgTpXv").unwrap(), + ); + // balance might change on solana devnet, so don't assert value here + assert!(res.output.contains_key("balance")); + assert!(res.node_errors.is_empty()); + assert!(res.not_run.is_empty()); + println!( + "output: {}", + serde_json::to_string_pretty(&res.output).unwrap() + ); + } + + #[tokio::test] + async fn test_const_form_data() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!("../../../test_files/const_form_data.json"); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + assert!(res.node_errors.is_empty()); + // TODO: check output values + } + + #[tokio::test] + async fn test_foreach() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!("../../../test_files/foreach.json"); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + assert_eq!(res.output["keypairs"], Value::Array([ + Value::new_keypair_bs58("3LUpzbebV5SCftt8CPmicbKxNtQhtJegEz4n8s6LBf3b1s4yfjLapgJhbMERhP73xLmWEP2XJ2Rz7Y3TFiYgTpXv").unwrap(), + Value::new_keypair_bs58("5WmnrZDv6oM4tkN5SfSTf5MGyPLPV4HjHGQZN4JiBDCxkcetz5LTYYhRwNxKXY5BCWBaVYALZ2GkpBpU5uRr2jMa").unwrap(), + Value::new_keypair_bs58("XunqA3LMMvpjD1JH9HMp2eSmvEaSoTdGhnNrseoFW9rMsSRhVefZYcTRDdfgVxoyJJvLwF2gzV4zyYMGiAoJaSS").unwrap(), + ].to_vec())); + } + + #[tokio::test] + async fn test_file_upload() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!("../../../test_files/file_upload.json"); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + dbg!(res); + } + + + #[tokio::test] + async fn test_flow_input() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!("../../../test_files/HTTP Request.json"); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + dbg!(res); + } + + #[test] + fn test_name_unique() { + let mut m = std::collections::HashSet::new(); + let mut dup = false; + for CommandDescription { name, .. } in inventory::iter::() { + if !m.insert(name) { + println!("Dupicated: {}", name); + dup = true; + } + } + assert!(!dup); + } +} diff --git a/crates/flow-server/src/main.rs b/crates/flow-server/src/main.rs new file mode 100644 index 00000000..b5d4ddae --- /dev/null +++ b/crates/flow-server/src/main.rs @@ -0,0 +1,298 @@ +use actix::Actor; +use actix_web::{ + middleware::{Compress, Logger}, + web, App, HttpServer, +}; +use db::{ + pool::{DbPool, ProxiedDbPool, RealDbPool}, + LocalStorage, WasmStorage, +}; +use either::Either; +use flow_server::{ + api::{self, prelude::Success}, + db_worker::{token_worker::token_from_apikeys, DBWorker, SystemShutdown}, + flow_logs, + middleware::auth_v1, + user::SupabaseAuth, + ws, Config, +}; +use futures_util::{future::ok, TryFutureExt}; +use std::{borrow::Cow, collections::BTreeSet, convert::Infallible, sync::Arc, time::Duration}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer}; +use utils::address_book::AddressBook; + +// avoid commands being optimized out by the compiler +use cmds_pdg as _; +use cmds_solana as _; +use cmds_std as _; + +#[actix::main] +async fn main() { + let (flow_logs, tracing_data) = flow_logs::FlowLogs::new(); + tracing_subscriber::Registry::default() + .with( + tracing_subscriber::fmt::layer() + .with_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_filter(flow_logs::IgnoreFlowLogs::new(tracing_data.clone())), + ) + .with(flow_logs) + .init(); + + let config = Config::get_config(); + + if let Err(errors) = config.healthcheck().await { + for error in errors { + tracing::error!("{}", error); + } + } + + let fac = flow::context::CommandFactory::new(); + let natives = fac.natives.keys().collect::>(); + tracing::info!("native commands: {:?}", natives); + + tracing::info!("allow CORS origins: {:?}", config.cors_origins); + + let wasm_storage = match WasmStorage::new( + config.supabase.get_endpoint(), + &config.supabase.anon_key, + &config.supabase.wasm_bucket, + ) { + Ok(db) => db, + Err(e) => { + tracing::error!("failed to build storage client: {}", e); + return; + } + }; + + let local = match LocalStorage::new(&config.local_storage) { + Ok(local) => local, + Err(e) => { + tracing::error!( + "failed to open local storage {:?}: {}", + config.local_storage.display(), + e + ); + return; + } + }; + + let mut actors = AddressBook::new(); + + let pool_size = if let Either::Left(cfg) = &config.db { + cfg.max_pool_size + } else { + None + }; + + let db = match match &config.db { + Either::Left(cfg) => RealDbPool::new(cfg, wasm_storage.clone(), local) + .await + .map(DbPool::Real), + Either::Right(cfg) => { + let (services, new_actors) = token_from_apikeys( + cfg.api_keys.clone(), + local.clone(), + config.endpoints(), + cfg.upstream_url.to_string(), + ) + .await; + for (id, addr) in new_actors { + assert!(actors.insert(id, addr.downgrade())); + } + ProxiedDbPool::new(cfg.clone(), local, services).map(DbPool::Proxied) + } + } { + Ok(db) => db, + Err(e) => { + tracing::error!("failed to start database connection pool: {}", e); + return; + } + }; + + if let DbPool::Real(db) = &db { + let res = db + .get_admin_conn() + .and_then(move |conn| async move { + let names = conn.get_natives_commands().await?; + let mut missing = BTreeSet::new(); + for name in names { + if !natives.contains(&&Cow::Borrowed(name.as_str())) + && !rhai_script::is_rhai_script(&name) + { + missing.insert(name); + } + } + Ok(missing) + }) + .await; + match res { + Ok(missing) => { + if !missing.is_empty() { + tracing::warn!("missing native commands: {:?}", missing); + } + } + Err(error) => { + tracing::error!("{}", error); + } + } + } + + let db_worker = + DBWorker::create(|ctx| DBWorker::new(db.clone(), &config, actors, tracing_data, ctx)); + + let sig_auth = config.signature_auth(); + let supabase_auth = match SupabaseAuth::new(&config.supabase, db.clone()) { + Ok(c) => Some(c), + Err(e) => { + tracing::warn!("missing credentials, some routes are not available: {}", e); + None + } + }; + + let host = config.host.clone(); + let port = config.port; + + tracing::info!("listening on {:?} port {:?}", host, port); + + let root = db_worker.clone(); + + let shutdown_timeout_secs = config.shutdown_timeout_secs; + + if let Some(key) = &config.helius_api_key { + std::env::set_var("HELIUS_API_KEY", key); + } + if let Some(solana) = &config.solana { + if let Some(url) = &solana.devnet_url { + std::env::set_var("SOLANA_DEVNET_URL", url.to_string()); + } + if let Some(url) = &solana.testnet_url { + std::env::set_var("SOLANA_TESTNET_URL", url.to_string()); + } + if let Some(url) = &solana.mainnet_url { + std::env::set_var("SOLANA_MAINNET_URL", url.to_string()); + } + } + + let config = Arc::new(config); + let mut server = HttpServer::new(move || { + let wallets = supabase_auth.as_ref().map(|supabase_auth| { + web::scope("/wallets") + .app_data(web::Data::new(supabase_auth.clone())) + .service(api::upsert_wallet::service(&config, db.clone())) + }); + + let auth = supabase_auth.as_ref().map(|supabase_auth| { + web::scope("/auth") + .app_data(web::Data::new(sig_auth)) + .app_data(web::Data::new(supabase_auth.clone())) + .service(api::claim_token::service(&config, db.clone())) + .service(api::init_auth::service(&config)) + .service(api::confirm_auth::service(&config)) + }); + + let mut flow = web::scope("/flow") + .service(api::start_flow::service(&config, db.clone())) + .service(api::stop_flow::service(&config, db.clone())) + .service(api::start_flow_shared::service(&config, db.clone())) + .service(api::clone_flow::service(&config, db.clone())) + .service(api::get_flow_output::service(&config, db.clone())) + .service(api::get_signature_request::service(&config, db.clone())) + .service(api::deploy_flow::service(&config)); + if let Some(supabase_auth) = &supabase_auth { + flow = flow.service(api::start_flow_unverified::service( + &config, + db.clone(), + web::Data::new(supabase_auth.clone()), + )) + } + + let websocket = web::scope("/ws").service(ws::service(&config, db.clone())); + let signature = web::scope("/signature").service(api::submit_signature::service(&config)); + + let healthcheck = web::resource("/healthcheck") + .route(web::get().to(|()| ok::<_, Infallible>(web::Json(Success)))); + let apikeys = web::scope("/apikey") + .service(api::create_apikey::service(&config, db.clone())) + .service(api::delete_apikey::service(&config, db.clone())) + .service(api::apikey_info::service(&config)); + let kvstore = web::scope("/kv") + .service(api::kvstore::create_store::service(&config, db.clone())) + .service(api::kvstore::delete_store::service(&config, db.clone())) + .service(api::kvstore::write_item::service(&config, db.clone())) + .service(api::kvstore::delete_item::service(&config, db.clone())) + .service(api::kvstore::read_item::service(&config, db.clone())); + + let db_proxy = if matches!(db, DbPool::Real(_)) { + Some( + web::scope("/proxy") + .service(api::db_rpc::service(&config, db.clone())) + .service(api::db_push_logs::service(&config, db.clone())) + .service(api::auth_proxy::service(&config, db.clone())) + .service(api::ws_auth_proxy::service(&config, db.clone())), + ) + } else { + None + }; + + let deployment = web::scope("/deployment").service(api::start_deployment::service(&config)); + + let mut app = App::new() + .wrap(Compress::default()) + .wrap(Logger::new(r#""%r" %s %b %{content-encoding}o %Dms"#).exclude("/healthcheck")) + .app_data(web::Data::new(db.clone())) + .app_data(web::Data::new(db_worker.clone())) + .configure(|cfg| auth_v1::configure(cfg, &config, &db)) + .app_data(web::Data::new(sig_auth)); + if let Some(auth) = supabase_auth.clone() { + app = app.app_data(web::Data::new(auth)); + } + + let mut app = match &db { + DbPool::Real(db) => app.app_data(web::Data::new(db.clone())), + DbPool::Proxied(db) => app.app_data(web::Data::new(db.clone())), + }; + + if let Some(wallets) = wallets { + app = app.service(wallets); + } + + if let Some(auth) = auth { + app = app.service(auth); + } + + if let Some(db_proxy) = db_proxy { + app = app.service(db_proxy); + } + + let data = { + let mut svc = + web::scope("/data").service(api::data_export::service(&config, db.clone())); + #[cfg(feature = "import")] + if let Some(import) = api::data_import::service(&config) { + svc = svc.service(import); + } + svc + }; + + app.service(flow) + .service(data) + .service(signature) + .service(apikeys) + .service(websocket) + .service(kvstore) + .service(healthcheck) + .service(api::get_info::service(&config)) + .service(deployment) + }) + .shutdown_timeout(shutdown_timeout_secs as u64); + if let Some(pool_size) = pool_size { + server = server.workers((pool_size / 2).max(4)); + } + server.bind((host, port)).unwrap().run().await.unwrap(); + + root.send(SystemShutdown { + timeout: Duration::from_secs(shutdown_timeout_secs as u64), + }) + .await + .unwrap(); +} diff --git a/crates/flow-server/src/middleware/auth.rs b/crates/flow-server/src/middleware/auth.rs new file mode 100644 index 00000000..43d51630 --- /dev/null +++ b/crates/flow-server/src/middleware/auth.rs @@ -0,0 +1,499 @@ +use crate::{ + api::{auth_proxy, ws_auth_proxy}, + error::ErrorBody, + user::{SignatureAuth, FLOW_RUN_TOKEN_PREFIX}, +}; +use actix_web::{ + body::EitherBody, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::{ + header::{ + from_one_raw_str, Header, HeaderName, HeaderValue, InvalidHeaderValue, + TryIntoHeaderValue, AUTHORIZATION, + }, + StatusCode, + }, + HttpMessage, HttpResponse, ResponseError, +}; +use db::{ + apikey, + pool::{ProxiedDbPool, RealDbPool}, +}; +use flow_lib::{FlowRunId, UserId}; +use futures_util::{future::LocalBoxFuture, FutureExt}; +use hmac::{Hmac, Mac}; +use reqwest::header as req; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::{convert::Infallible, future::Ready, panic::Location, rc::Rc, str::FromStr, sync::Arc}; +use thiserror::Error as ThisError; +use utils::bs58_decode; + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub enum TokenType { + JWT(JWTPayload), + ApiKey(JWTPayload), + FlowRun(FlowRunToken), +} + +impl TokenType { + pub fn is_user(&self, user_id: UserId) -> bool { + match self { + TokenType::JWT(x) | TokenType::ApiKey(x) => x.user_id == user_id, + TokenType::FlowRun(_) => false, + } + } + + pub fn is_flow_run(&self, flow_run_id: FlowRunId) -> bool { + match self { + TokenType::JWT(_) | TokenType::ApiKey(_) => false, + TokenType::FlowRun(x) => x.id == flow_run_id, + } + } + + pub fn user_id(&self) -> Option { + match self { + TokenType::JWT(x) | TokenType::ApiKey(x) => Some(x.user_id), + TokenType::FlowRun(_) => None, + } + } + + pub fn flow_run_id(&self) -> Option { + match self { + TokenType::FlowRun(x) => Some(x.id), + TokenType::JWT(_) | TokenType::ApiKey(_) => None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct FlowRunToken { + pub id: FlowRunId, +} + +pub static X_API_KEY: HeaderName = HeaderName::from_static("x-api-key"); + +pub struct ApiKey(pub String); + +impl ApiKey { + pub fn into_inner(self) -> String { + self.0 + } +} + +impl FromStr for ApiKey { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_owned())) + } +} + +impl TryIntoHeaderValue for ApiKey { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + HeaderValue::from_str(&self.0) + } +} + +impl Header for ApiKey { + fn name() -> HeaderName { + X_API_KEY.clone() + } + + fn parse(msg: &M) -> Result { + from_one_raw_str(msg.headers().get(Self::name())) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Token { + pub api_key: Option, + pub jwt: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct JWTPayload { + pub user_id: UserId, + #[serde(with = "utils::serde_bs58")] + pub pubkey: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct Unverified { + #[serde(with = "utils::serde_bs58")] + pub pubkey: [u8; 32], +} + +#[derive(ThisError, Debug)] +#[error("failed to verify access token, at {0}")] +pub struct Unauthorized(&'static Location<'static>); + +#[track_caller] +fn unauthorized() -> Unauthorized { + Unauthorized(Location::caller()) +} + +impl ResponseError for Unauthorized { + fn status_code(&self) -> StatusCode { + StatusCode::UNAUTHORIZED + } + + fn error_response(&self) -> HttpResponse { + ErrorBody::build(self) + } +} + +fn rsplit(b: &[u8]) -> Option<(&[u8], &[u8])> { + let dot = b.iter().rposition(|c| *c == b'.')?; + Some((&b[..dot], &b[dot + 1..])) +} + +fn split(b: &[u8]) -> Option<(&[u8], &[u8])> { + let dot = b.iter().position(|c| *c == b'.')?; + Some((&b[..dot], &b[dot + 1..])) +} + +fn jwt_verify(mut hmac: Hmac, token: &[u8], now: i64) -> Result { + let (header_payload, signature) = rsplit(token).ok_or_else(unauthorized)?; + + let signature = + base64::decode_config(signature, base64::URL_SAFE).map_err(|_| unauthorized())?; + hmac.update(header_payload); + hmac.verify_slice(&signature).map_err(|_| unauthorized())?; + let (_, payload) = split(header_payload).ok_or_else(unauthorized)?; + let payload = decode_payload(payload, now)?; + Ok(payload) +} + +#[derive(Deserialize)] +struct Payload<'a> { + exp: i64, + #[allow(dead_code)] + role: Role, + sub: UserId, + #[serde(borrow)] + user_metadata: UserMetadata<'a>, +} + +#[derive(Deserialize)] +struct UserMetadata<'a> { + pub_key: &'a str, +} + +#[derive(Deserialize)] +enum Role { + #[serde(rename = "authenticated")] + Authenticated, +} + +fn decode_payload(payload: &[u8], now: i64) -> Result { + let s = base64::decode_config(payload, base64::URL_SAFE).map_err(|_| unauthorized())?; + let p = serde_json::from_slice::(&s).map_err(|_| unauthorized())?; + + if p.exp <= now { + return Err(unauthorized()); + } + + let mut pubkey = [0u8; 32]; + let size = bs58::decode(p.user_metadata.pub_key) + .into(&mut pubkey) + .map_err(|_| unauthorized())?; + if size != 32 { + return Err(unauthorized()); + } + + Ok(JWTPayload { + user_id: p.sub, + pubkey, + }) +} + +#[derive(Clone)] +pub enum ApiAuth { + Real(RealApiAuth), + Proxied(ProxiedApiAuth), +} + +#[derive(Clone)] +pub struct RealApiAuth { + hmac: Hmac, + anon_key: String, + pool: RealDbPool, + sig: SignatureAuth, +} + +fn decode_base58_pubkey(v: &HeaderValue) -> Result<[u8; 32], Unauthorized> { + let s = std::str::from_utf8(v.as_bytes()).map_err(|_| unauthorized())?; + bs58_decode::<32>(s).map_err(|_| unauthorized()) +} + +fn verify_flow_run_token(token: &[u8], sig: SignatureAuth) -> Result { + let mut bytes = [0u8; 48]; + let written = base64::decode_config_slice(token, base64::URL_SAFE_NO_PAD, &mut bytes) + .map_err(|_| unauthorized())?; + if written != bytes.len() { + return Err(unauthorized()); + } + + let hash = sig.hash(&bytes[..16]); + if hash == blake3::Hash::from_bytes(bytes[16..].try_into().unwrap()) { + Ok(FlowRunId::from_bytes(bytes[..16].try_into().unwrap())) + } else { + Err(unauthorized()) + } +} + +impl RealApiAuth { + async fn run(&self, r: &ServiceRequest) -> Result<(), Unauthorized> { + match r.headers().get("x-api-key") { + Some(apikey) => { + if apikey.as_bytes() == self.anon_key.as_bytes() { + self.jwt_verify_request(r, chrono::Utc::now().timestamp()) + } else { + self.apikey_auth(apikey, r).await + } + } + None => self.jwt_verify_request(r, chrono::Utc::now().timestamp()), + } + } + + fn jwt_verify_request(&self, r: &ServiceRequest, now: i64) -> Result<(), Unauthorized> { + let header = r.headers().get(&AUTHORIZATION).ok_or_else(unauthorized)?; + + let bytes = header.as_bytes(); + + if let Some(token) = bytes.strip_prefix(b"Bearer ") { + let payload = jwt_verify(self.hmac.clone(), token, now)?; + + let mut ext = r.extensions_mut(); + ext.insert(payload); + ext.insert(Token { + jwt: Some(String::from_utf8(token.to_owned()).map_err(|_| unauthorized())?), + api_key: None, + }); + ext.insert(TokenType::JWT(payload)); + Ok(()) + } else if let Some(token) = bytes.strip_prefix(FLOW_RUN_TOKEN_PREFIX.as_bytes()) { + let id = verify_flow_run_token(token, self.sig)?; + let mut ext = r.extensions_mut(); + ext.insert(FlowRunToken { id }); + ext.insert(TokenType::FlowRun(FlowRunToken { id })); + Ok(()) + } else { + let pubkey = decode_base58_pubkey(header)?; + let mut ext = r.extensions_mut(); + ext.insert(Unverified { pubkey }); + Ok(()) + } + } + + async fn apikey_auth( + &self, + apikey: &HeaderValue, + r: &ServiceRequest, + ) -> Result<(), Unauthorized> { + let apikey = apikey.to_str().map_err(|_| unauthorized())?; + if !apikey.starts_with(apikey::KEY_PREFIX) { + return Err(unauthorized()); + } + let conn = self + .pool + .get_admin_conn() + .await + .map_err(|_| unauthorized())?; + let user = conn + .get_user_from_apikey(apikey) + .await + .map_err(|_| unauthorized())?; + let payload = JWTPayload { + pubkey: user.pubkey, + user_id: user.user_id, + }; + let mut ext = r.extensions_mut(); + ext.insert(payload); + ext.insert(Token { + jwt: None, + api_key: Some(apikey.to_owned()), + }); + ext.insert(TokenType::ApiKey(payload)); + Ok(()) + } + + pub async fn ws_authenticate(&self, token: String) -> Result { + if token.starts_with(apikey::KEY_PREFIX) { + let conn = self + .pool + .get_admin_conn() + .await + .map_err(|_| unauthorized())?; + let user = conn + .get_user_from_apikey(&token) + .await + .map_err(|_| unauthorized())?; + Ok(TokenType::ApiKey(JWTPayload { + pubkey: user.pubkey, + user_id: user.user_id, + })) + } else if let Some(token) = token.strip_prefix(FLOW_RUN_TOKEN_PREFIX) { + let id = verify_flow_run_token(token.as_bytes(), self.sig)?; + Ok(TokenType::FlowRun(FlowRunToken { id })) + } else { + Ok(TokenType::JWT(jwt_verify( + self.hmac.clone(), + token + .as_bytes() + .strip_prefix(b"Bearer ") + .unwrap_or(token.as_bytes()), + chrono::Utc::now().timestamp(), + )?)) + } + } +} + +#[derive(Clone)] +pub struct ProxiedApiAuth { + client: reqwest::Client, + upstream_url: String, + sig: SignatureAuth, +} + +impl ProxiedApiAuth { + async fn run(&self, r: &ServiceRequest) -> Result<(), Unauthorized> { + let mut req = self + .client + .post(format!("{}/proxy/auth", self.upstream_url)); + if let Some(value) = r.headers().get(AUTHORIZATION) { + if let Ok(pubkey) = decode_base58_pubkey(value) { + let mut ext = r.extensions_mut(); + ext.insert(Unverified { pubkey }); + return Ok(()); + } + req = req.header(req::AUTHORIZATION, value.as_bytes()); + } + if let Some(value) = r.headers().get(&X_API_KEY) { + req = req.header(X_API_KEY.as_str(), value.as_bytes()); + } + let output = req + .send() + .await + .map_err(|_| unauthorized())? + .json::() + .await + .map_err(|_| unauthorized())?; + let mut ext = r.extensions_mut(); + ext.insert(output.payload); + ext.insert(output.token); + Ok(()) + } + + pub async fn ws_authenticate(&self, token: String) -> Result { + if let Some(token) = token.strip_prefix(FLOW_RUN_TOKEN_PREFIX) { + let id = verify_flow_run_token(token.as_bytes(), self.sig)?; + Ok(TokenType::FlowRun(FlowRunToken { id })) + } else { + Ok(self + .client + .post(format!("{}/proxy/ws_auth", self.upstream_url)) + .json(&ws_auth_proxy::Params { token }) + .send() + .await + .map_err(|_| unauthorized())? + .json::() + .await + .map_err(|_| unauthorized())? + .payload) + } + } +} + +impl ApiAuth { + pub fn real(secret: &[u8], anon_key: String, pool: RealDbPool, sig: SignatureAuth) -> Self { + let hmac = Hmac::new_from_slice(secret).unwrap(); + ApiAuth::Real(RealApiAuth { + hmac, + anon_key, + pool, + sig, + }) + } + + pub fn proxied(pool: ProxiedDbPool, sig: SignatureAuth) -> Self { + ApiAuth::Proxied(ProxiedApiAuth { + client: pool.client, + upstream_url: pool.config.upstream_url, + sig, + }) + } + + async fn run(&self, r: &ServiceRequest) -> Result<(), Unauthorized> { + match self { + ApiAuth::Real(x) => x.run(r).await, + ApiAuth::Proxied(x) => x.run(r).await, + } + } + + pub async fn ws_authenticate( + self: Arc, + token: String, + ) -> Result { + match self.as_ref() { + ApiAuth::Real(x) => x.ws_authenticate(token).await, + ApiAuth::Proxied(x) => x.ws_authenticate(token).await, + } + } +} + +impl Transform for ApiAuth +where + S: Service, Error = actix_web::Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Transform = ApiAuthMiddleware; + type Response = >::Response; + type Error = >::Error; + type Future = Ready>; + type InitError = (); + + fn new_transform(&self, s: S) -> Self::Future { + std::future::ready(Ok(ApiAuthMiddleware { + service: Rc::new(s), + state: self.clone(), + })) + } +} + +pub struct ApiAuthMiddleware { + service: Rc, + state: ApiAuth, +} + +impl Service for ApiAuthMiddleware +where + S: Service, Error = actix_web::Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, r: ServiceRequest) -> Self::Future { + let service = Rc::clone(&self.service); + let state = self.state.clone(); + async move { + match state.run(&r).await { + Ok(_) => service + .call(r) + .await + .map(ServiceResponse::::map_into_left_body), + Err(e) => Ok(r.error_response(e).map_into_right_body()), + } + } + .boxed_local() + } +} diff --git a/crates/flow-server/src/middleware/auth_v1.rs b/crates/flow-server/src/middleware/auth_v1.rs new file mode 100644 index 00000000..26da9eec --- /dev/null +++ b/crates/flow-server/src/middleware/auth_v1.rs @@ -0,0 +1,386 @@ +use crate::{ + error::ErrorBody, + user::{SignatureAuth, FLOW_RUN_TOKEN_PREFIX}, +}; +use actix_web::{ + http::{ + header::{HeaderName, AUTHORIZATION}, + StatusCode, + }, + web::{self, ServiceConfig}, + FromRequest, HttpRequest, ResponseError, +}; +use chrono::Utc; +use db::{ + apikey, + pool::{DbPool, RealDbPool}, +}; +use flow_lib::{FlowRunId, UserId}; +use futures_util::{future::LocalBoxFuture, FutureExt}; +use getset::Getters; +use hmac::{Hmac, Mac}; +use serde::Deserialize; +use sha2::Sha256; +use std::{ + future::Future, + ops::{ControlFlow, Deref}, +}; +use thiserror::Error as ThisError; + +macro_rules! early_return { + ($t:expr) => {{ + let c: ControlFlow<_, _> = $t; + if let ControlFlow::Break(b) = c { + return b; + } + }}; +} + +fn rsplit(b: &[u8]) -> Option<(&[u8], &[u8])> { + let dot = b.iter().rposition(|c| *c == b'.')?; + Some((&b[..dot], &b[dot + 1..])) +} + +trait Identity: Sized { + fn verify<'a>( + req: &'a HttpRequest, + auth: &'a web::ThinData, + ) -> impl Future> + 'a; +} + +#[derive(Deserialize)] +struct Payload<'a> { + exp: i64, + #[allow(dead_code)] + role: Role, + sub: UserId, + #[serde(borrow)] + user_metadata: UserMetadata<'a>, +} + +#[derive(Deserialize)] +struct UserMetadata<'a> { + pub_key: &'a str, +} + +#[derive(Deserialize)] +enum Role { + #[serde(rename = "authenticated")] + Authenticated, +} + +pub fn verify_jwt(mut hmac: Hmac, http_header: &[u8], now: i64) -> Result { + let token = http_header + .strip_prefix(b"Bearer ") + .ok_or(AuthError::InvalidFormat)?; + let (header_payload, signature) = rsplit(token).ok_or(AuthError::InvalidFormat)?; + let (_, payload) = rsplit(header_payload).ok_or(AuthError::InvalidFormat)?; + + let signature = base64::decode_config(signature, base64::URL_SAFE_NO_PAD) + .map_err(|_| AuthError::InvalidFormat)?; + hmac.update(header_payload); + hmac.verify_slice(&signature) + .map_err(|_| AuthError::HmacFailed)?; + + let bytes = base64::decode_config(payload, base64::URL_SAFE_NO_PAD) + .map_err(|_| AuthError::InvalidPayload)?; + let payload = + serde_json::from_slice::(&bytes).map_err(|_| AuthError::InvalidPayload)?; + if payload.exp <= now { + return Err(AuthError::Expired); + } + let mut pubkey = [0u8; 32]; + five8::decode_32(payload.user_metadata.pub_key, &mut pubkey) + .map_err(|_| AuthError::InvalidPayload)?; + + Err(AuthError::NotConfigured) +} + +#[derive(ThisError, Debug)] +#[error("unauthenticated")] +pub enum AuthError { + NotConfigured, + NoHeader(HeaderName), + InvalidFormat, + HmacFailed, + InvalidPayload, + Expired, + Db(#[from] db::Error), +} + +impl AuthError { + fn try_again(&self) -> bool { + match self { + AuthError::NotConfigured => false, + AuthError::NoHeader(_) => true, + AuthError::InvalidFormat => true, + AuthError::HmacFailed => false, + AuthError::InvalidPayload => false, + AuthError::Expired => false, + AuthError::Db(_) => false, + } + } +} + +impl ResponseError for AuthError { + fn status_code(&self) -> StatusCode { + match self { + AuthError::NotConfigured | AuthError::Db(_) => StatusCode::INTERNAL_SERVER_ERROR, + _ => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +pub fn configure(cfg: &mut ServiceConfig, server_config: &crate::Config, db: &DbPool) { + let Some(ref jwt_key) = server_config.supabase.jwt_key else { + return; + }; + let hmac = Hmac::new_from_slice(jwt_key.as_bytes()).unwrap(); + let DbPool::Real(pool) = db else { return }; + let sig = SignatureAuth::new(server_config.blake3_key); + let state = AuthState { + hmac, + pool: pool.clone(), + sig, + }; + cfg.app_data(web::ThinData(state)); +} + +#[derive(Clone)] +struct AuthState { + hmac: Hmac, + pool: RealDbPool, + sig: SignatureAuth, +} + +pub struct Jwt { + token: String, + user_id: UserId, + pubkey: [u8; 32], +} + +impl Identity for Jwt { + async fn verify<'a>( + req: &'a HttpRequest, + auth: &'a web::ThinData, + ) -> Result { + let http_header = req + .headers() + .get(AUTHORIZATION) + .ok_or_else(|| AuthError::NoHeader(AUTHORIZATION))? + .as_bytes(); + let now = Utc::now().timestamp(); + let token = http_header + .strip_prefix(b"Bearer ") + .ok_or(AuthError::InvalidFormat)?; + let token_str = + String::from_utf8(token.to_owned()).map_err(|_| AuthError::InvalidFormat)?; + let (header_payload, signature) = rsplit(token).ok_or(AuthError::InvalidFormat)?; + let (_, payload) = rsplit(header_payload).ok_or(AuthError::InvalidFormat)?; + + let signature = base64::decode_config(signature, base64::URL_SAFE_NO_PAD) + .map_err(|_| AuthError::InvalidFormat)?; + let mut hmac = auth.hmac.clone(); + hmac.update(header_payload); + hmac.verify_slice(&signature) + .map_err(|_| AuthError::HmacFailed)?; + + let bytes = base64::decode_config(payload, base64::URL_SAFE_NO_PAD) + .map_err(|_| AuthError::InvalidPayload)?; + let payload = + serde_json::from_slice::(&bytes).map_err(|_| AuthError::InvalidPayload)?; + if payload.exp <= now { + return Err(AuthError::Expired); + } + let mut pubkey = [0u8; 32]; + five8::decode_32(payload.user_metadata.pub_key, &mut pubkey) + .map_err(|_| AuthError::InvalidPayload)?; + + Ok(Self { + token: token_str, + user_id: payload.sub, + pubkey, + }) + } +} + +pub struct ApiKey { + key: String, + user_id: UserId, + pubkey: [u8; 32], +} + +static X_API_KEY: HeaderName = HeaderName::from_static("x-api-key"); + +impl Identity for ApiKey { + async fn verify<'a>( + req: &'a HttpRequest, + auth: &'a web::ThinData, + ) -> Result { + let key = req + .headers() + .get(&X_API_KEY) + .ok_or_else(|| AuthError::NoHeader(X_API_KEY.clone()))?; + let key = key.to_str().map_err(|_| AuthError::InvalidFormat)?; + if !key.starts_with(apikey::KEY_PREFIX) { + return Err(AuthError::InvalidFormat); + } + + let conn = auth.pool.get_admin_conn().await?; + let user = conn.get_user_from_apikey(&key).await?; + Ok(ApiKey { + key: key.to_owned(), + user_id: user.user_id, + pubkey: user.pubkey, + }) + } +} + +#[derive(Getters)] +pub struct Unverified { + #[getset(get = "pub")] + pubkey: [u8; 32], +} + +impl Identity for Unverified { + async fn verify<'a>( + req: &'a HttpRequest, + _: &'a web::ThinData, + ) -> Result { + let key = req + .headers() + .get(&AUTHORIZATION) + .ok_or_else(|| AuthError::NoHeader(AUTHORIZATION))? + .to_str() + .map_err(|_| AuthError::InvalidFormat)?; + let key = key.strip_prefix("Bearer ").unwrap_or(key); + let mut pubkey = [0u8; 32]; + five8::decode_32(key, &mut pubkey).map_err(|_| AuthError::InvalidFormat)?; + Ok(Unverified { pubkey }) + } +} + +pub struct FlowRunToken { + id: FlowRunId, +} + +impl Identity for FlowRunToken { + async fn verify<'a>( + req: &'a HttpRequest, + auth: &'a web::ThinData, + ) -> Result { + let key = req + .headers() + .get(&AUTHORIZATION) + .ok_or_else(|| AuthError::NoHeader(AUTHORIZATION))? + .to_str() + .map_err(|_| AuthError::InvalidFormat)?; + let key = key.strip_prefix("Bearer ").unwrap_or(key); + let key = key + .strip_prefix(FLOW_RUN_TOKEN_PREFIX) + .ok_or(AuthError::InvalidFormat)?; + let mut bytes = [0u8; 48]; + let written = base64::decode_config_slice(key, base64::URL_SAFE_NO_PAD, &mut bytes) + .map_err(|_| AuthError::InvalidPayload)?; + if written != bytes.len() { + return Err(AuthError::InvalidPayload); + } + let hash = auth.sig.hash(&bytes[..16]); + if hash == blake3::Hash::from_bytes(bytes[16..].try_into().unwrap()) { + Ok(FlowRunToken { + id: FlowRunId::from_bytes(bytes[..16].try_into().unwrap()), + }) + } else { + Err(AuthError::InvalidPayload) + } + } +} + +#[derive(Getters)] +pub struct AuthenticatedUser { + #[getset(get = "pub")] + user_id: UserId, + #[getset(get = "pub")] + pubkey: [u8; 32], +} + +impl Identity for AuthenticatedUser { + async fn verify<'a>( + req: &'a HttpRequest, + auth: &'a web::ThinData, + ) -> Result { + let result = Jwt::verify(req, auth).await.map(|x| Self { + user_id: x.user_id, + pubkey: x.pubkey, + }); + early_return!(control_flow(result)); + + ApiKey::verify(req, auth).await.map(|x| Self { + user_id: x.user_id, + pubkey: x.pubkey, + }) + } +} + +pub struct Auth(T); + +impl Deref for Auth { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromRequest for Auth +where + T: Identity, +{ + type Error = AuthError; + type Future = LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { + let req = req.clone(); + async move { + let auth = req + .app_data::>() + .ok_or_else(|| AuthError::NotConfigured)?; + T::verify(&req, auth).await.map(Auth) + } + .boxed_local() + } +} + +pub enum Auth2 { + One(One), + Two(Two), +} + +fn control_flow(r: Result) -> ControlFlow> { + if matches!(&r, Err(e) if e.try_again()) { + ControlFlow::Continue(()) + } else { + ControlFlow::Break(r) + } +} + +impl FromRequest for Auth2 { + type Error = AuthError; + type Future = LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { + let req = req.clone(); + async move { + let auth = req + .app_data::>() + .ok_or_else(|| AuthError::NotConfigured)?; + let result = One::verify(&req, auth).await.map(Auth2::One); + early_return!(control_flow(result)); + Two::verify(&req, auth).await.map(Auth2::Two) + } + .boxed_local() + } +} diff --git a/crates/flow-server/src/middleware/mod.rs b/crates/flow-server/src/middleware/mod.rs new file mode 100644 index 00000000..09c79de8 --- /dev/null +++ b/crates/flow-server/src/middleware/mod.rs @@ -0,0 +1,19 @@ +pub mod auth; +pub mod auth_v1; +pub mod req_fn; + +pub fn optional>( + x: Result, +) -> Result, actix_web::Error> { + match x { + Ok(x) => Ok(Some(x)), + Err(e) => { + let e: actix_web::Error = e.into(); + let j = e.as_error::(); + match j { + Some(&actix_web::error::JsonPayloadError::ContentType) => Ok(None), + _ => Err(e), + } + } + } +} diff --git a/crates/flow-server/src/middleware/req_fn.rs b/crates/flow-server/src/middleware/req_fn.rs new file mode 100644 index 00000000..ac538a1d --- /dev/null +++ b/crates/flow-server/src/middleware/req_fn.rs @@ -0,0 +1,103 @@ +use actix_web::{ + body::EitherBody, + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + Error, ResponseError, +}; +use futures_util::{ + future::{Either, MapOk}, + TryFutureExt, +}; +use std::{ + future::{ready, Ready}, + ops::Deref, + rc::Rc, +}; + +pub fn rc_fn_ref(f: F) -> ReqFn> +where + F: for<'r> Fn(&'r ServiceRequest) -> Result<(), E> + 'static, + E: ResponseError + 'static, +{ + ReqFn { + f: Rc::new(FnRef(f)), + } +} + +pub trait Function: 'static { + fn call(&self, req: ServiceRequest) -> Result; +} + +struct FnRef(F); + +impl Function for FnRef +where + F: for<'r> Fn(&'r ServiceRequest) -> Result<(), E> + 'static, + E: ResponseError + 'static, +{ + fn call(&self, mut req: ServiceRequest) -> Result { + match (self.0)(&mut req) { + Ok(_) => Ok(req), + Err(e) => Err(req.error_response(e)), + } + } +} + +pub struct ReqFn { + f: C, +} + +impl Transform for ReqFn +where + C: Deref + Clone + 'static, + F: Function + ?Sized, + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Transform = ReqFnMiddleware; + type Response = >::Response; + type Error = >::Error; + type Future = Ready>; + type InitError = (); + + fn new_transform(&self, s: S) -> Self::Future { + ready(Ok(ReqFnMiddleware { + s, + f: self.f.clone(), + })) + } +} + +pub struct ReqFnMiddleware { + f: C, + s: S, +} + +impl Service for ReqFnMiddleware +where + C: Deref + Clone + 'static, + F: Function + ?Sized, + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = Either< + MapOk) -> Self::Response>, + Ready>, + >; + + forward_ready!(s); + + fn call(&self, req: ServiceRequest) -> Self::Future { + match self.f.deref().call(req) { + Ok(req) => Either::Left( + self.s + .call(req) + .map_ok(ServiceResponse::::map_into_left_body), + ), + Err(resp) => Either::Right(ready(Ok(resp.map_into_right_body()))), + } + } +} diff --git a/crates/flow-server/src/user.rs b/crates/flow-server/src/user.rs new file mode 100644 index 00000000..88ac2f97 --- /dev/null +++ b/crates/flow-server/src/user.rs @@ -0,0 +1,426 @@ +use crate::error::ErrorBody; +use crate::SupabaseConfig; +use actix_web::ResponseError; +use bincode::{Decode, Encode}; +use db::pool::{DbPool, RealDbPool}; +use flow::BoxedError; +use flow_lib::solana::{Keypair, KeypairExt}; +use flow_lib::{FlowRunId, UserId}; +use hashbrown::HashMap; +use reqwest::header::{self, HeaderName, HeaderValue}; +use reqwest::{StatusCode, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::panic::Location; +use std::sync::{Arc, Mutex}; +use thiserror::Error as ThisError; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +pub const FLOW_RUN_TOKEN_PREFIX: &str = "fr-"; +pub const SIGNING_TIMEOUT_SECS: i64 = 60; +const HEADER: &str = "space-operator authentication\n\n"; + +#[derive(Clone, Copy)] +pub struct SignatureAuth { + secret: [u8; blake3::KEY_LEN], +} + +#[derive(Encode, Decode)] +pub struct Payload { + pubkey: [u8; 32], + timestamp: i64, +} + +fn bincode_config() -> impl bincode::config::Config { + bincode::config::standard() + .with_fixed_int_encoding() + .skip_fixed_array_length() +} + +#[derive(ThisError, Debug)] +#[error("signature verification failed")] +pub struct Invalid(&'static Location<'static>); + +#[track_caller] +fn invalid() -> Invalid { + Invalid(std::panic::Location::caller()) +} + +impl SignatureAuth { + pub fn new(secret: [u8; 32]) -> Self { + Self { secret } + } + + pub(crate) fn hash(&self, data: &[u8]) -> blake3::Hash { + blake3::keyed_hash(&self.secret, data) + } + + /// `fr-` + `base64(id + hash(id))` + pub fn flow_run_token(&self, id: FlowRunId) -> String { + let mut buf = Vec::::with_capacity(48); + buf.extend_from_slice(id.as_bytes()); + let hash = blake3::keyed_hash(&self.secret, &buf); + buf.extend_from_slice(hash.as_bytes()); + let mut msg = FLOW_RUN_TOKEN_PREFIX.to_owned(); + base64::encode_config_buf(&buf, base64::URL_SAFE_NO_PAD, &mut msg); + msg + } + + pub fn init_login(&self, now: i64, pubkey: &[u8; 32]) -> String { + let payload = Payload { + pubkey: *pubkey, + timestamp: now, + }; + let mut bytes = Vec::with_capacity(72); + bincode::encode_into_std_write(&payload, &mut bytes, bincode_config()).unwrap(); + let sig = blake3::keyed_hash(&self.secret, &bytes); + bytes.extend_from_slice(sig.as_bytes()); + let mut msg = HEADER.to_owned(); + base64::encode_config_buf(&bytes, base64::URL_SAFE_NO_PAD, &mut msg); + msg + } + + /// `.` + pub fn confirm(&self, now: i64, input: &str) -> Result { + if !input.starts_with(HEADER) { + return Err(invalid()); + } + + let (signed_payload, sig) = input.split_once('.').ok_or_else(invalid)?; + + let signed_payload_bytes = base64::decode_config( + signed_payload.strip_prefix(HEADER).unwrap(), + base64::URL_SAFE, + ) + .map_err(|_| invalid())?; + let split_pos = signed_payload_bytes + .len() + .checked_sub(32) + .ok_or_else(invalid)?; + let (payload_bytes, blake3_sig) = signed_payload_bytes.split_at(split_pos); + let (payload, size) = + bincode::decode_from_slice::(payload_bytes, bincode_config()) + .map_err(|_| invalid())?; + if size != payload_bytes.len() { + return Err(invalid()); + } + if now - payload.timestamp >= SIGNING_TIMEOUT_SECS { + return Err(invalid()); + } + if blake3::keyed_hash(&self.secret, payload_bytes) != *blake3_sig { + return Err(invalid()); + } + let mut signature = [0u8; 64]; + let size = bs58::decode(sig) + .into(&mut signature) + .map_err(|_| invalid())?; + if size != 64 { + return Err(invalid()); + } + let signature = ed25519_dalek::Signature::from_bytes(&signature); + let pubkey = + ed25519_dalek::VerifyingKey::from_bytes(&payload.pubkey).map_err(|_| invalid())?; + pubkey + .verify_strict(signed_payload.as_bytes(), &signature) + .map_err(|_| invalid())?; + Ok(payload) + } +} + +#[derive(Clone)] +pub struct SupabaseAuth { + client: reqwest::Client, + pool: RealDbPool, + anon_key: String, + login_url: Url, + create_user_url: Url, + upsert_wallet_url: Url, + admin_token: HeaderValue, + open_whitelists: bool, + limits: Arc>>>, +} + +#[derive(ThisError, Debug)] +pub enum LoginError { + #[error("login error")] + Failed(&'static Location<'static>), + #[error(transparent)] + Db(#[from] db::Error), + #[error(transparent)] + Supabase(SupabaseError), +} + +impl ResponseError for LoginError { + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::UNAUTHORIZED + } + + fn error_response(&self) -> actix_web::HttpResponse { + ErrorBody::build(self) + } +} + +#[derive(Deserialize, ThisError, Debug)] +#[error("{msg}")] +pub struct SupabaseError { + pub msg: String, +} + +async fn supabase_error(resp: reqwest::Response) -> LoginError { + let bytes = match resp.bytes().await { + Ok(bytes) => bytes, + Err(error) => { + tracing::warn!("network error: {}", error); + return login_error(); + } + }; + match serde_json::from_slice::(&bytes) { + Ok(msg) => LoginError::Supabase(msg), + Err(error) => { + tracing::warn!("decode error: {}", error); + tracing::warn!("error body: {}", String::from_utf8_lossy(&bytes)); + login_error() + } + } +} + +#[track_caller] +fn login_error() -> LoginError { + LoginError::Failed(std::panic::Location::caller()) +} + +#[derive(Serialize)] +pub struct PasswordLogin { + pub email: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct CreateUser { + email: String, + email_confirm: bool, + user_metadata: UserMetadata, +} + +#[derive(Serialize)] +struct UserMetadata { + pub_key: String, +} + +pub fn get_email(pubkey: &[u8; 32]) -> String { + hex::encode(pubkey) + "@spaceoperator.com" +} + +impl CreateUser { + fn new(pk: &[u8; 32]) -> Self { + let pub_key = bs58::encode(pk).into_string(); + let email = get_email(pk); + Self { + email, + email_confirm: true, + user_metadata: UserMetadata { pub_key }, + } + } +} + +#[derive(Deserialize)] +struct CreateUserResponse { + id: UserId, +} + +#[derive(Deserialize)] +pub struct UpsertWalletBody { + pub keypair: Option, + #[serde(flatten)] + pub others: serde_json::Map, +} + +impl SupabaseAuth { + pub fn new(config: &SupabaseConfig, pool: DbPool) -> Result { + let pool = match pool { + DbPool::Real(pool) => pool, + _ => return Err("need database credentials".into()), + }; + let base_url = config.endpoint.url.join("auth/v1/")?; + let service_key = config.service_key.as_ref().ok_or("need service_key")?; + let login_url = base_url.join("token?grant_type=password")?; + let create_user_url = base_url.join("admin/users")?; + let upsert_wallet_url = config.endpoint.url.join("rest/v1/wallets")?; + let admin_token = HeaderValue::from_str(&format!("Bearer {}", service_key))?; + + Ok(Self { + client: reqwest::Client::new(), + anon_key: config.anon_key.clone(), + login_url, + create_user_url, + upsert_wallet_url, + admin_token, + pool, + open_whitelists: config.open_whitelists, + limits: Default::default(), + }) + } + + pub async fn upsert_wallet( + &self, + user_jwt: &str, + body: UpsertWalletBody, + ) -> Result<(StatusCode, Box), anyhow::Error> { + let encrypted_keypair = body + .keypair + .map(|s| { + let keypair = Keypair::from_str(&s)?; + Ok::<_, anyhow::Error>(self.pool.encrypt_keypair(&keypair)?) + }) + .transpose()?; + let mut body = body.others; + body.insert( + "encrypted_keypair".to_owned(), + serde_json::to_value(&encrypted_keypair)?, + ); + + let resp = self + .client + .post(self.upsert_wallet_url.clone()) + .header("apikey", &self.anon_key) + .header("Prefer", "resolution=merge-duplicates") + .header("Prefer", "return=representation") + .header(header::AUTHORIZATION, format!("Bearer {}", user_jwt)) + .json(&body) + .send() + .await?; + + let status = resp.status(); + let json = resp.json().await?; + Ok((status, json)) + } + + async fn get_semaphore_permit(&self, pubkey: &[u8; 32]) -> OwnedSemaphorePermit { + let semaphore = self + .limits + .lock() + .unwrap() + .entry(*pubkey) + .or_insert_with(|| Arc::new(Semaphore::new(1))) + .clone(); + + semaphore.acquire_owned().await.unwrap() + } + + fn cleanup_semaphore(&self, pubkey: &[u8; 32]) { + let mut limits = self.limits.lock().unwrap(); + if let Some(semaphore) = limits.get(pubkey) { + if Arc::strong_count(semaphore) == 1 { + limits.remove(pubkey).unwrap(); + } + } + tracing::debug!("semaphore counts: {}", limits.len()); + } + + pub async fn get_or_create_user( + &self, + pubkey: &[u8; 32], + ) -> Result<(UserId, bool), LoginError> { + let permit = self.get_semaphore_permit(pubkey).await; + let result = self.get_or_create_user_impl(pubkey).await; + drop(permit); + self.cleanup_semaphore(pubkey); + result + } + + pub async fn get_or_create_user_impl( + &self, + pubkey: &[u8; 32], + ) -> Result<(UserId, bool), LoginError> { + let conn = self.pool.get_admin_conn().await?; + let pk_bs58 = bs58::encode(pubkey).into_string(); + let maybe_user = conn.get_user_id_by_pubkey(&pk_bs58).await?; + if let Some(user_id) = maybe_user { + return Ok((user_id, false)); + } + + tracing::info!("creating user {}", pk_bs58); + if self.open_whitelists { + conn.insert_whitelist(&pk_bs58).await?; + } + drop(conn); + + let resp = self + .client + .post(self.create_user_url.clone()) + .header(HeaderName::from_static("apikey"), &self.anon_key) + .header(header::AUTHORIZATION, &self.admin_token) + .json(&CreateUser::new(pubkey)) + .send() + .await + .map_err(|_| login_error())?; + if resp.status() != StatusCode::OK { + return Err(supabase_error(resp).await); + } + let CreateUserResponse { id } = resp.json().await.map_err(|_| login_error())?; + + Ok((id, true)) + } + + pub async fn login(&self, payload: &Payload) -> Result<(Box, bool), LoginError> { + let permit = self.get_semaphore_permit(&payload.pubkey).await; + let result = self.login_impl(payload).await; + drop(permit); + self.cleanup_semaphore(&payload.pubkey); + result + } + + async fn login_impl(&self, payload: &Payload) -> Result<(Box, bool), LoginError> { + let pk = bs58::encode(&payload.pubkey).into_string(); + tracing::info!("login {}", pk); + + let (user_id, new_user) = self.get_or_create_user_impl(&payload.pubkey).await?; + let r = self + .pool + .get_admin_conn() + .await? + .get_login_credential(user_id) + .await?; + + let body = PasswordLogin { + email: r.email, + password: r.password, + }; + + tracing::debug!("calling supabase login"); + let resp = self + .client + .post(self.login_url.clone()) + .header(HeaderName::from_static("apikey"), &self.anon_key) + .json(&body) + .send() + .await + .map_err(|_| login_error())?; + if resp.status() != StatusCode::OK { + return Err(supabase_error(resp).await); + } + + let body: Box = resp.json().await.map_err(|_| login_error())?; + + Ok((body, new_user)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::Signer; + + fn now() -> i64 { + chrono::Utc::now().timestamp() + } + + #[test] + fn test_sign_verify() { + let kp = ed25519_dalek::SigningKey::from_bytes(&rand::random::<[u8; 32]>()); + let m = SignatureAuth::new(rand::random()); + let msg = m.init_login(now(), kp.verifying_key().as_bytes()); + let signature = bs58::encode(&kp.sign(msg.as_bytes()).to_bytes()).into_string(); + m.confirm(now(), &format!("{msg}.{signature}")).unwrap(); + } +} diff --git a/crates/flow-server/src/ws.rs b/crates/flow-server/src/ws.rs new file mode 100644 index 00000000..58f35b0d --- /dev/null +++ b/crates/flow-server/src/ws.rs @@ -0,0 +1,358 @@ +use crate::{ + api::prelude::auth::TokenType, + auth::ApiAuth, + db_worker::{ + flow_run_worker::{FlowRunWorker, SubscribeEvents}, + messages::SubscriptionID, + user_worker::SubscribeSigReq, + DBWorker, FindActor, GetUserWorker, + }, + Config, +}; +use actix::{ + Actor, ActorContext, ActorFutureExt, ActorStreamExt, AsyncContext, WrapFuture, WrapStream, +}; +use actix_web::{dev::HttpServiceFactory, guard, web, HttpRequest}; +use actix_web_actors::ws::{self, CloseCode, WebsocketContext}; +use db::pool::DbPool; +use flow::flow_run_events::Event; +use flow_lib::{BoxError, FlowRunId}; +use hashbrown::HashSet; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; + +#[derive(Deserialize)] +struct Request { + id: i64, + #[serde(flatten)] + data: WsMessage, +} + +#[derive(Deserialize)] +#[serde(tag = "method", content = "params")] +enum WsMessage { + Authenticate(Authenticate), + SubscribeFlowRunEvents(SubscribeFlowRunEvents), + SubscribeSignatureRequests(SubscribeSignatureRequests), +} + +#[derive(Deserialize)] +struct Authenticate { + token: String, +} + +#[derive(Deserialize)] +pub struct SubscribeFlowRunEvents { + flow_run_id: FlowRunId, + #[serde(default)] + token: Option, +} + +#[derive(Deserialize)] +pub struct SubscribeSignatureRequests {} + +#[derive(Serialize, Deserialize)] +pub struct WsResponse { + id: i64, + #[serde(flatten)] + data: Result, +} + +#[derive(Serialize, Deserialize)] +pub struct WsEvent { + stream_id: SubscriptionID, + #[serde(flatten)] + event: T, +} + +pub fn service(config: &Config, db: DbPool) -> impl HttpServiceFactory { + let auth = web::Data::new(config.all_auth(db)); + web::resource("") + .app_data(auth) + .wrap(config.cors()) + .route(web::route().guard(guard::Get()).to(ws_handler)) +} + +async fn ws_handler( + auth: web::Data, + db_worker: web::Data>, + req: HttpRequest, + stream: web::Payload, +) -> Result { + let resp = ws::start( + WsConn { + tokens: <_>::default(), + subscribing: <_>::default(), + + auth_service: auth.into_inner(), + db_worker: (**db_worker).clone(), + }, + &req, + stream, + )?; + Ok(resp) +} + +/// Actor holding a user's websocket connection +pub struct WsConn { + tokens: HashSet, + subscribing: HashSet, + + auth_service: Arc, + db_worker: actix::Addr, +} + +impl Actor for WsConn { + type Context = WebsocketContext; + + fn started(&mut self, _: &mut Self::Context) { + tracing::info!("started websocket connection"); + } +} + +fn find_id(msg: &str) -> Option { + #[derive(Deserialize)] + struct Id { + id: i64, + } + Some(serde_json::from_str::(msg).ok()?.id) +} + +impl actix::StreamHandler> for WsConn { + fn handle(&mut self, item: Result, ctx: &mut Self::Context) { + match item { + Ok(ws::Message::Text(text)) => { + tracing::debug!("received text '{}'", text); + match serde_json::from_str::(&text) { + Ok(msg) => match msg.data { + WsMessage::Authenticate(params) => self.authenticate(msg.id, params, ctx), + WsMessage::SubscribeFlowRunEvents(params) => { + self.subscribe_run(msg.id, params, ctx) + } + WsMessage::SubscribeSignatureRequests(params) => { + self.subscribe_sig(msg.id, params, ctx) + } + }, + Err(error) => { + let id = find_id(&text).unwrap_or(-1); + error_response(ctx, id, &error); + } + }; + } + Ok(ws::Message::Ping(data)) => { + ctx.pong(&data); + } + Ok(ws::Message::Close(reason)) => { + ctx.close(reason); + ctx.stop(); + } + Err(error) => { + tracing::error!("WS error: {}, stopping connection", error); + ctx.close(Some(CloseCode::Invalid.into())); + ctx.stop(); + } + Ok(ws::Message::Binary(_)) => { + tracing::warn!("received Binary message"); + } + Ok(ws::Message::Continuation(_)) => { + tracing::warn!("received Continuation message"); + } + Ok(ws::Message::Pong(_)) => {} + Ok(ws::Message::Nop) => {} + } + } +} + +fn inject_run_id(event: &Event, id: FlowRunId) -> serde_json::Value { + let mut json = serde_json::to_value(event).unwrap(); + json.get_mut("data") + .unwrap() + .as_object_mut() + .unwrap() + .insert("flow_run_id".to_owned(), id.to_string().into()); + json +} + +impl WsConn { + fn authenticate(&mut self, id: i64, params: Authenticate, ctx: &mut WebsocketContext) { + let token = params.token; + let fut = self + .auth_service + .clone() + .ws_authenticate(token) + .into_actor(&*self) + .map(move |res, act, ctx| match res { + Ok(token) => { + act.tokens.insert(token.clone()); + success_response(ctx, id, token) + } + Err(error) => error_response(ctx, id, &error), + }); + ctx.wait(fut); + } + + fn subscribe_run( + &mut self, + id: i64, + params: SubscribeFlowRunEvents, + ctx: &mut WebsocketContext, + ) { + // TODO: implement token for interflow + let flow_run_id = params.flow_run_id; + if let Some(token) = params.token { + let fut = self + .auth_service + .clone() + .ws_authenticate(token) + .into_actor(&*self) + .map(move |res, act, ctx| match res { + Ok(token) => { + act.tokens.insert(token.clone()); + act.subscribe_run( + id, + SubscribeFlowRunEvents { + flow_run_id, + token: None, + }, + ctx, + ); + } + Err(error) => error_response(ctx, id, &error), + }); + ctx.wait(fut); + return; + } + let db_worker = self.db_worker.clone(); + let tokens = self.tokens.clone(); + let fut = async move { + Ok::<_, BoxError>( + db_worker + .send(FindActor::::new(flow_run_id)) + .await? + .ok_or("not found")? + .send(SubscribeEvents { tokens }) + .await??, + ) + } + .into_actor(&*self) + .map(move |res, act, ctx| match res { + Ok((stream_id, rx)) => { + tracing::info!("subscribed flow-run"); + act.subscribing.insert(stream_id); + success_response(ctx, id, json!({ "stream_id": stream_id })); + let fut = rx + .into_actor(&*act) + .map(move |event, _, ctx| { + text_stream(ctx, stream_id, inject_run_id(&event, flow_run_id)) + }) + .finish() + .map(move |_, act, _| { + // TODO: send a message indicating stream ended? + act.subscribing.remove(&stream_id); + }); + ctx.spawn(fut); + } + Err(error) => error_response(ctx, id, &error), + }); + ctx.wait(fut); + } + + fn subscribe_sig( + &mut self, + id: i64, + _params: SubscribeSignatureRequests, + ctx: &mut WebsocketContext, + ) { + let user_id = self.tokens.iter().find_map(|token| token.user_id()); + let user_id = match user_id { + Some(user_id) => user_id, + None => { + error_response(ctx, id, &"unauthorized"); + return; + } + }; + + let db_worker = self.db_worker.clone(); + let fut = async move { + let stream_id = db_worker + .send(GetUserWorker { user_id }) + .await? + .send(SubscribeSigReq {}) + .await??; + + Ok::<_, BoxError>(stream_id) + } + .into_actor(&*self) + .map(move |res, act, ctx| match res { + Ok((stream_id, rx)) => { + tracing::info!("subscribed signature requests"); + act.subscribing.insert(stream_id); + success_response(ctx, id, json!({ "stream_id": stream_id })); + let fut = rx + .into_actor(&*act) + .map(move |event, _, ctx| { + text_stream( + ctx, + stream_id, + json!({ + "event": "SignatureRequest", + "data": event, + }), + ) + }) + .finish() + .map(move |_, act, _| { + // TODO: send a message indicating stream ended? + act.subscribing.remove(&stream_id); + }); + ctx.spawn(fut); + } + Err(error) => error_response(ctx, id, &error), + }); + ctx.wait(fut); + } +} + +fn error_response(ctx: &mut WebsocketContext, id: i64, error: &E) { + let text = serde_json::to_string(&WsResponse::<()> { + id, + data: Err(error.to_string()), + }) + .unwrap(); + tracing::debug!("sending '{}'", text); + ctx.text(text); +} + +fn success_response(ctx: &mut WebsocketContext, id: i64, value: T) { + let result = serde_json::to_string(&WsResponse:: { + id, + data: Ok(value), + }); + match result { + Ok(text) => { + tracing::debug!("sending '{}'", text); + ctx.text(text) + } + Err(error) => error_response( + ctx, + id, + &format!("InternalError: failed to serialize event, {}", error), + ), + } +} + +fn text_stream( + ctx: &mut WebsocketContext, + stream_id: SubscriptionID, + event: T, +) { + let result = serde_json::to_string(&WsEvent:: { stream_id, event }); + match result { + Ok(text) => { + tracing::debug!("sending '{}'", text); + ctx.text(text) + } + Err(error) => tracing::error!("failed to serialize event: {}", error), + } +} diff --git a/crates/flow/.gitignore b/crates/flow/.gitignore new file mode 100644 index 00000000..4fffb2f8 --- /dev/null +++ b/crates/flow/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/crates/flow/Cargo.toml b/crates/flow/Cargo.toml new file mode 100644 index 00000000..3d8328cb --- /dev/null +++ b/crates/flow/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "flow" +version = "0.0.0" +edition = "2021" + +[dependencies] +flow-lib = { workspace = true } +value = { workspace = true } +utils = { workspace = true } +space-wasm = { workspace = true } +rhai-script.workspace = true +command-rpc.workspace = true +srpc.workspace = true +cmds-deno.workspace = true + +anyhow = "1.0" +bs58 = "0.4" +chrono = "0.4" +bytes = "1.2.1" +futures = "0.3.21" +petgraph = "0.6.2" +inventory = "0.3" +thiserror = "1.0.31" +async-trait = "0.1.56" +solana-sdk = { version = "1", default-features = false } +tokio = "1" +uuid = { version = "1", features = ["v4", "serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["preserve_order"] } +hashbrown = { version = "0.14", features = ["serde"] } +tower = { version = "0.4", features = ["util"] } +tokio-util = "0.7" +tracing = "0.1" +actix = "0.13" +derive_more = "0.99" +indexmap = "2" +rust_decimal = { version = "1", features = ["serde-with-float"] } +reqwest = { version = "0.12", features = ["json"] } +mime_guess = "2.0.4" +crossbeam-channel = "0.5.8" +tempfile = "3.10.1" +url = "2.5.0" +home = "0.5.9" +bincode = "1.3.3" +base64 = "0.22.1" +bon = "3.3.0" +getset = "0.1.3" + +[dev-dependencies] +cmds-std.workspace = true +cmds-solana.workspace = true +actix-web = "4.5.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crates/flow/src/command/collect.rs b/crates/flow/src/command/collect.rs new file mode 100644 index 00000000..c4dd4cc5 --- /dev/null +++ b/crates/flow/src/command/collect.rs @@ -0,0 +1,55 @@ +use crate::command::prelude::*; + +#[derive(Debug, Clone)] +pub struct Collect; + +pub const COLLECT: &str = "collect"; + +pub const ELEMENT: &str = "element"; + +pub const ARRAY: &str = "array"; + +#[async_trait] +impl CommandTrait for Collect { + fn name(&self) -> Name { + COLLECT.into() + } + + fn inputs(&self) -> Vec { + [Input { + name: ELEMENT.into(), + type_bounds: [ValueType::Free].to_vec(), + required: false, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: ARRAY.into(), + r#type: ValueType::Free, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, mut inputs: ValueSet) -> Result { + let v = inputs + .swap_remove(ELEMENT) + .unwrap_or_else(|| Value::Array(Vec::new())); + if matches!(&v, Value::Array(_)) { + Ok(value::map! { + ARRAY => v, + }) + } else { + // FlowGraph must prepare input for this node correctly + unreachable!(); + // Err(value::Error::invalid_type(v.unexpected(), &"array").into()) + } + } +} + +flow_lib::submit!(CommandDescription::new(COLLECT, |_| { + Ok(Box::new(Collect)) +})); diff --git a/crates/flow/src/command/flow_input.rs b/crates/flow/src/command/flow_input.rs new file mode 100644 index 00000000..ec6aab44 --- /dev/null +++ b/crates/flow/src/command/flow_input.rs @@ -0,0 +1,64 @@ +use crate::command::prelude::*; + +#[derive(Debug)] +pub struct FlowInputCommand { + label: Name, +} + +pub const FLOW_INPUT: &str = "flow_input"; + +impl FlowInputCommand { + fn new(data: &NodeData) -> Self { + let form = &data.targets_form.form_data; + + let label = form + .get("label") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_owned(); + + Self { label } + } +} + +#[async_trait] +impl CommandTrait for FlowInputCommand { + fn name(&self) -> Name { + FLOW_INPUT.into() + } + + fn inputs(&self) -> Vec { + [].to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: self.label.clone(), + r#type: ValueType::Free, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, mut inputs: ValueSet) -> Result { + let value = inputs.swap_remove(&self.label).unwrap_or(Value::Null); + Ok(value::map! { + &self.label => value, + }) + } + + fn read_form_data(&self, data: JsonValue) -> ValueSet { + data.get("form_label") + .map(|value| { + // TODO: is this a good way to do it? + value::map! { + &self.label => value.clone(), + } + }) + .unwrap_or_default() + } +} + +flow_lib::submit!(CommandDescription::new(FLOW_INPUT, |data: &NodeData| { + Ok(Box::new(FlowInputCommand::new(data))) +})); diff --git a/crates/flow/src/command/flow_output.rs b/crates/flow/src/command/flow_output.rs new file mode 100644 index 00000000..1c37bd32 --- /dev/null +++ b/crates/flow/src/command/flow_output.rs @@ -0,0 +1,59 @@ +use crate::command::prelude::*; + +#[derive(Debug)] +pub struct FlowOutputCommand { + pub rename: Name, +} + +pub const FLOW_OUTPUT: &str = "flow_output"; + +impl FlowOutputCommand { + fn new(data: &NodeData) -> Self { + let form = &data.targets_form.form_data; + + let rename = form + .get("label") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_owned(); + + Self { rename } + } +} + +#[async_trait] +impl CommandTrait for FlowOutputCommand { + fn name(&self) -> Name { + FLOW_OUTPUT.into() + } + + fn inputs(&self) -> Vec { + [Input { + name: self.rename.clone(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: self.rename.clone(), + r#type: ValueType::Free, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, inputs: ValueSet) -> Result { + Ok(match inputs.into_values().next() { + Some(value) => ValueSet::from([(self.rename.clone(), value)]), + None => ValueSet::new(), + }) + } +} + +flow_lib::submit!(CommandDescription::new(FLOW_OUTPUT, |data: &NodeData| { + Ok(Box::new(FlowOutputCommand::new(data))) +})); diff --git a/crates/flow/src/command/foreach.rs b/crates/flow/src/command/foreach.rs new file mode 100644 index 00000000..c197bd84 --- /dev/null +++ b/crates/flow/src/command/foreach.rs @@ -0,0 +1,56 @@ +use crate::command::prelude::*; + +#[derive(Debug, Clone)] +pub struct Foreach; + +pub const FOREACH: &str = "foreach"; + +pub const ARRAY: &str = "array"; + +pub const ELEMENT: &str = "element"; + +#[async_trait] +impl CommandTrait for Foreach { + fn name(&self) -> Name { + FOREACH.into() + } + + fn inputs(&self) -> Vec { + [Input { + name: ARRAY.into(), + type_bounds: [ValueType::Free].to_vec(), + required: true, + passthrough: false, + }] + .to_vec() + } + + fn outputs(&self) -> Vec { + [Output { + name: ELEMENT.into(), + r#type: ValueType::Free, + optional: false, + }] + .to_vec() + } + + async fn run(&self, _ctx: Context, mut inputs: ValueSet) -> Result { + let v = inputs + .swap_remove(ARRAY) + .ok_or_else(|| crate::Error::ValueNotFound(ARRAY.into()))?; + if matches!(&v, Value::Array(_)) { + Ok(value::map! { + ELEMENT => v, + }) + } else { + // if it's not an array, treat it as a 1-element array. + Ok(value::map! { + ELEMENT => Value::Array([v].to_vec()), + }) + } + } +} + +flow_lib::submit!(CommandDescription::new(FOREACH, |_| { + Ok(Box::new(Foreach)) +})); diff --git a/crates/flow/src/command/interflow.rs b/crates/flow/src/command/interflow.rs new file mode 100644 index 00000000..f6af3578 --- /dev/null +++ b/crates/flow/src/command/interflow.rs @@ -0,0 +1,124 @@ +use crate::{ + command::prelude::*, + flow_registry::{FlowRegistry, StartFlowOptions}, +}; +use anyhow::anyhow; +use flow_lib::command::InstructionInfo; + +pub const INTERFLOW: &str = "interflow"; + +pub struct Interflow { + id: FlowId, + inputs: Vec, + outputs: Vec, + instruction_info: Option, +} + +pub fn get_interflow_id(n: &NodeData) -> Result { + let id = n + .targets_form + .form_data + .get("id") + .unwrap_or(&JsonValue::Null); + FlowId::deserialize(id) +} + +impl Interflow { + fn new(n: &NodeData) -> Result { + let id = get_interflow_id(n)?; + let inputs = n + .targets + .iter() + .map(|x| Input { + name: x.name.clone(), + type_bounds: [ValueType::Free].to_vec(), + required: x.required, + passthrough: x.passthrough, + }) + .collect(); + + let outputs = n + .sources + .iter() + .map(|x| Output { + name: x.name.clone(), + r#type: ValueType::Free, + optional: x.optional, + }) + .collect(); + + Ok(Self { + id, + inputs, + outputs, + instruction_info: n.instruction_info.clone(), + }) + } +} + +#[async_trait] +impl CommandTrait for Interflow { + fn name(&self) -> Name { + INTERFLOW.into() + } + + fn inputs(&self) -> Vec { + self.inputs.clone() + } + + fn outputs(&self) -> Vec { + self.outputs.clone() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let registry = ctx + .get::() + .ok_or_else(|| anyhow::anyhow!("FlowRegistry not found"))?; + + let svc = if self.instruction_info.is_some() { + Some( + ctx.command + .as_ref() + .ok_or_else(|| CommandError::msg("command context not found"))? + .svc + .clone(), + ) + } else { + None + }; + + let (_, handle) = registry + .start( + self.id, + inputs, + StartFlowOptions { + origin: ctx + .new_interflow_origin() + .ok_or_else(|| anyhow::anyhow!("this is a bug"))?, + solana_client: Some(ctx.cfg.solana_client.clone()), + parent_flow_execute: svc, + ..Default::default() + }, + ) + .await?; + let result = handle.await?; + if result.flow_errors.is_empty() { + Ok(result.output) + } else { + let mut errors = String::new(); + for error in result.flow_errors { + errors += &error; + errors += ";\n"; + } + Err(anyhow!(errors)) + } + } + + fn instruction_info(&self) -> Option { + self.instruction_info.clone() + } +} + +flow_lib::submit!(CommandDescription::new(INTERFLOW, |data: &NodeData| { + Ok(Box::new(Interflow::new(data)?)) +})); diff --git a/crates/flow/src/command/interflow_instructions.rs b/crates/flow/src/command/interflow_instructions.rs new file mode 100644 index 00000000..8d715705 --- /dev/null +++ b/crates/flow/src/command/interflow_instructions.rs @@ -0,0 +1,136 @@ +use super::interflow::get_interflow_id; +use crate::{ + command::prelude::*, + flow_graph::FlowRunResult, + flow_registry::{FlowRegistry, StartFlowOptions}, +}; +use bytes::Bytes; + +pub const INTERFLOW_INSTRUCTIONS: &str = "interflow_instructions"; + +struct Interflow { + id: FlowId, + inputs: Vec, +} + +impl Interflow { + fn new(n: &NodeData) -> Result { + let id = get_interflow_id(n)?; + let inputs = n + .targets + .iter() + .map(|x| Input { + name: x.name.clone(), + type_bounds: [ValueType::Free].to_vec(), + required: x.required, + passthrough: x.passthrough, + }) + .collect(); + + Ok(Self { id, inputs }) + } +} + +fn build_error(r: &FlowRunResult) -> CommandError { + let mut msg = "no instructions\n".to_owned(); + for e in &r.flow_errors { + msg.push_str(e); + msg.push('\n'); + } + for e in r.node_errors.values().flatten() { + msg.push_str(e); + msg.push('\n'); + } + + CommandError::msg(msg) +} + +#[async_trait] +impl CommandTrait for Interflow { + fn name(&self) -> Name { + INTERFLOW_INSTRUCTIONS.into() + } + + fn inputs(&self) -> Vec { + self.inputs.clone() + } + + fn outputs(&self) -> Vec { + [ + Output { + name: "fee_payer".into(), + r#type: ValueType::Pubkey, + optional: false, + }, + Output { + name: "signers".into(), + r#type: ValueType::Array, + optional: false, + }, + Output { + name: "instructions".into(), + r#type: ValueType::Array, + optional: false, + }, + ] + .into() + } + + async fn run(&self, ctx: Context, inputs: ValueSet) -> Result { + let registry = ctx + .get::() + .ok_or_else(|| anyhow::anyhow!("FlowRegistry not found"))?; + + let (_, handle) = registry + .start( + self.id, + inputs, + StartFlowOptions { + collect_instructions: true, + origin: ctx + .new_interflow_origin() + .ok_or_else(|| anyhow::anyhow!("this is a bug"))?, + solana_client: Some(ctx.cfg.solana_client.clone()), + ..Default::default() + }, + ) + .await?; + let result = handle.await?; + + if result.instructions.is_none() { + return Err(build_error(&result)); + } + let ins = result.instructions.unwrap(); + let signers = ins + .signers + .into_iter() + .map(|w| value::to_value(&w)) + .collect::, _>>()?; + let instructions = ins + .instructions + .into_iter() + .map(|i| { + Value::Map(value::map! { + "program_id" => i.program_id, + "accounts" => i.accounts.into_iter().map(|a| Value::Map(value::map! { + "pubkey" => a.pubkey, + "is_signer" => a.is_signer, + "is_writable" => a.is_writable, + })).collect::>(), + "data" => Bytes::from(i.data), + }) + }) + .collect::>(); + let output = value::map! { + "fee_payer" => ins.fee_payer, + "signers" => signers, + "instructions" => instructions, + }; + Ok(output) + } +} + +flow_lib::submit!(CommandDescription::new( + INTERFLOW_INSTRUCTIONS, + |data: &NodeData| { Ok(Box::new(Interflow::new(data)?)) } +)); diff --git a/crates/flow/src/command/mod.rs b/crates/flow/src/command/mod.rs new file mode 100644 index 00000000..43da85e6 --- /dev/null +++ b/crates/flow/src/command/mod.rs @@ -0,0 +1,26 @@ +pub mod collect; +pub mod flow_input; +pub mod flow_output; +pub mod foreach; +pub mod interflow; +pub mod interflow_instructions; +pub mod rhai; +pub mod wasm; + +pub mod prelude { + pub use async_trait::async_trait; + pub use flow_lib::{ + command::{ + builder::{BuildResult, BuilderCache, CmdBuilder}, + CommandDescription, CommandError, CommandTrait, + }, + config::client::NodeData, + context::Context, + CmdInputDescription as Input, CmdOutputDescription as Output, FlowId, Name, Value, + ValueSet, ValueType, + }; + pub use serde::{Deserialize, Serialize}; + pub use serde_json::Value as JsonValue; + pub use std::sync::Arc; + pub use thiserror::Error as ThisError; +} diff --git a/crates/flow/src/command/rhai.rs b/crates/flow/src/command/rhai.rs new file mode 100644 index 00000000..3a7215fb --- /dev/null +++ b/crates/flow/src/command/rhai.rs @@ -0,0 +1,64 @@ +use flow_lib::command::prelude::*; +use std::sync::Arc; + +use crate::flow_registry::{run_rhai, FlowRegistry}; + +struct Command { + name: Name, + inner: Arc, +} + +#[async_trait] +impl CommandTrait for Command { + fn name(&self) -> Name { + self.name.clone() + } + + fn inputs(&self) -> Vec { + self.inner.inputs.clone() + } + + fn outputs(&self) -> Vec { + self.inner.outputs.clone() + } + + async fn run(&self, ctx: Context, input: ValueSet) -> Result { + ctx.get::() + .ok_or_else(|| anyhow::anyhow!("FlowRegistry not found"))? + .run_rhai(run_rhai::Request { + command: self.inner.clone(), + ctx: ctx.clone(), + input, + }) + .await + } +} + +pub fn build(nd: &NodeData) -> Result, CommandError> { + let inputs: Vec = nd.targets.iter().cloned().map(Into::into).collect(); + let outputs: Vec = nd + .sources + .iter() + .cloned() + .map(|s| Output { + // TODO: we did not upload this field to db + optional: true, + ..Output::from(s) + }) + .collect(); + let source_code_name = inputs + .first() + .ok_or_else(|| CommandError::msg("no source code input"))? + .name + .clone(); + let cmd = Arc::new(rhai_script::Command { + source_code_name, + inputs, + outputs, + }); + + Ok(Box::new(Command { + name: nd.node_id.clone(), + inner: cmd, + })) +} diff --git a/crates/flow/src/command/wasm.rs b/crates/flow/src/command/wasm.rs new file mode 100644 index 00000000..2988e9c3 --- /dev/null +++ b/crates/flow/src/command/wasm.rs @@ -0,0 +1,101 @@ +use crate::command::prelude::*; +use async_trait::async_trait; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use space_wasm::Wasm; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Description { + pub name: Name, + pub r#type: ValueType, +} + +#[derive(Debug, Clone)] +pub struct WasmCommand { + pub bytes: Bytes, + pub function: String, + pub inputs: Vec, + pub outputs: Vec, +} + +#[async_trait] +impl CommandTrait for WasmCommand { + fn name(&self) -> Name { + "Wasm".into() + } + + fn inputs(&self) -> Vec { + self.inputs + .iter() + .map(|it| Input { + name: it.name.clone(), + type_bounds: [it.r#type.clone()].to_vec(), + required: true, + passthrough: false, + }) + .collect() + } + + fn outputs(&self) -> Vec { + self.outputs + .iter() + .map(|it| Output { + name: it.name.clone(), + r#type: it.r#type.clone(), + optional: false, + }) + .collect() + } + + async fn run(&self, ctx: Context, values: ValueSet) -> Result { + let command = self.clone(); + let output_name = self + .outputs + .first() + .ok_or(CommandError::msg("Expected 1 output, got 0"))? + .name + .clone(); + let env = ctx.environment; + tokio::task::spawn_blocking(move || { + let input: Json = match values.first() { + _ if values.len() > 1 => { + let map = values + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(); + Json::Object(map) + } + Some((_, input)) => input.clone().into(), + _ => Err(CommandError::msg("Expected some input, got none"))?, + }; + let wasm = Wasm::new(&command.bytes, env)?; + let output = wasm.run::(&command.function, &input)?; + match output.as_object() { + Some(object) if object.contains_key("Err") => { + let message = format!("{}", object["Err"]["description"]); + Err(CommandError::msg(message)) + } + Some(mut object) => { + if object.contains_key("Ok") { + match object["Ok"].as_object() { + Some(ok_object) => object = ok_object, + _ => { + return Ok(ValueSet::from([( + output_name, + object["Ok"].clone().into(), + )])) + } + } + }; + Ok(object + .into_iter() + .map(|(key, value)| (key.to_owned(), value.to_owned().into())) + .collect::()) + } + _ => Ok(ValueSet::from([(output_name, output.into())])), + } + }) + .await? + } +} diff --git a/crates/flow/src/context.rs b/crates/flow/src/context.rs new file mode 100644 index 00000000..262a44d3 --- /dev/null +++ b/crates/flow/src/context.rs @@ -0,0 +1,110 @@ +use crate::{ + command::wasm::{Description, WasmCommand}, + Error, +}; +use flow_lib::{ + command::{CommandDescription, CommandTrait}, + config::client::NodeData, + CommandType, +}; +use std::{borrow::Cow, collections::BTreeMap}; +use tokio::process::Child; + +pub struct CommandFactory { + pub natives: BTreeMap, CommandDescription>, +} + +impl Default for CommandFactory { + fn default() -> Self { + Self::new() + } +} + +impl CommandFactory { + pub fn new() -> Self { + let mut natives = BTreeMap::new(); + for d in inventory::iter::() { + let name = d.name.clone(); + if natives.insert(name.clone(), d.clone()).is_some() { + tracing::error!("duplicated command {:?}", name); + } + } + + Self { natives } + } + + pub fn new_native_command( + &self, + name: &str, + config: &NodeData, + ) -> crate::Result> { + match self.natives.get(name) { + Some(d) => (d.fn_new)(config).map_err(crate::Error::CreateCmd), + None => { + if rhai_script::is_rhai_script(name) { + crate::command::rhai::build(config).map_err(crate::Error::CreateCmd) + } else { + Err(Error::Any(format!("native not found: {}", name).into())) + } + } + } + } + + pub async fn new_deno_command( + &self, + config: &NodeData, + spawned: &mut Vec, + ) -> crate::Result> { + let (cmd, child) = cmds_deno::new(config).await.map_err(Error::custom)?; + spawned.push(child); + Ok(cmd) + } + + pub async fn new_command( + &self, + name: &str, + config: &NodeData, + spawned: &mut Vec, + ) -> crate::Result> { + match config.r#type { + CommandType::Mock => Err(Error::custom("mock node")), + CommandType::Native => self.new_native_command(name, config), + CommandType::Deno => self.new_deno_command(config, spawned).await, + CommandType::Wasm => { + let bytes = config + .targets_form + .wasm_bytes + .clone() + .ok_or_else(|| Error::Any("wasm_bytes not found".into()))?; + + // Map inputs and outputs + let inputs = config + .targets + .iter() + .map(|it| Description { + name: it.name.clone(), + r#type: it.type_bounds[0].clone(), + }) + .collect(); + + let outputs = config + .sources + .iter() + .map(|it| Description { + name: it.name.clone(), + r#type: it.r#type.clone(), + }) + .collect(); + + // Compile wasm and create command + let command: Box = Box::new(WasmCommand { + bytes, + function: String::from("main"), + inputs, + outputs, + }); + Ok(command) + } + } + } +} diff --git a/crates/flow/src/error.rs b/crates/flow/src/error.rs new file mode 100644 index 00000000..42ff1ff3 --- /dev/null +++ b/crates/flow/src/error.rs @@ -0,0 +1,48 @@ +use crate::{ + flow_graph::BuildGraphError, + flow_registry::get_flow, + flow_set::{get_flow_row, make_signer}, +}; +use flow_lib::{command::CommandError, Name}; +use std::error::Error as StdError; +use thiserror::Error as ThisError; + +pub type BoxedError = Box; + +pub type Result = std::result::Result; + +fn unwrap(s: &Option) -> &str { + s.as_ref().map(|v| v.as_str()).unwrap_or_default() +} + +#[derive(ThisError, Debug)] +pub enum Error { + #[error(transparent)] + Any(#[from] BoxedError), + #[error("canceled by user {}", unwrap(.0))] + Canceled(Option), + #[error("value not found in field \"{0}\"")] + ValueNotFound(Name), + #[error("failed to create command: {}", .0)] + CreateCmd(#[source] CommandError), + #[error(transparent)] + BuildGraphError(#[from] BuildGraphError), + #[error(transparent)] + GetFlow(#[from] get_flow::Error), + #[error(transparent)] + GetFlowRow(#[from] get_flow_row::Error), + #[error(transparent)] + MakeSigner(#[from] make_signer::Error), + #[error("graph has cycle")] + Cycle, + #[error("flow must contain exactly 1 tx")] + NeedOneTx, + #[error("flow must have exactly 1 Flow Output node connected to a signature output")] + NeedOneSignatureOutput, +} + +impl Error { + pub fn custom>(e: E) -> Self { + Error::Any(e.into()) + } +} diff --git a/crates/flow/src/flow_graph.rs b/crates/flow/src/flow_graph.rs new file mode 100644 index 00000000..ad1d5196 --- /dev/null +++ b/crates/flow/src/flow_graph.rs @@ -0,0 +1,1972 @@ +use crate::{ + command::flow_output::FLOW_OUTPUT, + context::CommandFactory, + flow_registry::FlowRegistry, + flow_run_events::{ + EventSender, FlowError, FlowFinish, FlowStart, NodeError, NodeFinish, NodeOutput, + NodeStart, NODE_SPAN_NAME, + }, +}; +use base64::prelude::*; +use chrono::{DateTime, Utc}; +use flow_lib::{ + command::{CommandError, CommandTrait, InstructionInfo}, + config::client::{self, PartialConfig}, + context::{execute, get_jwt, CommandContext, Context}, + solana::{find_failed_instruction, ExecutionConfig, Instructions, Pubkey, Wallet}, + utils::{Extensions, TowerClient}, + CommandType, FlowConfig, FlowId, FlowRunId, Name, NodeId, ValueSet, +}; +use futures::{ + channel::{mpsc, oneshot}, + future::{BoxFuture, Either}, + stream::FuturesUnordered, + FutureExt, StreamExt, +}; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use petgraph::{ + csr::DefaultIx, + graph::EdgeIndex, + stable_graph::{Edges, NodeIndex, StableGraph}, + visit::{Bfs, EdgeRef, GraphRef, VisitMap, Visitable}, + Directed, Direction, +}; +use solana_sdk::system_instruction::transfer_many; +use std::{ + collections::{BTreeSet, VecDeque}, + ops::ControlFlow, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, RwLock, + }, + task::Poll, + time::Duration, +}; +use thiserror::Error as ThisError; +use tokio::{ + process::Child, + sync::Semaphore, + task::{JoinError, JoinHandle}, +}; +use tokio_util::sync::CancellationToken; +use tracing::Instrument; +use uuid::Uuid; +use value::Value; + +pub const MAX_STOP_TIMEOUT: u32 = Duration::from_secs(5 * 60).as_millis() as u32; + +#[derive(Debug, Clone)] +pub struct StopSignal { + pub token: CancellationToken, + pub timeout_millies: Arc, + pub reason: Arc>>, +} + +impl Default for StopSignal { + fn default() -> Self { + Self::new() + } +} + +impl StopSignal { + pub fn new() -> Self { + Self { + token: CancellationToken::new(), + timeout_millies: Arc::new(AtomicU32::new(0)), + reason: Arc::new(RwLock::new(None)), + } + } + + pub fn stop(&self, timeout_millies: u32, reason: Option) { + if !self.token.is_cancelled() { + let timeout = timeout_millies.min(MAX_STOP_TIMEOUT); + self.timeout_millies.store(timeout, Ordering::Relaxed); + *self.reason.write().unwrap() = reason; + self.token.cancel(); + } + } + + pub fn get_reason(&self) -> Option { + self.reason.read().unwrap().clone() + } + + pub async fn race(&self, task: F, canceled_error: FE) -> Result + where + FE: FnOnce(Option) -> E, + F: std::future::Future> + Unpin, + { + match futures::future::select(task, std::pin::pin!(self.token.cancelled())).await { + Either::Left((result, _)) => result, + Either::Right((_, task)) => { + let timeout = self.timeout_millies.load(Ordering::Relaxed); + if timeout == 0 { + Err(canceled_error(self.get_reason())) + } else { + let duration = Duration::from_millis(timeout as u64); + match tokio::time::timeout(duration, task).await { + Ok(result) => result, + Err(_) => Err(canceled_error(self.get_reason())), + } + } + } + } + } +} + +pub struct FlowGraph { + pub id: FlowId, + pub ctx: Context, + pub g: StableGraph, + pub nodes: HashMap, + pub mode: client::BundlingMode, + pub output_instructions: bool, + pub action_identity: Option, + pub rhai_permit: Arc, + pub tx_exec_config: ExecutionConfig, + pub spawned: Vec, + pub parent_flow_execute: Option, + pub fees: Vec<(Pubkey, u64)>, +} + +pub struct UsePreviousValue { + node_id: NodeId, + output_name: Name, + foreach: bool, +} + +pub struct Node { + pub id: NodeId, + pub command: Box, + pub form_inputs: ValueSet, + /// Index in the graph + pub idx: NodeIndex, + /// List of input ports to use previous run's values + pub use_previous_values: HashMap, +} + +impl std::fmt::Debug for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Node") + .field("id", &self.id) + .field("command", &self.command.name()) + .field("form_inputs", &self.form_inputs) + .field("idx", &self.idx) + .finish() + } +} + +pub struct PartialOutput { + pub node_id: NodeId, + pub times: u32, + pub output: Result<(Option, ValueSet), CommandError>, + pub resp: oneshot::Sender>, +} + +#[derive(Debug, Clone)] +pub struct Edge { + pub from: Name, + pub to: Name, + pub is_required_input: bool, + pub is_optional_output: bool, + pub values: VecDeque, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TrackEdgeValue { + None, + Element(NodeIndex, u32), + Zip(BTreeSet), + Nest(Vec), +} + +impl TrackEdgeValue { + fn is_array(&self) -> bool { + !matches!(self, TrackEdgeValue::None) + } +} + +impl TrackEdgeValue { + fn zip(&self, other: &Self) -> Self { + if matches!(other, TrackEdgeValue::None) { + return self.clone(); + } + match self.clone() { + TrackEdgeValue::None => other.clone(), + TrackEdgeValue::Zip(mut set) => { + match other { + TrackEdgeValue::Zip(other_set) => { + set.extend(other_set.clone()); + } + other => { + set.insert(other.clone()); + } + } + TrackEdgeValue::Zip(set) + } + this => { + if this == *other { + this + } else { + let mut set = BTreeSet::new(); + set.insert(this); + set.insert(other.clone()); + TrackEdgeValue::Zip(set) + } + } + } + } + + fn nest(&self, (nid, pos): (NodeIndex, u32)) -> Self { + let other = TrackEdgeValue::Element(nid, pos); + match self.clone() { + TrackEdgeValue::None => other, + TrackEdgeValue::Nest(mut vec) => { + vec.push(other); + TrackEdgeValue::Nest(vec) + } + this => TrackEdgeValue::Nest(vec![this, other]), + } + } +} + +#[derive(Clone, Debug)] +pub struct EdgeValue { + value: Option, + tracker: TrackEdgeValue, +} + +#[derive(Debug)] +struct State { + flow_run_id: FlowRunId, + previous_values: HashMap>, + flow_inputs: value::Map, + event_tx: EventSender, + ran: HashMap, + running: FuturesUnordered>, + running_info: HashMap<(NodeId, u32), RunningNodeInfo>, + result: FlowRunResult, + stop: StopSignal, + stop_shared: StopSignal, + out_tx: mpsc::UnboundedSender, + out_rx: mpsc::UnboundedReceiver, +} + +#[derive(Debug)] +struct RunningNodeInfo { + id: NodeId, + times: u32, + node_idx: NodeIndex, + command_name: Name, + tracker: TrackEdgeValue, + instruction_info: Option, + passthrough: value::Map, + waiting: Option, + instruction_sent: bool, +} + +#[derive(Debug)] +struct Waiting { + instructions: Instructions, + resp: oneshot::Sender>, +} + +fn node_error( + event_tx: &EventSender, + result: &mut FlowRunResult, + node_id: NodeId, + times: u32, + error: String, +) { + event_tx + .unbounded_send( + NodeError { + time: Utc::now(), + node_id, + times, + error: error.clone(), + } + .into(), + ) + .ok(); + result + .node_errors + .entry((node_id, times)) + .or_default() + .push(error); +} + +impl State { + fn flow_error(&mut self, error: String) { + self.event_tx + .unbounded_send( + FlowError { + time: Utc::now(), + error: error.clone(), + } + .into(), + ) + .ok(); + self.result.flow_errors.push(error); + } + + async fn wait(mut self) -> (Self, Vec, Vec>) { + let len = self.running.len(); + let mut output_chunk = self.out_rx.ready_chunks(len); + let mut node_chunk = self.running.ready_chunks(len); + tracing::trace!("waiting for updates"); + let (outputs, finished) = + match futures::future::select(output_chunk.next(), node_chunk.next()).await { + Either::Left((outputs, fut)) => { + let outputs = outputs.unwrap_or_default(); + let finished = match futures::poll!(fut) { + Poll::Ready(t) => t.expect("running is not empty"), + Poll::Pending => Vec::new(), + }; + (outputs, finished) + } + Either::Right((finished, fut)) => { + let finished = finished.expect("running is not empty"); + let outputs = match futures::poll!(fut) { + Poll::Ready(t) => t.unwrap_or_default(), + Poll::Pending => Vec::new(), + }; + (outputs, finished) + } + }; + self.out_rx = output_chunk.into_inner(); + self.running = node_chunk.into_inner(); + (self, outputs, finished) + } +} + +#[derive(Debug, Default)] +pub struct FlowRunResult { + /// Collected from flow_output nodes + pub output: ValueSet, + pub node_outputs: HashMap>, + pub node_errors: HashMap<(NodeId, u32), Vec>, + /// List of nodes that didn't run (missing inputs) + pub not_run: Vec, + pub flow_errors: Vec, + pub instructions: Option, +} + +#[derive(ThisError, Debug)] +pub enum BuildGraphError { + #[error("2 edges connected to the same target")] + EdgesSameTarget, + #[error("edge's source not found: {:?}", .0)] + EdgeSourceNotFound(NodeId), + #[error("node not found in partial_config: {:?}", .0)] + NodeNotFoundInPartialConfig(NodeId), + #[error("node {:?}:{} has no input {:?}", .0.0, .1, .0.1)] + NoInput((NodeId, String), String), + #[error("node {:?}:{} has no output {:?}", .0.0, .1, .0.1)] + NoOutput((NodeId, String), String), +} + +impl FlowGraph { + pub async fn from_cfg( + c: FlowConfig, + registry: FlowRegistry, + partial_config: Option<&PartialConfig>, + ) -> crate::Result { + let flow_owner = registry.flow_owner; + let started_by = registry.started_by; + let signer = registry.signer.clone(); + let token = registry.token.clone(); + let rhai_permit = registry.rhai_permit.clone(); + let tx_exec_config = ExecutionConfig::from_env(&c.ctx.environment) + .inspect_err(|error| tracing::error!("error parsing ExecutionConfig: {}", error)) + .unwrap_or_default(); + let parent_flow_execute = registry.parent_flow_execute.clone(); + tracing::debug!("execution config: {:?}", tx_exec_config); + + let ext = { + let mut ext = Extensions::new(); + if let Some(rpc) = registry.rpc_server.clone() { + ext.insert(rpc); + } + ext.insert(registry); + ext.insert(tokio::runtime::Handle::current()); + ext + }; + + let ctx = Context::from_cfg(&c.ctx, flow_owner, started_by, signer, token, ext); + + let f = CommandFactory::new(); + + let mut g = StableGraph::new(); + let mut spawned = Vec::new(); + + let mut mocks = HashSet::new(); + let mut nodes = HashMap::new(); + let all_nodes = HashSet::from_iter(c.nodes.iter().map(|n| n.id)); + let only_nodes = partial_config + .map(|c| HashSet::::from_iter(c.only_nodes.iter().copied())) + .unwrap_or_else(|| all_nodes.clone()); + for id in &only_nodes { + if !all_nodes.contains(id) { + return Err(BuildGraphError::NodeNotFoundInPartialConfig(*id).into()); + } + } + let mut excluded_foreach = HashSet::new(); + for n in c.nodes { + if n.client_node_data.r#type == CommandType::Mock { + mocks.insert(n.id); + continue; + } + if !only_nodes.contains(&n.id) { + tracing::info!("excluding node {:?}", n.id); + if n.command_name == crate::command::foreach::FOREACH { + excluded_foreach.insert(n.id); + } + continue; + } + let command = f + .new_command(&n.command_name, &n.client_node_data, &mut spawned) + .await?; + let id = n.id; + let idx = g.add_node(id); + let node = Node { + id, + idx, + form_inputs: command.read_form_data(n.form_data), + command, + use_previous_values: <_>::default(), + }; + nodes.insert(id, node); + } + + let mut edges = c.edges; + edges.sort_by(|u, v| (&u.1, &u.0).cmp(&(&v.1, &v.0))); + + for (i, (from, to)) in edges.iter().enumerate() { + if i > 0 && edges[i - 1].1 == *to { + return Err(BuildGraphError::EdgesSameTarget.into()); + } + + let (to_idx, required) = match nodes.get(&to.0) { + None => { + if !all_nodes.contains(&from.0) { + return Err(BuildGraphError::EdgeSourceNotFound(from.0).into()); + } else if mocks.contains(&from.0) { + tracing::warn!("ignoring edge from mock node: {:?} -> {:?}", from, to); + continue; + } else { + tracing::trace!("ignoring edge from excluded node: {:?} -> {:?}", from, to); + continue; + } + } + Some(n) => { + let required = n + .command + .input_is_required(&to.1) + .ok_or_else(|| BuildGraphError::NoInput(to.clone(), n.command.name()))?; + (n.idx, required) + } + }; + let (from_idx, optional) = match nodes.get(&from.0) { + None => { + if !all_nodes.contains(&from.0) { + return Err(BuildGraphError::EdgeSourceNotFound(from.0).into()); + } else if mocks.contains(&from.0) { + tracing::warn!("ignoring edge from mock node: {:?} -> {:?}", from, to); + continue; + } else { + nodes + .get_mut(&g[to_idx]) + .unwrap() + .use_previous_values + .insert( + to.1.clone(), + UsePreviousValue { + node_id: from.0, + output_name: from.1.clone(), + foreach: excluded_foreach.contains(&from.0), + }, + ); + continue; + } + } + Some(n) => { + let optional = n + .command + .output_is_optional(&from.1) + .ok_or_else(|| BuildGraphError::NoOutput(from.clone(), n.command.name()))?; + (n.idx, optional) + } + }; + + g.add_edge( + from_idx, + to_idx, + Edge { + from: from.1.clone(), + to: to.1.clone(), + values: <_>::default(), + is_required_input: required, + is_optional_output: optional, + }, + ); + } + + Ok(Self { + id: c.id, + ctx, + g, + nodes, + mode: c.instructions_bundling, + output_instructions: false, + action_identity: None, + fees: Vec::new(), + rhai_permit, + tx_exec_config, + spawned, + parent_flow_execute, + }) + } + + pub fn get_interflow_instruction_info(&self) -> crate::Result { + let mut txs = self.sort_transactions()?; + if txs.len() != 1 { + return Err(crate::Error::NeedOneTx); + } + let tx = txs.pop().unwrap(); + + // find siganture output + let mut signature = tx + .iter() + .filter_map(|node_id| { + let idx = self.nodes[node_id].idx; + let mut flow_output = self + .out_edges(idx) + .map(|e| self.g[e.target()]) + .filter_map(|node_id| { + let cmd = &self.nodes[&node_id].command; + if cmd.name() == FLOW_OUTPUT { + cmd.outputs().pop().map(|o| o.name) + } else { + None + } + }) + .collect::>(); + if flow_output.len() != 1 { + return None; + } + Some(flow_output.pop().expect("flow_output.len() != 1")) + }) + .collect::>(); + if signature.len() != 1 { + return Err(crate::Error::NeedOneSignatureOutput); + } + let signature = signature.pop().unwrap(); + + // find after outputs + let g = self.g.filter_map( + |_, n| Some(n), + |eid, e| { + let (source, _) = self.g.edge_endpoints(eid)?; + let source_id = self.g[source]; + if tx.contains(&source_id) { + let info = self.nodes[&source_id] + .command + .instruction_info() + .expect("node is in tx"); + if !(info.signature == e.from || info.after.contains(&e.from)) { + return None; + } + } + Some(Edge { + from: e.from.clone(), + to: e.to.clone(), + is_required_input: e.is_required_input, + is_optional_output: e.is_optional_output, + values: <_>::default(), + }) + }, + ); + let mut bfs = new_bfs(&g, tx.iter().map(|id| self.nodes[id].idx)); + let mut after = Vec::new(); + while let Some(nid) = bfs.next(&g) { + let node = &self.nodes[g[nid]]; + if node.command.name() == FLOW_OUTPUT { + let label = match node.command.outputs().pop() { + Some(label) => label.name, + None => continue, + }; + if label != signature { + after.push(label); + } + } + } + + let before = self + .nodes + .values() + .filter_map(|n| { + (n.command.name() == FLOW_OUTPUT) + .then(|| n.command.outputs().pop().map(|o| o.name)) + .flatten() + }) + .filter(|name| name != &signature && !after.contains(name)) + .collect(); + + Ok(InstructionInfo { + signature, + before, + after, + }) + } + + pub fn need_previous_outputs(&self) -> HashSet { + self.nodes + .values() + .flat_map(|n| n.use_previous_values.values().map(|u| u.node_id)) + .collect() + } + + pub fn sort_transactions(&self) -> crate::Result>> { + let nodes = petgraph::algo::toposort(&self.g, None) + .map_err(|_| crate::Error::Cycle)? + .into_iter() + .map(|idx| self.g[idx]) + .collect::>(); + + let mut node_visited = HashSet::::new(); + let mut edge_visited = HashSet::::new(); + let mut result = Vec::new(); + loop { + let mut should_loop = false; + let mut has_instructions = IndexMap::new(); + for n in &nodes { + if node_visited.contains(n) { + continue; + } + + let node = &self.nodes[n]; + + let ready = self + .g + .edges_directed(node.idx, Direction::Incoming) + .all(|e| edge_visited.contains(&e.id())); + if !ready { + continue; + } + node_visited.insert(*n); + should_loop = true; + + let spread = match node.command.instruction_info() { + None => self + .g + .edges_directed(node.idx, Direction::Outgoing) + .map(|e| e.id()) + .collect::>(), + Some(info) => { + let before = &info.before; + let spread = self + .g + .edges_directed(node.idx, Direction::Outgoing) + .filter(|e| before.contains(&e.weight().from)) + .map(|e| e.id()) + .collect::>(); + has_instructions.insert(node.id, info); + spread + } + }; + edge_visited.extend(spread); + } + + let tx = has_instructions.keys().copied().collect::>(); + if !tx.is_empty() { + result.push(has_instructions.keys().copied().collect()); + + edge_visited.extend(has_instructions.iter().flat_map(|(n, info)| { + let idx = self.nodes[n].idx; + self.g + .edges_directed(idx, Direction::Outgoing) + .filter(|e| { + info.signature == e.weight().from + || info.after.contains(&e.weight().from) + }) + .map(|e| e.id()) + })); + } + + if !should_loop { + break; + } + } + + Ok(result) + } + + fn in_edges(&self, idx: NodeIndex) -> Edges<'_, Edge, Directed, DefaultIx> { + self.g.edges_directed(idx, Direction::Incoming) + } + + fn out_edges(&self, idx: NodeIndex) -> Edges<'_, Edge, Directed, DefaultIx> { + self.g.edges_directed(idx, Direction::Outgoing) + } + + fn submit_is_false(&self, idx: NodeIndex) -> bool { + self.in_edges(idx) + .find_map(|e| (e.weight().to == "submit").then(|| e.weight().values.front())) + .flatten() + .map(|v| v.value == Some(Value::Bool(false))) + .unwrap_or(false) + } + + fn ready(&self, idx: NodeIndex, s: &State) -> bool { + let id = self.g[idx]; + let cmd = &self.nodes[&id].command; + if cmd.instruction_info().is_some() { + // Don't start node if it has a `false` submit input + if self.submit_is_false(idx) { + return false; + } + } + if cmd.name() == crate::command::collect::COLLECT { + let source = match self.in_edges(idx).next() { + None => return true, + Some(e) => e.source(), + }; + // no nested loop, so collect can only run once + return !s.ran.contains_key(&id) && finished(self, s, source); + } + + let filled = self.in_edges(idx).all(|e| { + let w = e.weight(); + w.values + .front() + .map(|EdgeValue { value, .. }| value.is_some() || !w.is_required_input) + .unwrap_or_else(|| !w.is_required_input && finished(self, s, e.source())) + }); + if !filled { + return false; + } + + if s.ran.contains_key(&id) { + // must have at least 1 input from an array + self.in_edges(idx).any(|e| { + e.weight() + .values + .front() + .map(|v| v.tracker.is_array()) + .unwrap_or(false) + }) + } else { + true + } + } + + fn take_inputs(&mut self, idx: NodeIndex) -> (ValueSet, TrackEdgeValue) { + let in_edges = self.in_edges(idx).map(|e| e.id()).collect::>(); + let mut values = ValueSet::new(); + let mut tracker = TrackEdgeValue::None; + for edge_id in in_edges { + let w = self.g.edge_weight_mut(edge_id).unwrap(); + if let Some(EdgeValue { + value: Some(value), + tracker: edge_tracker, + }) = w.values.front() + { + let is_from_array = edge_tracker.is_array(); + tracker = tracker.zip(edge_tracker); + values.insert(w.to.clone(), value.clone()); + if is_from_array { + w.values.pop_front(); + } + } + } + (values, tracker) + } + + // For COLLECT command + fn collect_array_input(&mut self, idx: NodeIndex) -> (ValueSet, TrackEdgeValue) { + use crate::command::collect::ELEMENT; + + let in_edges = || self.g.edges_directed(idx, Direction::Incoming); + let array = match in_edges().next() { + None => Vec::new(), + Some(e) => { + let w = self.g.edge_weight_mut(e.id()).unwrap(); + let queue = std::mem::take(&mut w.values); + queue.into_iter().filter_map(|v| v.value).collect() + } + }; + + (value::map! { ELEMENT => array }, TrackEdgeValue::None) + } + + fn save_missing_optional_outputs(&mut self, node_id: NodeId, times: u32, s: &mut State) { + let info = s + .running_info + .get(&(node_id, times)) + .expect("node must be running"); + + tracing::trace!("saving {}:{}", info.id, info.command_name); + + let output = &s.result.node_outputs[&node_id][times as usize]; + let edges = self + .out_edges(info.node_idx) + .filter_map(|e| { + let w = e.weight(); + (w.is_optional_output && !output.contains_key(&w.from)).then_some(e.id()) + }) + .collect::>(); + for eid in edges { + let w = self.g.edge_weight_mut(eid).unwrap(); + w.values.push_back(EdgeValue { + value: None, + tracker: info.tracker.clone(), + }); + } + } + + fn save_outputs(&mut self, o: PartialOutput, s: &mut State) { + let info = s + .running_info + .get_mut(&(o.node_id, o.times)) + .expect("node must be running"); + + tracing::trace!("saving {}:{}", info.id, info.command_name); + + match o.output { + Ok((ins, mut values)) => { + if let Some(ins_info) = &info.instruction_info { + if info.instruction_sent { + for name in &ins_info.after { + if !values.contains_key(name) && info.passthrough.contains_key(name) { + values.insert(name.clone(), info.passthrough[name].clone()); + } + } + } else { + info.instruction_sent = true; + for name in &ins_info.before { + if !values.contains_key(name) && info.passthrough.contains_key(name) { + values.insert(name.clone(), info.passthrough[name].clone()); + } + } + } + } else { + // no instruction_info, + // node should only return inputs once + // this is the final output of the node + // we extend it with passthrough + values.extend(info.passthrough.iter().map(|(k, v)| (k.clone(), v.clone()))); + } + + let nid = info.node_idx; + let out_edges = self + .g + .edges_directed(nid, Direction::Outgoing) + .map(|e| e.id()) + .collect::>(); + + for eid in out_edges { + let w = self.g.edge_weight_mut(eid).unwrap(); + let value = match values.get(&w.from).cloned() { + Some(value) => value, + None => continue, + }; + + debug_assert!( + w.values.is_empty() + || w.values.front().unwrap().tracker.is_array() + == info.tracker.is_array() + ); + + let should_loop = info.command_name == crate::command::foreach::FOREACH; + + if should_loop && matches!(value, Value::Array(_)) { + let Value::Array(array) = value else { + unreachable!() + }; + let array_iter = + array.into_iter().enumerate().map(|(i, value)| EdgeValue { + value: Some(value), + tracker: info.tracker.nest((nid, i as u32)), + }); + + w.values.extend(array_iter); + } else { + w.values.push_back(EdgeValue { + value: Some(value), + tracker: info.tracker.clone(), + }); + } + } + + s.event_tx + .unbounded_send( + NodeOutput { + time: Utc::now(), + node_id: info.id, + times: info.times, + output: values.clone().into(), + } + .into(), + ) + .ok(); + s.result + .node_outputs + .get_mut(&info.id) + .expect("bug in start_node") + .get_mut(info.times as usize) + .expect("bug in start_node") + .extend(values); + + if ins.is_none() { + o.resp.send(Ok(execute::Response { signature: None })).ok(); + } else if info.instruction_info.is_some() { + info.waiting = Some(Waiting { + instructions: ins.expect("ins.is_none() == false"), + resp: o.resp, + }); + } else { + let error = "this node should not have instructions, did you forget to define instruction_info?"; + o.resp.send(Err(execute::Error::other(error))).ok(); + node_error( + &s.event_tx, + &mut s.result, + info.id, + info.times, + error.to_owned(), + ); + } + } + Err(error) => { + let err_str = error.to_string(); + o.resp.send(Err(error.into())).ok(); + node_error(&s.event_tx, &mut s.result, info.id, info.times, err_str); + } + } + } + + fn node_finished( + &mut self, + join_result: Result, + s: &mut State, + ) -> Result<(), String> { + match join_result { + Ok(Finished { + node, + times, + finished_at, + result, + }) => { + let (resp, _) = oneshot::channel(); + self.save_outputs( + PartialOutput { + node_id: node.id, + times, + output: result.map(|v| (None, v)), + resp, + }, + s, + ); + + self.save_missing_optional_outputs(node.id, times, s); + + let output = &s.result.node_outputs[&node.id][times as usize]; + let missing = node + .command + .outputs() + .into_iter() + .filter_map(|o| { + (!o.optional && !output.contains_key(&o.name)).then_some(o.name) + }) + .collect::>(); + if !missing.is_empty() { + node_error( + &s.event_tx, + &mut s.result, + node.id, + times, + format!("output not found: {:?}", missing), + ); + } + + tracing::trace!("node finished {}:{}", node.id, node.command.name()); + s.event_tx + .unbounded_send( + NodeFinish { + time: finished_at, + node_id: node.id, + times, + } + .into(), + ) + .ok(); + s.running_info.remove(&(node.id, times)); + self.nodes.insert(node.id, node); + Ok(()) + } + Err(error) => { + let error = if error.is_panic() { + format!("command panicked: {error}") + } else { + "task canceled".to_owned() + }; + s.flow_error(error.clone()); + Err(error) + } + } + } + + async fn collect_and_execute_instructions( + &mut self, + s: &mut State, + txs: Vec>, + ) -> ControlFlow> { + tracing::info!("collecting instructions"); + let mut txs: Vec>> = txs + .into_iter() + .map(|tx| tx.into_iter().map(|id| (id, None)).collect()) + .collect(); + for info in s.running_info.values_mut() { + let w = match info.waiting.take() { + Some(w) => w, + None => unreachable!("all_waiting == true"), + }; + let tx = txs.iter_mut().find(|tx| tx.contains_key(&info.id)); + let tx = match tx { + Some(tx) => tx, + None => { + return ControlFlow::Break(Err(format!( + "could not find transaction position of {}:{}", + info.id, info.command_name + ))); + } + }; + tx[&info.id] = Some(w); + } + + for tx in txs { + debug_assert!(!tx.is_empty()); + let is_complete = tx.iter().all(|(node_id, wait)| { + match self.nodes.get(node_id) { + Some(node) => { + // node is not running + self.submit_is_false(node.idx) + } + None => { + // none mean it is running + wait.is_some() + } + } + }); + if is_complete { + let mut tx = tx.into_values().rev().flatten().collect::>(); + while let Some(w) = tx.pop() { + use std::ops::Range; + struct Responder { + sender: oneshot::Sender>, + range: Range, + } + + let (mut ins, resp) = { + let mut ins = w.instructions; + if let Some(signer) = self + .tx_exec_config + .overwrite_feepayer + .clone() + .map(|x| x.to_keypair()) + { + ins.set_feepayer(signer); + } + let mut resp = vec![Responder { + sender: w.resp, + range: 0..ins.instructions.len(), + }]; + while let Some(w) = tx.pop() { + let old_len = ins.instructions.len(); + match ins.combine(w.instructions) { + Ok(_) => { + let new_len = ins.instructions.len(); + resp.push(Responder { + sender: w.resp, + range: old_len..new_len, + }); + } + Err(ins) => { + tx.push(Waiting { + instructions: ins, + resp: w.resp, + }); + break; + } + } + } + (ins, resp) + }; + if !self.fees.is_empty() { + ins.combine(Instructions { + lookup_tables: None, + fee_payer: ins.fee_payer, + signers: vec![], + instructions: transfer_many(&ins.fee_payer, &self.fees), + }) + .expect("same fee payer"); + } + + // this flow is an "Interflow instructions" + if self.output_instructions { + for resp in resp { + resp.sender.send(Err(execute::Error::Canceled(None))).ok(); + } + return ControlFlow::Break(Ok(ins)); + } + let res = if s.stop.token.is_cancelled() { + Err(execute::Error::Canceled(s.stop.get_reason())) + } else if s.stop_shared.token.is_cancelled() { + Err(execute::Error::Canceled(s.stop_shared.get_reason())) + } else if ins.instructions.is_empty() { + Ok(execute::Response { signature: None }) + } else if let Some(exec) = &self.parent_flow_execute { + self.collect_flow_output(s).await; + s.stop + .race( + std::pin::pin!(s.stop_shared.race( + std::pin::pin!(exec.call_ref(execute::Request { + instructions: ins, + output: s.result.output.clone(), + })), + execute::Error::Canceled, + )), + execute::Error::Canceled, + ) + .await + } else { + tracing::info!("executing instructions"); + let config = self.tx_exec_config.clone(); + let network = self.ctx.cfg.solana_client.cluster; + s.stop + .race( + std::pin::pin!(s.stop_shared.race( + std::pin::pin!(ins.execute( + &self.ctx.solana_client, + network, + self.ctx.signer.clone(), + Some(s.flow_run_id), + config, + )), + execute::Error::Canceled, + )), + execute::Error::Canceled, + ) + .await + .map(|signature| execute::Response { + signature: Some(signature), + }) + }; + + let failed_instruction = res.as_ref().err().and_then(|e| match e { + execute::Error::Solana { error, inserted } => { + find_failed_instruction(error) + .and_then(|pos| pos.checked_sub(*inserted)) + } + _ => None, + }); + for resp in resp { + if let Some(pos) = failed_instruction { + if resp.range.contains(&pos) { + resp.sender.send(res.clone()).ok(); + } else { + debug_assert!(res.is_err()); + resp.sender.send(Err(execute::Error::TxSimFailed)).ok(); + } + } else { + resp.sender.send(res.clone()).ok(); + } + } + + if let Err(error) = &res { + for w in tx.into_iter().rev() { + w.resp.send(Err(error.clone())).ok(); + } + break; + } + } + } else { + // Some nodes didn't output their instructions + for v in tx.into_values().flatten() { + v.resp.send(Err(execute::Error::TxIncomplete)).ok(); + } + } + } + ControlFlow::Continue(()) + } + + fn supply_partial_run_values(&mut self, fake_node: NodeIndex, s: &mut State) { + let out_edges = self + .out_edges(fake_node) + .map(|e| (e.id(), e.target())) + .collect::>(); + for (eid, target) in out_edges { + let target_id = self.g[target]; + let w = self.g.edge_weight_mut(eid).unwrap(); + let (node_id, output_name) = w.from.split_once('/').expect("separated by /"); + let node_id: Uuid = node_id.parse().expect("UUID"); + let outputs = s + .previous_values + .get(&node_id) + .expect("checked in `FlowGraph::run`"); + let mut i = 0; + // run_fake is run before all other nodes, so this won't + // panick + let foreach = self.nodes[&target_id].use_previous_values[&w.to].foreach; + let use_element = outputs.len() > 1; + for map in outputs { + let value = match map { + Value::Map(map) => match map.get(output_name) { + Some(value) => value.clone(), + None => { + tracing::warn!("value not found for port {:?}", output_name); + continue; + } + }, + _ => { + tracing::warn!("expecting map"); + continue; + } + }; + if foreach { + if let Value::Array(array) = value { + for value in array { + w.values.push_back(EdgeValue { + value: Some(value), + tracker: TrackEdgeValue::Element(fake_node, i as u32), + }); + i += 1; + } + } else { + tracing::error!("expecting array: {:?}", w.from); + } + } else { + let tracker = if use_element { + TrackEdgeValue::Element(fake_node, i as u32) + } else { + TrackEdgeValue::None + }; + w.values.push_back(EdgeValue { + value: Some(value), + tracker, + }); + i += 1; + } + } + tracing::trace!("{} values for fake edge {}:{}", i, node_id, output_name); + } + } + + pub async fn run( + &mut self, + event_tx: EventSender, + flow_run_id: FlowRunId, + flow_inputs: value::Map, + stop: StopSignal, + stop_shared: StopSignal, + previous_values: HashMap>, + ) -> FlowRunResult { + event_tx + .unbounded_send(FlowStart { time: Utc::now() }.into()) + .ok(); + + let (out_tx, out_rx) = mpsc::unbounded::(); + let mut s = State { + flow_run_id, + previous_values, + flow_inputs, + event_tx, + ran: HashMap::new(), + running: FuturesUnordered::new(), + running_info: HashMap::new(), + result: FlowRunResult::default(), + stop, + stop_shared, + out_tx, + out_rx, + }; + + let txs = self.sort_transactions(); + + match &txs { + Ok(txs) => { + let txs = txs + .iter() + .map(|tx| { + tx.iter() + .map(|id| { + let name = + self.nodes.get(id).expect("node not found").command.name(); + format!("{}:{}", id, name) + }) + .collect::>() + }) + .collect::>(); + tracing::trace!("transactions: {:?}", txs); + } + Err(error) => { + s.flow_error(format!("sort_transactions failed: {}", error)); + s.stop.token.cancel(); + } + } + + let fake_node = self.g.add_node(Uuid::nil()); + for n in self.nodes.values() { + for ( + input_name, + UsePreviousValue { + node_id, + output_name, + .. + }, + ) in &n.use_previous_values + { + if s.previous_values.contains_key(node_id) { + let is_required_input = n.command.input_is_required(input_name).unwrap_or(true); + self.g.add_edge( + fake_node, + n.idx, + Edge { + from: format!("{}/{}", node_id, output_name), + to: input_name.clone(), + values: <_>::default(), + is_required_input, + is_optional_output: !is_required_input, + }, + ); + } else { + // TODO: more diagnostic info + s.flow_error(format!("no value for port {:?}", input_name)); + s.stop.token.cancel(); + } + } + } + self.supply_partial_run_values(fake_node, &mut s); + + // TODO: is this the best way to do this + match Arc::get_mut(&mut self.ctx.extensions) { + Some(ext) => { + ext.insert(s.event_tx.clone()); + ext.insert(s.stop.token.clone()); + } + None => { + tracing::error!("could not insert to extensions, this is a bug"); + } + } + + 'LOOP: loop { + tracing::trace!("new round"); + if s.stop.token.is_cancelled() { + break; + } + let nodes = self.g.node_indices().collect::>(); + let mut started_new_nodes = false; + for idx in nodes { + let id = self.g.node_weight(idx).unwrap(); + + if !self.nodes.contains_key(id) { + continue; + } + + if self.ready(idx, &s) { + self.start_node(*id, &mut s); + started_new_nodes = true; + } + } + + if s.running.is_empty() { + break 'LOOP; + } + + let all_waiting = s.running_info.values().all(|i| i.waiting.is_some()); + if !started_new_nodes && all_waiting { + let txs = match &txs { + Ok(txs) => txs.clone(), + Err(error) => { + s.flow_error(error.to_string()); + s.stop.token.cancel(); + continue; + } + }; + + if let ControlFlow::Break(result) = + self.collect_and_execute_instructions(&mut s, txs).await + { + match result { + Ok(ins) => { + s.result.instructions = Some(ins); + } + Err(error) => { + s.flow_error(error); + } + } + s.stop.token.cancel(); + continue; + } + } + + let (outputs, finished); + (s, outputs, finished) = s.wait().await; + + for output in outputs { + self.save_outputs(output, &mut s); + } + for join_result in finished { + if let Err(error) = self.node_finished(join_result, &mut s) { + tracing::trace!("{}, stopping flow", error); + break 'LOOP; + } + } + } + + for id in self.nodes.keys() { + if !s.ran.contains_key(id) { + s.result.not_run.push(*id); + } + } + + self.collect_flow_output(&mut s).await; + + let failed = s + .result + .node_errors + .iter() + .filter(|(_, e)| !e.is_empty()) + .count(); + if failed > 0 { + s.flow_error(format!("{} nodes failed", failed)); + } + + s.event_tx + .unbounded_send( + FlowFinish { + time: Utc::now(), + output: s.result.output.clone().into(), + not_run: s.result.not_run.clone(), + } + .into(), + ) + .ok(); + + self.g.remove_node(fake_node); + + s.result + } + + async fn collect_flow_output(&self, s: &mut State) { + for (id, n) in self + .nodes + .iter() + .filter(|(_, n)| n.command.name() == FLOW_OUTPUT) + { + let name = &n.command.outputs()[0].name; + if let Some(values) = s.result.node_outputs.get(id) { + let mut values = values + .iter() + .filter_map(|o| o.get(name)) + .cloned() + .collect::>(); + let value = match values.len() { + 0 => continue, + 1 => values.pop().unwrap(), + _ => Value::Array(values), + }; + s.result.output.insert(name.clone(), value); + } + } + if let Some(ins) = s.result.instructions.take() { + let fee_payer = ins.fee_payer; + if !s.result.output.contains_key("transaction") { + match ins + .build_for_solana_action( + fee_payer, + self.action_identity, + &self.ctx.solana_client, + self.ctx.cfg.solana_client.cluster, + self.ctx.signer.clone(), + Some(s.flow_run_id), + &self.tx_exec_config, + ) + .await + { + Ok(tx) => { + let tx_bytes = tx.0.serialize(); + let tx_base64 = BASE64_STANDARD.encode(&tx_bytes); + s.result + .output + .insert("transaction".into(), Value::String(tx_base64)); + } + Err(error) => { + s.flow_error(error.to_string()); + } + } + } + } + } + + fn start_node(&mut self, id: NodeId, s: &mut State) { + let node = self.nodes.remove(&id).unwrap(); + let idx = node.idx; + let times = *s.ran.entry(node.id).and_modify(|t| *t += 1).or_insert(0); + + let (mut inputs, tracker) = match node.command.name().as_str() { + crate::command::flow_input::FLOW_INPUT => (s.flow_inputs.clone(), TrackEdgeValue::None), + crate::command::collect::COLLECT => self.collect_array_input(idx), + _ => self.take_inputs(idx), + }; + + for (name, value) in &node.form_inputs { + // use form values if they are not supplied by edges + inputs.entry(name.clone()).or_insert_with(|| value.clone()); + } + + s.running_info.insert( + (node.id, times), + RunningNodeInfo { + id: node.id, + times, + node_idx: node.idx, + command_name: node.command.name(), + tracker, + instruction_info: node.command.instruction_info(), + passthrough: node.command.passthrough_outputs(&inputs), + waiting: None, + instruction_sent: false, + }, + ); + let outputs = s.result.node_outputs.entry(node.id).or_default(); + debug_assert_eq!(outputs.len(), times as usize); + outputs.push(<_>::default()); + let rhai_permit = self.rhai_permit.clone(); + let is_rhai_script = rhai_script::is_rhai_script(&node.command.name()); + let span = + tracing::error_span!(NODE_SPAN_NAME, node_id = node.id.to_string(), times = times); + let task = run_command( + node, + s.flow_run_id, + times, + inputs, + self.ctx.clone(), + s.event_tx.clone(), + s.stop.clone(), + s.stop_shared.clone(), + s.out_tx.clone(), + self.mode.clone(), + self.tx_exec_config.clone(), + ); + let handler = tokio::spawn( + async move { + if is_rhai_script { + let p = rhai_permit.acquire().await; + let result = task.await; + std::mem::drop(p); + result + } else { + task.await + } + } + .instrument(span), + ); + + s.running.push(handler); + } +} + +struct ExecuteWithBundling { + node_id: NodeId, + times: u32, + tx: mpsc::UnboundedSender, +} + +impl tower::Service for ExecuteWithBundling { + type Response = execute::Response; + type Error = execute::Error; + type Future = BoxFuture<'static, Result>; + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + fn call(&mut self, req: execute::Request) -> Self::Future { + let (tx, rx) = oneshot::channel(); + self.tx + .unbounded_send(PartialOutput { + node_id: self.node_id, + times: self.times, + output: Ok((Some(req.instructions), req.output)), + resp: tx, + }) + .ok(); + rx.map(|r| r?).boxed() + } +} + +struct ExecuteNoBundling { + node_id: NodeId, + times: u32, + tx: mpsc::UnboundedSender, + stop_shared: StopSignal, + simple_svc: execute::Svc, + overwrite_feepayer: Option, +} + +impl tower::Service for ExecuteNoBundling { + type Response = execute::Response; + type Error = execute::Error; + type Future = BoxFuture<'static, Result>; + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + fn call(&mut self, mut req: execute::Request) -> Self::Future { + use tower::ServiceExt; + if self.stop_shared.token.is_cancelled() { + let reason = self.stop_shared.get_reason(); + return Box::pin(async move { Err(execute::Error::Canceled(reason)) }); + } + // execute before sending the partial output + let mut svc = self.simple_svc.clone(); + let tx = self.tx.clone(); + let node_id = self.node_id; + let times = self.times; + let output = req.output.clone(); + let overwrite_feepayer = self.overwrite_feepayer.clone(); + let task = async move { + if let Some(signer) = overwrite_feepayer { + req.instructions.set_feepayer(signer); + } + let res = svc.ready().await?.call(req).await; + let output = match &res { + Ok(_) => Ok((Instructions::default(), output)), + Err(error) => Err(error.clone().into()), + }; + // send output with empty instructions after we've executed them + // only send on Ok, because the node should send Err by itself + if output.is_ok() { + let (resp, rx) = oneshot::channel(); + tx.unbounded_send(PartialOutput { + node_id, + times, + output: output.map(|(ins, output)| (Some(ins), output)), + resp, + }) + .ok(); + rx.await.ok(); + } + res + } + .boxed(); + let stop = self.stop_shared.clone(); + Box::pin(async move { stop.race(task, execute::Error::Canceled).await }) + } +} + +struct Finished { + node: Node, + times: u32, + finished_at: DateTime, + result: Result, +} + +#[allow(clippy::too_many_arguments)] +async fn run_command( + node: Node, + flow_run_id: FlowRunId, + times: u32, + inputs: value::Map, + mut ctx: Context, + event_tx: EventSender, + stop: StopSignal, + stop_shared: StopSignal, + tx: mpsc::UnboundedSender, + mode: client::BundlingMode, + tx_exec_config: ExecutionConfig, +) -> Finished { + let svc = match mode { + client::BundlingMode::Off => TowerClient::from_service( + ExecuteNoBundling { + node_id: node.id, + times, + tx: tx.clone(), + simple_svc: execute::simple(&ctx, 32, Some(flow_run_id), tx_exec_config.clone()), + stop_shared, + overwrite_feepayer: tx_exec_config + .overwrite_feepayer + .clone() + .map(|x| x.to_keypair()), + }, + execute::Error::worker, + 32, + ), + client::BundlingMode::Automatic => TowerClient::from_service( + ExecuteWithBundling { + node_id: node.id, + times, + tx: tx.clone(), + }, + execute::Error::worker, + 32, + ), + }; + ctx.command = Some(CommandContext { + svc, + flow_run_id, + node_id: node.id, + times, + }); + if !node.command.permissions().user_tokens { + ctx.get_jwt = get_jwt::not_allowed(); + } + + event_tx + .unbounded_send( + NodeStart { + time: Utc::now(), + node_id: node.id, + times, + input: inputs.clone().into(), + } + .into(), + ) + .ok(); + + tracing::trace!("starting node {}:{}", node.id, node.command.name()); + let result = stop + .race(node.command.run(ctx, inputs), |reason| { + crate::Error::Canceled(reason).into() + }) + .await; + + Finished { + node, + times, + result, + finished_at: Utc::now(), + } +} + +fn finished(f: &FlowGraph, s: &State, nid: NodeIndex) -> bool { + let mut visited = HashSet::new(); + + finished_recursive(f, s, nid, &mut visited) +} + +fn finished_recursive( + f: &FlowGraph, + s: &State, + nid: NodeIndex, + visited: &mut HashSet>, +) -> bool { + if !visited.insert(nid) { + return false; + } + + if !f.nodes.contains_key(&f.g[nid]) { + // running + return false; + } + + let ran = s.ran.contains_key(&f.g[nid]); + + if f.in_edges(nid).count() == 0 { + return ran; + } + + let mut has_array_input = false; + let mut filled = true; + let mut all_sources_not_finished = true; + + for e in f.in_edges(nid) { + if let Some(EdgeValue { + value: Some(_), + tracker, + }) = e.weight().values.front() + { + has_array_input |= tracker.is_array(); + } else if e.weight().is_required_input { + let source_finished = finished_recursive(f, s, e.source(), visited); + all_sources_not_finished &= !source_finished; + filled = false; + } + } + + if filled { + if has_array_input { + false + } else { + ran + } + } else { + !all_sources_not_finished + } +} + +fn new_bfs(graph: G, entrypoint: I) -> Bfs +where + G: GraphRef + Visitable, + I: IntoIterator, +{ + let mut discovered = graph.visit_map(); + let mut stack = VecDeque::new(); + for id in entrypoint.into_iter() { + discovered.visit(id); + stack.push_back(id); + } + Bfs { stack, discovered } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::flow_run_events::event_channel; + use anyhow::anyhow; + use flow_lib::config::client::ClientConfig; + + use cmds_solana as _; + use cmds_std as _; + + #[derive(serde::Deserialize)] + struct TestFile { + flow: ClientConfig, + } + + #[tokio::test] + async fn test_stop() { + let task = async { + tokio::time::sleep(Duration::from_secs(4)).await; + Ok::<_, anyhow::Error>(()) + }; + let first = StopSignal::new(); + let second = StopSignal::new(); + + tokio::spawn({ + let second = second.clone(); + async move { + tokio::time::sleep(Duration::from_secs(1)).await; + second.stop(0, None); + } + }); + + let error = first + .race( + std::pin::pin!(second.race(std::pin::pin!(task), |_| anyhow!("second"))), + |_| anyhow!("first"), + ) + .await + .unwrap_err() + .to_string(); + assert_eq!(error, "second"); + } + + #[tokio::test] + async fn test_foreach_nested() { + let json = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_files/2_foreach.json" + )); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + assert_eq!( + res.output["output"], + Value::Array( + [ + Value::String("0,0".to_owned()), + Value::String("0,1".to_owned()), + Value::String("1,0".to_owned()), + Value::String("1,1".to_owned()), + ] + .to_vec() + ) + ); + } + + #[tokio::test] + async fn test_uneven_loop() { + let json = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_files/uneven_loop.json" + )); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let mut flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + let (tx, _rx) = event_channel(); + let res = flow + .run( + tx, + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + <_>::default(), + ) + .await; + + assert_eq!( + res.output["1"], + Value::Array([Value::U64(1), Value::U64(2), Value::U64(3),].to_vec()) + ); + assert_eq!( + res.output["2"], + Value::Array( + [ + Value::String("0,0".to_owned()), + Value::String("0,1".to_owned()), + Value::String("1,0".to_owned()), + ] + .to_vec() + ) + ); + } + + /* + * // TODO: a node in this flow changed + #[tokio::test] + async fn test_collect_instructions() { + tracing_subscriber::fmt::try_init().ok(); + let json = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/test_files/nft.json")); + let flow_config = FlowConfig::new(serde_json::from_str::(json).unwrap().flow); + let flow = FlowGraph::from_cfg(flow_config, <_>::default(), None) + .await + .unwrap(); + + let mut txs = flow.sort_transactions().unwrap(); + assert_eq!(txs.len(), 1); + let tx = txs.pop().unwrap(); + let mut names = Vec::new(); + for id in tx { + let name = flow.nodes[&id].command.name(); + names.push(name); + } + let expected = [ + "create_mint_account", + "associated_token_account", + "mint_token", + "create_metadata_account", + "create_master_edition", + ]; + assert_eq!(names, expected); + } + */ +} diff --git a/crates/flow/src/flow_registry.rs b/crates/flow/src/flow_registry.rs new file mode 100644 index 00000000..72a808fa --- /dev/null +++ b/crates/flow/src/flow_registry.rs @@ -0,0 +1,670 @@ +use crate::{ + command::{interflow, interflow_instructions}, + flow_graph::FlowRunResult, + flow_run_events::{self, EventSender, NodeLog}, + FlowGraph, +}; +use chrono::Utc; +use flow_lib::{ + config::{ + client::{BundlingMode, ClientConfig, FlowRunOrigin, PartialConfig}, + Endpoints, + }, + context::{execute, get_jwt, signer, User}, + solana::{ExecuteOn, Pubkey, SolanaActionConfig}, + utils::TowerClient, + CommandType, FlowConfig, FlowId, FlowRunId, NodeId, SolanaClientConfig, UserId, ValueSet, +}; +use futures::channel::oneshot; +use hashbrown::HashMap; +use serde_json::Value as JsonValue; +use std::sync::{Arc, Mutex}; +use thiserror::Error as ThisError; +use tokio::sync::Semaphore; +use tokio_util::sync::CancellationToken; +use tracing::Instrument; +use utils::actix_service::ActixService; + +// make a registry from entrypoint flow (currently) +// get a registry from DB +// start flow + +pub const MAX_CALL_DEPTH: u32 = 1024; + +#[derive(Debug, ThisError)] +pub enum StopError { + #[error("flow not found")] + NotFound, + #[error("forbidden")] + Forbidden, +} + +#[derive(Default)] +pub struct StartFlowOptions { + pub partial_config: Option, + pub collect_instructions: bool, + pub action_identity: Option, + pub action_config: Option, + pub fees: Vec<(Pubkey, u64)>, + pub origin: FlowRunOrigin, + pub solana_client: Option, + pub parent_flow_execute: Option, +} + +/// A collection of flows config to run together +#[derive(Clone, bon::Builder)] +pub struct FlowRegistry { + flows: Arc>, + pub(crate) flow_owner: User, + pub(crate) started_by: User, + shared_with: Vec, + signers_info: JsonValue, + endpoints: Endpoints, + + depth: u32, + + pub(crate) signer: signer::Svc, + pub(crate) token: get_jwt::Svc, + new_flow_run: new_flow_run::Svc, + get_previous_values: get_previous_values::Svc, + pub(crate) parent_flow_execute: Option, + + pub(crate) rhai_permit: Arc, + rhai_tx: Arc>>>, + + pub(crate) rpc_server: Option>, +} + +impl Default for FlowRegistry { + fn default() -> Self { + let signer = signer::unimplemented_svc(); + let new_flow_run = new_flow_run::unimplemented_svc(); + let get_previous_values = get_previous_values::unimplemented_svc(); + let token = get_jwt::unimplemented_svc(); + Self { + depth: 0, + flow_owner: User::new(UserId::nil()), + started_by: User::new(UserId::nil()), + shared_with: <_>::default(), + flows: <_>::default(), + endpoints: <_>::default(), + signers_info: <_>::default(), + signer, + token, + new_flow_run, + get_previous_values, + parent_flow_execute: None, + rhai_permit: Arc::new(Semaphore::new(1)), + rhai_tx: <_>::default(), + rpc_server: None, // TODO: try this + } + } +} + +async fn get_all_flows( + entrypoint: FlowId, + user_id: UserId, + mut get_flow: get_flow::Svc, + environment: HashMap, +) -> crate::Result> { + let mut flows = HashMap::new(); + + let mut queue = [entrypoint].to_vec(); + while let Some(flow_id) = queue.pop() { + let config = { + let mut config = get_flow + .call_mut(get_flow::Request { user_id, flow_id }) + .await? + .config; + for (k, v) in &environment { + config + .environment + .entry(k.clone()) + .or_insert_with(|| v.clone()); + } + config + }; + let interflow_nodes = config + .nodes + .iter() + .filter(|n| { + n.data.r#type == CommandType::Native + && (n.data.node_id == interflow::INTERFLOW + || n.data.node_id == interflow_instructions::INTERFLOW_INSTRUCTIONS) + }) + .map(|n| (n.id, interflow::get_interflow_id(&n.data))); + for (node_id, result) in interflow_nodes { + match result { + Ok(id) => { + if id != flow_id && !flows.contains_key(&id) { + queue.push(id); + } + } + Err(error) => { + return Err(get_flow::Error::InvalidInferflow { + flow_id, + node_id, + error, + } + .into()) + } + } + } + flows.insert(flow_id, config); + } + + let mut info = HashMap::new(); + for (id, config) in flows.iter_mut() { + if config.instructions_bundling != BundlingMode::Off { + let g = FlowGraph::from_cfg( + FlowConfig::new(config.clone()), + FlowRegistry::default(), + None, + ) + .await?; + config.interflow_instruction_info = g + .get_interflow_instruction_info() + .map_err(|error| error.to_string()); + if let Ok(i) = config.interflow_instruction_info.as_ref() { + info.insert(*id, i.clone()); + } + } + } + + for (parent_flow, config) in flows.iter_mut() { + let interflows = config.nodes.iter_mut().filter(|n| { + n.data.r#type == CommandType::Native && n.data.node_id == interflow::INTERFLOW + }); + + for n in interflows { + let flow_id = interflow::get_interflow_id(&n.data).map_err(|error| { + get_flow::Error::InvalidInferflow { + flow_id: *parent_flow, + node_id: n.id, + error, + } + })?; + n.data.instruction_info = info.get(&flow_id).cloned(); + } + } + + Ok(flows) +} + +fn spawn_rhai_thread(rx: crossbeam_channel::Receiver) { + tokio::task::spawn_blocking(move || { + let mut engine = rhai_script::setup_engine(); + while let Ok((req, tx)) = rx.recv() { + match (req.ctx.extensions.get::(), &req.ctx.command) { + (Some(tx), Some(cmd)) => { + let tx1 = tx.clone(); + let tx2 = tx.clone(); + let node_id = cmd.node_id; + let times = cmd.times; + engine + .on_print(move |s| { + tx1.unbounded_send( + NodeLog { + time: Utc::now(), + node_id, + times, + level: flow_run_events::LogLevel::Info, + module: None, + content: s.to_owned(), + } + .into(), + ) + .ok(); + }) + .on_debug(move |s, _, pos| { + let module = if let (Some(line), Some(position)) = + (pos.line(), pos.position()) + { + Some(format!("script.rhai:{}:{}", line, position)) + } else { + None + }; + tx2.unbounded_send( + NodeLog { + time: Utc::now(), + node_id, + times, + level: flow_run_events::LogLevel::Debug, + module, + content: s.to_owned(), + } + .into(), + ) + .ok(); + }); + } + _ => { + engine + .on_print(|s| { + tracing::info!("rhai: {}", s); + }) + .on_debug(move |s, _, pos| { + tracing::info!("rhai: {}, at {}", s, pos); + }); + } + } + match req.ctx.extensions.get::().cloned() { + Some(stop_token) => { + engine.on_progress(move |c| { + (c % 4096 == 0 && stop_token.is_cancelled()).then(|| "canceled".into()) + }); + } + None => { + engine.on_progress(|_| None); + } + } + let result = req.command.run(&mut engine, req.ctx, req.input); + if tx.send(result).is_err() { + tracing::debug!("command stopped waiting"); + } + } + }); +} + +impl FlowRegistry { + #[allow(clippy::too_many_arguments)] + pub async fn new( + flow_owner: User, + started_by: User, + shared_with: Vec, + entrypoint: FlowId, + (signer, signers_info): (signer::Svc, JsonValue), + new_flow_run: new_flow_run::Svc, + get_flow: get_flow::Svc, + get_previous_values: get_previous_values::Svc, + token: get_jwt::Svc, + environment: HashMap, + endpoints: Endpoints, + ) -> crate::Result { + let flows = get_all_flows(entrypoint, flow_owner.id, get_flow, environment).await?; + Ok(Self { + depth: 0, + flow_owner, + started_by, + shared_with, + flows: Arc::new(flows), + signer, + signers_info, + new_flow_run, + get_previous_values, + parent_flow_execute: None, + token, + endpoints, + rhai_permit: Arc::new(Semaphore::new(1)), + rhai_tx: <_>::default(), + rpc_server: srpc::Server::start_http_server() + .inspect_err(|error| tracing::error!("srpc error: {}", error)) + .ok(), + }) + } + + pub async fn run_rhai( + &self, + req: run_rhai::Request, + ) -> Result { + let worker = { + let mut tx = self.rhai_tx.lock().unwrap(); + if tx.is_none() { + let (new_tx, rx) = crossbeam_channel::unbounded(); + spawn_rhai_thread(rx); + *tx = Some(new_tx.clone()); + } + tx.clone().unwrap() + }; + let (tx, rx) = oneshot::channel(); + worker + .send((req, tx)) + .map_err(|_| run_rhai::Error::msg("rhai worker stopped"))?; + rx.await + .map_err(|_| run_rhai::Error::msg("rhai worker stopped"))? + } + + #[allow(clippy::too_many_arguments)] + pub async fn from_actix( + flow_owner: User, + started_by: User, + shared_with: Vec, + entrypoint: FlowId, + (signer, signers_info): (actix::Recipient, JsonValue), + new_flow_run: actix::Recipient, + get_flow: actix::Recipient, + get_previous_values: actix::Recipient, + token: actix::Recipient, + environment: HashMap, + endpoints: Endpoints, + ) -> crate::Result { + Self::new( + flow_owner, + started_by, + shared_with, + entrypoint, + ( + TowerClient::from_service(ActixService::from(signer), signer::Error::Worker, 16), + signers_info, + ), + TowerClient::from_service( + ActixService::from(new_flow_run), + new_flow_run::Error::Worker, + 16, + ), + TowerClient::from_service(ActixService::from(get_flow), get_flow::Error::Worker, 16), + TowerClient::from_service( + ActixService::from(get_previous_values), + get_previous_values::Error::Worker, + 16, + ), + TowerClient::from_service( + tower::retry::Retry::new( + get_jwt::RetryPolicy::default(), + ActixService::from(token), + ), + get_jwt::Error::worker, + 16, + ), + environment, + endpoints, + ) + .await + } + + pub async fn start( + &self, + flow_id: FlowId, + inputs: ValueSet, + options: StartFlowOptions, + ) -> Result<(FlowRunId, tokio::task::JoinHandle), new_flow_run::Error> { + let StartFlowOptions { + partial_config, + collect_instructions, + action_identity, + action_config, + fees, + origin, + solana_client, + parent_flow_execute, + } = options; + let config = self + .flows + .get(&flow_id) + .ok_or(new_flow_run::Error::NotFound)?; + let solana_client = solana_client.unwrap_or(config.sol_network.clone().into()); + + if self.depth >= MAX_CALL_DEPTH { + return Err(new_flow_run::Error::MaxDepthReached); + } + let this = Self { + depth: self.depth + 1, + parent_flow_execute, + ..self.clone() + }; + + let (tx, rx) = flow_run_events::channel(); + let run = self + .new_flow_run + .call_ref(new_flow_run::Request { + user_id: self.flow_owner.id, + shared_with: self.shared_with.clone(), + config: ClientConfig { + call_depth: self.depth, + origin, + sol_network: solana_client.clone().into(), + collect_instructions, + partial_config: partial_config.clone(), + instructions_bundling: if collect_instructions + && matches!(config.instructions_bundling, BundlingMode::Off) + { + BundlingMode::Automatic + } else { + config.instructions_bundling.clone() + }, + signers: self.signers_info.clone(), + ..config.clone() + }, + inputs: inputs.clone(), + tx: tx.clone(), + stream: Box::pin(rx), + }) + .await?; + + let flow_run_id = run.flow_run_id; + let stop = run.stop_signal; + let stop_shared = run.stop_shared_signal; + + async move { + this.flows.iter().for_each(|(id, flow)| { + if let Err(error) = &flow.interflow_instruction_info { + tracing::debug!("flow {} no instruction_info: {}", id, error); + } + }); + + let mut get_previous_values_svc = this.get_previous_values.clone(); + let user_id = this.flow_owner.id; + let mut flow_config = FlowConfig::new(config.clone()); + flow_config.ctx.endpoints = this.endpoints.clone(); + flow_config.ctx.solana_client = solana_client.clone(); + let mut flow = FlowGraph::from_cfg(flow_config, this, partial_config.as_ref()).await?; + + if let Some(config) = action_config { + flow.tx_exec_config.execute_on = ExecuteOn::SolanaAction(config); + } + flow.action_identity = action_identity; + flow.fees = fees; + + if collect_instructions { + if let BundlingMode::Off = flow.mode { + flow.mode = BundlingMode::Automatic; + } + flow.output_instructions = true; + } + + let nodes = flow.need_previous_outputs(); + let nodes = nodes + .into_iter() + .filter_map(|id| { + partial_config + .as_ref() + .and_then(|c| { + c.values_config + .nodes + .get(&id) + .copied() + .or(c.values_config.default_run_id) + }) + .map(|run_id| (id, run_id)) + }) + .collect::>(); + let previous_values = if !nodes.is_empty() { + get_previous_values_svc + .call_mut(get_previous_values::Request { user_id, nodes }) + .await? + .values + } else { + <_>::default() + }; + + let join_handle = tokio::spawn( + async move { + flow.run(tx, flow_run_id, inputs, stop, stop_shared, previous_values) + .await + } + .in_current_span(), + ); + + Ok((flow_run_id, join_handle)) + } + .instrument(run.span) + .await + } +} + +pub mod run_rhai { + use flow_lib::{command::CommandError, Context, ValueSet}; + use futures::channel::oneshot; + use std::sync::Arc; + + pub type ChannelMessage = (Request, oneshot::Sender>); + + pub struct Request { + pub command: Arc, + pub ctx: Context, + pub input: ValueSet, + } + + pub type Response = ValueSet; + + pub type Error = CommandError; +} + +pub mod new_flow_run { + use crate::{ + flow_graph::StopSignal, + flow_run_events::{Event, EventSender}, + }; + use flow_lib::{ + config::client::ClientConfig, utils::TowerClient, BoxError, FlowRunId, UserId, ValueSet, + }; + use futures::stream::BoxStream; + use thiserror::Error as ThisError; + + pub type Svc = TowerClient; + + pub struct Request { + pub user_id: UserId, + pub config: ClientConfig, + pub shared_with: Vec, + pub inputs: ValueSet, + pub tx: EventSender, + pub stream: BoxStream<'static, Event>, + } + + impl actix::Message for Request { + type Result = Result; + } + + #[derive(ThisError, Debug)] + pub enum Error { + #[error("recursive depth reached")] + MaxDepthReached, + #[error("flow not found")] + NotFound, + #[error("unauthorized")] + Unauthorized, + #[error(transparent)] + GetPreviousValues(#[from] super::get_previous_values::Error), + #[error(transparent)] + BuildFlow(#[from] crate::Error), + #[error(transparent)] + Worker(tower::BoxError), + #[error(transparent)] + MailBox(#[from] actix::MailboxError), + #[error(transparent)] + Other(#[from] BoxError), + } + + impl Error { + pub fn other>(e: E) -> Self { + Self::Other(e.into()) + } + } + + pub struct Response { + pub flow_run_id: FlowRunId, + pub stop_signal: StopSignal, + pub stop_shared_signal: StopSignal, + pub span: tracing::Span, + } + + pub fn unimplemented_svc() -> Svc { + Svc::unimplemented(|| BoxError::from("unimplemented").into(), Error::Worker) + } +} + +pub mod get_flow { + use flow_lib::{ + config::client::ClientConfig, utils::TowerClient, BoxError, FlowId, NodeId, UserId, + }; + use thiserror::Error as ThisError; + + pub type Svc = TowerClient; + + pub struct Request { + pub user_id: UserId, + pub flow_id: FlowId, + } + + impl actix::Message for Request { + type Result = Result; + } + + pub struct Response { + pub config: ClientConfig, + } + + #[derive(ThisError, Debug)] + pub enum Error { + #[error("flow not found")] + NotFound, + #[error("unauthorized")] + Unauthorized, + #[error( + "parsing interflow failed, flow_id={}, node_id={}: {}", + flow_id, + node_id, + error + )] + InvalidInferflow { + flow_id: FlowId, + node_id: NodeId, + error: serde_json::Error, + }, + #[error(transparent)] + Worker(tower::BoxError), + #[error(transparent)] + MailBox(#[from] actix::MailboxError), + #[error(transparent)] + Other(#[from] BoxError), + } +} + +pub mod get_previous_values { + use flow_lib::{utils::TowerClient, BoxError, FlowRunId, NodeId, UserId}; + use hashbrown::HashMap; + use thiserror::Error as ThisError; + use value::Value; + + pub type Svc = TowerClient; + + pub struct Request { + pub user_id: UserId, + pub nodes: HashMap, + } + + impl actix::Message for Request { + type Result = Result; + } + + pub struct Response { + pub values: HashMap>, + } + + #[derive(ThisError, Debug)] + pub enum Error { + #[error("unauthorized")] + Unauthorized, + #[error(transparent)] + Worker(tower::BoxError), + #[error(transparent)] + MailBox(#[from] actix::MailboxError), + #[error(transparent)] + Other(#[from] BoxError), + } + + pub fn unimplemented_svc() -> Svc { + Svc::unimplemented(|| BoxError::from("unimplemented").into(), Error::Worker) + } +} diff --git a/crates/flow/src/flow_run_events.rs b/crates/flow/src/flow_run_events.rs new file mode 100644 index 00000000..9f36eafb --- /dev/null +++ b/crates/flow/src/flow_run_events.rs @@ -0,0 +1,155 @@ +use chrono::{DateTime, Utc}; +use flow_lib::{context::signer::SignatureRequest, NodeId}; +use serde::Serialize; +use value::Value; + +#[derive(derive_more::From, actix::Message, Clone, Debug, Serialize)] +#[rtype(result = "()")] +#[serde(tag = "event", content = "data")] +pub enum Event { + FlowStart(FlowStart), + FlowError(FlowError), + FlowLog(FlowLog), + FlowFinish(FlowFinish), + NodeStart(NodeStart), + NodeOutput(NodeOutput), + NodeError(NodeError), + NodeLog(NodeLog), + NodeFinish(NodeFinish), + SignatureRequest(SignatureRequest), +} + +impl Event { + pub fn time(&self) -> DateTime { + match self { + Event::FlowStart(e) => e.time, + Event::FlowError(e) => e.time, + Event::FlowLog(e) => e.time, + Event::FlowFinish(e) => e.time, + Event::NodeStart(e) => e.time, + Event::NodeOutput(e) => e.time, + Event::NodeError(e) => e.time, + Event::NodeLog(e) => e.time, + Event::NodeFinish(e) => e.time, + Event::SignatureRequest(e) => e.time, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Default)] +pub enum LogLevel { + Trace, + Debug, + #[default] + Info, + Warn, + Error, +} + +impl std::fmt::Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.serialize(f) + } +} + +impl From for LogLevel { + fn from(value: tracing::Level) -> Self { + match value { + tracing::Level::TRACE => LogLevel::Trace, + tracing::Level::DEBUG => LogLevel::Debug, + tracing::Level::INFO => LogLevel::Info, + tracing::Level::WARN => LogLevel::Warn, + tracing::Level::ERROR => LogLevel::Error, + } + } +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct FlowStart { + pub time: DateTime, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct FlowError { + pub time: DateTime, + pub error: String, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct FlowLog { + pub time: DateTime, + pub level: LogLevel, + pub module: Option, + pub content: String, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct FlowFinish { + pub time: DateTime, + pub not_run: Vec, + pub output: Value, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct NodeStart { + pub time: DateTime, + pub node_id: NodeId, + pub times: u32, + pub input: Value, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct NodeOutput { + pub time: DateTime, + pub node_id: NodeId, + pub times: u32, + pub output: Value, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct NodeError { + pub time: DateTime, + pub node_id: NodeId, + pub times: u32, + pub error: String, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct NodeLog { + pub time: DateTime, + pub node_id: NodeId, + pub times: u32, + pub level: LogLevel, + pub module: Option, + pub content: String, +} + +#[derive(actix::Message, Default, Clone, Debug, Serialize)] +#[rtype(result = "()")] +pub struct NodeFinish { + pub time: DateTime, + pub node_id: NodeId, + pub times: u32, +} + +pub fn channel() -> (EventSender, EventReceiver) { + futures::channel::mpsc::unbounded() +} +pub type EventSender = futures::channel::mpsc::UnboundedSender; +pub type EventReceiver = futures::channel::mpsc::UnboundedReceiver; + +pub fn event_channel() -> (EventSender, EventReceiver) { + futures::channel::mpsc::unbounded() +} + +pub const DEFAULT_LOG_FILTER: &str = "info,solana_client=debug"; +pub const FLOW_SPAN_NAME: &str = "flow_logs"; +pub const NODE_SPAN_NAME: &str = "node_logs"; diff --git a/crates/flow/src/flow_set.rs b/crates/flow/src/flow_set.rs new file mode 100644 index 00000000..22022089 --- /dev/null +++ b/crates/flow/src/flow_set.rs @@ -0,0 +1,377 @@ +use flow_lib::{ + config::{ + client::{ClientConfig, FlowRow, FlowRunOrigin}, + Endpoints, + }, + context::{execute, get_jwt, signer}, + solana::{Pubkey, SolanaActionConfig}, + CommandType, FlowId, FlowRunId, NodeId, SolanaClientConfig, User, UserId, ValueSet, +}; +use getset::Getters; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::sync::{Arc, Mutex}; +use tokio::{sync::Semaphore, task::JoinHandle}; +use tower::ServiceExt; +use uuid::Uuid; + +use crate::{ + command::{interflow, interflow_instructions}, + flow_graph::FlowRunResult, + flow_registry::{get_previous_values, new_flow_run, run_rhai, FlowRegistry, StartFlowOptions}, +}; + +/// Who can start flows +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub enum StartPermission { + /// Only flow owner can start + Owner, + /// Any authenticated user + Authenticated, + /// Any unauthenticated user + Anonymous, +} + +#[derive(bon::Builder, Clone, Debug)] +pub struct Flow { + pub row: FlowRow, +} + +impl Flow { + pub fn start_permission(&self) -> StartPermission { + match ( + self.row.is_public, + self.row.start_shared, + self.row.start_unverified, + ) { + (false, _, _) => StartPermission::Owner, + (true, true, false) => StartPermission::Authenticated, + (true, true, true) => StartPermission::Anonymous, + (true, false, _) => StartPermission::Owner, + } + } + + pub fn wallets_id(&self) -> Vec { + self.row + .nodes + .iter() + .filter_map(|n| { + (n.data.r#type == CommandType::Native && n.data.node_id == "wallet") + .then(|| { + n.data + .targets_form + .form_data + .get("wallet_id") + .and_then(|v| v.as_i64()) + }) + .flatten() + }) + .collect() + } + + pub fn interflows_id(&self) -> Vec { + self.row + .nodes + .iter() + .filter_map(|n| { + let is_interflow = n.data.r#type == CommandType::Native + && (n.data.node_id == interflow::INTERFLOW + || n.data.node_id == interflow_instructions::INTERFLOW_INSTRUCTIONS); + is_interflow + .then(|| interflow::get_interflow_id(&n.data).ok()) + .flatten() + }) + .collect() + } +} + +pub type DeploymentId = Uuid; + +#[derive(bon::Builder, Debug, Clone)] +pub struct FlowDeployment { + /// Owner of this deployment (and all flows belonging to it) + pub user_id: UserId, + /// Flow ID to start the set + pub entrypoint: FlowId, + /// Flow configs + pub flows: HashMap, + + /// Who can start the deployment + pub start_permission: StartPermission, + /// Wallets are stored separately + pub wallets_id: Vec, + + pub output_instructions: bool, + pub action_identity: Option, + pub action_config: Option, + pub fees: Vec<(Pubkey, u64)>, +} + +impl FlowDeployment { + fn new(entry: FlowRow) -> Self { + let flow = Flow::builder().row(entry).build(); + Self::builder() + .entrypoint(flow.row.id) + .start_permission(flow.start_permission()) + .wallets_id(flow.wallets_id()) + .user_id(flow.row.user_id) + .flows([(flow.row.id, flow)].into()) + .output_instructions(false) + .fees(Vec::new()) + .build() + } + + pub async fn from_entrypoint(flow_id: FlowId, get_flow_row: &mut S) -> Result + where + S: tower::Service, + { + let resp = get_flow_row + .ready() + .await? + .call(get_flow_row::Request { flow_id }) + .await?; + let mut dep = FlowDeployment::new(resp.row); + + let mut queue: Vec = dep + .flows + .values() + .map(|flow| flow.interflows_id()) + .flatten() + .collect(); + + while let Some(id) = queue.pop() { + if dep.flows.contains_key(&id) { + continue; + } + + let row = get_flow_row + .ready() + .await? + .call(get_flow_row::Request { flow_id: id }) + .await? + .row; + let flow = Flow::builder().row(row).build(); + queue.extend(flow.interflows_id()); + dep.flows.insert(id, flow); + } + + let mut wallets_id = dep + .flows + .values() + .flat_map(|f| f.wallets_id()) + .collect::>(); + wallets_id.sort_unstable(); + wallets_id.dedup(); + dep.wallets_id = wallets_id.clone(); + + Ok(dep) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct FlowStarter { + pub user_id: UserId, + pub pubkey: Pubkey, + pub authenticated: bool, +} + +/// Start a flow deployment by starting the entrypoint +#[derive(Debug)] +pub struct StartFlowDeploymentOptions { + pub inputs: ValueSet, + pub starter: FlowStarter, +} + +pub enum StartFlowOrigin { + Start {}, + Interflow { + flow_run_id: FlowRunId, + node_id: NodeId, + times: u32, + depth: u32, + }, +} + +#[derive(bon::Builder)] +pub struct FlowSet { + deployment: FlowDeployment, + context: FlowSetContext, +} + +pub mod get_flow_row { + use flow_lib::{config::client::FlowRow, utils::TowerClient, BoxError, FlowId}; + use thiserror::Error as ThisError; + + pub type Svc = TowerClient; + + pub struct Request { + pub flow_id: FlowId, + } + + impl actix::Message for Request { + type Result = Result; + } + + pub struct Response { + pub row: FlowRow, + } + + #[derive(ThisError, Debug)] + pub enum Error { + #[error("flow not found")] + NotFound, + #[error("unauthorized")] + Unauthorized, + #[error(transparent)] + Worker(tower::BoxError), + #[error(transparent)] + MailBox(#[from] actix::MailboxError), + #[error(transparent)] + Other(#[from] BoxError), + } +} + +pub mod make_signer { + use flow_lib::{context::signer, utils::TowerClient, BoxError}; + use thiserror::Error as ThisError; + + pub type Svc = TowerClient; + + pub struct Request { + pub wallets_id: Vec, + } + + impl actix::Message for Request { + type Result = Result; + } + + pub struct Response { + pub signer: signer::Svc, + } + + #[derive(ThisError, Debug)] + pub enum Error { + #[error(transparent)] + Worker(tower::BoxError), + #[error(transparent)] + MailBox(#[from] actix::MailboxError), + #[error(transparent)] + Other(#[from] BoxError), + } +} + +fn to_client_config(flow: Flow) -> ClientConfig { + ClientConfig { + user_id: flow.row.user_id, + id: flow.row.id, + nodes: flow.row.nodes, + edges: flow.row.edges, + environment: flow.row.environment, + sol_network: flow.row.current_network, + instructions_bundling: flow.row.instructions_bundling, + partial_config: None, + collect_instructions: false, + call_depth: 0, + origin: FlowRunOrigin::Start {}, + signers: JsonValue::Null, + interflow_instruction_info: Err("unimplemented".to_owned()), + } +} + +impl FlowSet { + pub async fn start( + self, + options: StartFlowDeploymentOptions, + ) -> Result<(FlowRunId, JoinHandle), new_flow_run::Error> { + let flow_id = self.deployment.entrypoint; + let flow = self.deployment.flows.get(&flow_id).unwrap().clone(); + let shared_with = if flow.row.user_id != options.starter.user_id { + [options.starter.user_id].into() + } else { + Vec::new() + }; + let registry = FlowRegistry::builder() + .flows(Arc::new( + self.deployment + .flows + .into_iter() + .map(|(k, v)| (k, to_client_config(v))) + .collect(), + )) + .flow_owner(User { + id: self.deployment.user_id, + }) + .started_by(User { + id: self.deployment.user_id, + }) + .shared_with(shared_with) + .signers_info(JsonValue::Null) + .endpoints(self.context.endpoints) + .depth(self.context.depth) + .signer(self.context.signer) + .token(self.context.get_jwt) + .new_flow_run(self.context.new_flow_run) + .get_previous_values(get_previous_values::unimplemented_svc()) + .rhai_permit(self.context.rhai_permit) + .rhai_tx(self.context.rhai_tx) + .maybe_parent_flow_execute(self.context.parent_flow_execute) + .maybe_rpc_server(self.context.rpc_server) + .build(); + registry + .start( + flow_id, + options.inputs, + StartFlowOptions { + partial_config: None, + collect_instructions: self.deployment.output_instructions, + action_identity: self.deployment.action_identity, + action_config: self.deployment.action_config, + fees: self.deployment.fees, + origin: FlowRunOrigin::Start {}, + solana_client: Some(SolanaClientConfig { + url: flow.row.current_network.url.clone(), + cluster: flow.row.current_network.cluster, + }), + parent_flow_execute: None, + }, + ) + .await + } +} + +#[derive(bon::Builder, Getters, Clone)] +pub struct FlowSetContext { + depth: u32, + endpoints: Endpoints, + + signer: signer::Svc, + get_jwt: get_jwt::Svc, + new_flow_run: new_flow_run::Svc, + parent_flow_execute: Option, + + #[builder(default = Arc::new(Semaphore::new(1)))] + rhai_permit: Arc, + #[builder(default)] + rhai_tx: Arc>>>, + + rpc_server: Option>, +} + +/* +pub struct FlowContext { + set_context: FlowSetContext, + flow_run_id: FlowRunId, + http: reqwest::Client, + solana_rpc: Arc, + parent_flow_execute: execute::Svc, +} + +pub struct Context { + flow: FlowContext, + execute: execute::Svc, + start_interflow: start_interflow::Svc, + node_id: NodeId, + times: u32, +} +*/ diff --git a/crates/flow/src/lib.rs b/crates/flow/src/lib.rs new file mode 100644 index 00000000..36033d55 --- /dev/null +++ b/crates/flow/src/lib.rs @@ -0,0 +1,10 @@ +pub mod command; +pub mod context; +pub mod error; +pub mod flow_graph; +pub mod flow_registry; +pub mod flow_run_events; +pub mod flow_set; + +pub use error::{BoxedError, Error, Result}; +pub use flow_graph::FlowGraph; diff --git a/crates/flow/test_files/2_foreach.json b/crates/flow/test_files/2_foreach.json new file mode 100644 index 00000000..04302102 --- /dev/null +++ b/crates/flow/test_files/2_foreach.json @@ -0,0 +1,446 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "2_foreach", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 80 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-01-11", + "parent_flow": null, + "viewport": { + "x": -224.4966791986351, + "y": 386.82938868493756, + "zoom": 1.032398535483242 + }, + "nodes": [ + { + "width": 300, + "height": 180, + "selected": false, + "id": "de17120d-2e55-4506-9385-df982f528c66", + "type": "native", + "position": { + "x": -135, + "y": -15 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "a0758dda-37a2-4f60-8ea0-6b06fbdca215", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "7bc9b872-fa36-4500-abf9-d723efe2aad5" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "[\n[\"0,0\", \"0,1\"],\n[\"1,0\", \"1,1\"]\n]", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "draggable": true, + "positionAbsolute": { + "x": -135, + "y": -15 + } + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "94677b38-9608-4988-83e7-dbff10b22387", + "type": "native", + "position": { + "x": 195, + "y": -15 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "75878cbe-427e-4dfc-9b61-040efce9a792", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "2b883db4-8181-43e1-a0e7-9a3d721cd9e6" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "d8b86826-33f2-4eb2-812d-c391d55d39fd" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": 195, + "y": -15 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "01fb551e-2db8-4dd0-891d-5e91b5f80169", + "type": "native", + "position": { + "x": 480, + "y": -15 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "05e5902b-ccd8-4551-b3b1-cccadb7e1f23", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "cf2bb599-4825-4cbf-a9fe-5ea962fbba22" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "408f9fb9-acc1-419c-99d2-42d600caf05f" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": 480, + "y": -15 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "9f66d3f6-ddec-4a8e-8fe9-3340a7333c02", + "type": "native", + "position": { + "x": 765, + "y": -15 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "4f6af3f2-79c1-4416-91a5-c606b0de6a8c", + "unique_node_id": "collect.0.1", + "node_id": "collect", + "version": "0.1", + "description": "Collect inputs into an array", + "name": "Collect", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "array", + "type": "free", + "defaultValue": null, + "tooltip": "", + "id": "665e73cb-5756-407c-851b-6b7a34470d60" + } + ], + "targets": [ + { + "name": "element", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "1ab694e9-adb2-452e-83d2-3cf8f7bc973b" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 303 + } + } + }, + "positionAbsolute": { + "x": 765, + "y": -15 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "887b8ae7-01c1-4e16-8f5e-8950e93355cf", + "type": "native", + "position": { + "x": 1050, + "y": -15 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "79b0afe9-4634-43d0-b306-60bb3ff616f9", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "output", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "e9cb3e8e-2069-47e9-80bb-ee53e2d8bd9a" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "output" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 1050, + "y": -15 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "de17120d-2e55-4506-9385-df982f528c66", + "sourceHandle": "7bc9b872-fa36-4500-abf9-d723efe2aad5", + "target": "94677b38-9608-4988-83e7-dbff10b22387", + "targetHandle": "d8b86826-33f2-4eb2-812d-c391d55d39fd", + "id": "reactflow__edge-de17120d-2e55-4506-9385-df982f528c667bc9b872-fa36-4500-abf9-d723efe2aad5-94677b38-9608-4988-83e7-dbff10b22387d8b86826-33f2-4eb2-812d-c391d55d39fd" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "94677b38-9608-4988-83e7-dbff10b22387", + "sourceHandle": "2b883db4-8181-43e1-a0e7-9a3d721cd9e6", + "target": "01fb551e-2db8-4dd0-891d-5e91b5f80169", + "targetHandle": "408f9fb9-acc1-419c-99d2-42d600caf05f", + "id": "reactflow__edge-94677b38-9608-4988-83e7-dbff10b223872b883db4-8181-43e1-a0e7-9a3d721cd9e6-01fb551e-2db8-4dd0-891d-5e91b5f80169408f9fb9-acc1-419c-99d2-42d600caf05f" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "01fb551e-2db8-4dd0-891d-5e91b5f80169", + "sourceHandle": "cf2bb599-4825-4cbf-a9fe-5ea962fbba22", + "target": "9f66d3f6-ddec-4a8e-8fe9-3340a7333c02", + "targetHandle": "1ab694e9-adb2-452e-83d2-3cf8f7bc973b", + "id": "reactflow__edge-01fb551e-2db8-4dd0-891d-5e91b5f80169cf2bb599-4825-4cbf-a9fe-5ea962fbba22-9f66d3f6-ddec-4a8e-8fe9-3340a7333c021ab694e9-adb2-452e-83d2-3cf8f7bc973b" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "9f66d3f6-ddec-4a8e-8fe9-3340a7333c02", + "sourceHandle": "665e73cb-5756-407c-851b-6b7a34470d60", + "target": "887b8ae7-01c1-4e16-8f5e-8950e93355cf", + "targetHandle": "e9cb3e8e-2069-47e9-80bb-ee53e2d8bd9a", + "id": "reactflow__edge-9f66d3f6-ddec-4a8e-8fe9-3340a7333c02665e73cb-5756-407c-851b-6b7a34470d60-887b8ae7-01c1-4e16-8f5e-8950e93355cfe9cb3e8e-2069-47e9-80bb-ee53e2d8bd9a" + } + ], + "uuid": "a9be2d77-4115-4da5-bf11-c2068bcbf960", + "network": "devnet", + "updated_at": "2023-01-11T14:41:08.824964", + "lastest_flow_run_id": "7da8abec-23a7-4616-b944-a1ec8f03d24b", + "environment": null, + "current_rpc": null, + "custom_rpc": null + }, + "bookmarks": [] +} diff --git a/crates/flow/test_files/deno_instructions/deno.json b/crates/flow/test_files/deno_instructions/deno.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/crates/flow/test_files/deno_instructions/deno.json @@ -0,0 +1 @@ +{} diff --git a/crates/flow/test_files/deno_instructions/deno.lock b/crates/flow/test_files/deno_instructions/deno.lock new file mode 100644 index 00000000..fa9f896c --- /dev/null +++ b/crates/flow/test_files/deno_instructions/deno.lock @@ -0,0 +1,323 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@space-operator/flow-lib": "jsr:@space-operator/flow-lib@0.8.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/encoding": "jsr:@std/encoding@0.221.0", + "jsr:@std/encoding@^0.221.0": "jsr:@std/encoding@0.221.0", + "jsr:@std/msgpack@0.221.0": "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js": "npm:@solana/web3.js@1.91.4", + "npm:@solana/web3.js@^1.91.4": "npm:@solana/web3.js@1.91.4" + }, + "jsr": { + "@space-operator/flow-lib@0.8.0": { + "integrity": "0dbf01bf13b6da46325ac7e37b26ffc5e838462be2904f1e83762f00ac2dc686", + "dependencies": [ + "jsr:@std/encoding@^0.221.0", + "jsr:@std/msgpack@0.221.0", + "npm:@solana/web3.js@^1.91.4" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/msgpack@0.221.0": { + "integrity": "78a99bca814808f08f49dd2b21a55185540a5ebba861d29d3ee63429157ad490", + "dependencies": [ + "jsr:@std/bytes@^0.221.0" + ] + } + }, + "npm": { + "@babel/runtime@7.24.4": { + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.4": { + "integrity": "sha512-zconqecIcBqEF6JiM4xYF865Xc4aas+iWK5qnu7nwKPq9ilRYcn+2GiwpYXqUqqBUe0XCO17w18KO0F8h+QATg==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.4", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/crates/flow/test_files/deno_instructions/node-definition.json b/crates/flow/test_files/deno_instructions/node-definition.json new file mode 100644 index 00000000..499991d1 --- /dev/null +++ b/crates/flow/test_files/deno_instructions/node-definition.json @@ -0,0 +1,97 @@ +{ + "type": "deno", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "duy_instructions_example", + "version": "0.1", + "display_name": "Deno instructions example", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {}, + "instruction_info": { + "before": [], + "signature": "signature", + "after": [] + } + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "from", + "type_bounds": [ + "pubkey" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "to", + "type_bounds": [ + "pubkey" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": [ + "f64" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} diff --git a/crates/flow/test_files/deno_instructions/transfer_sol.ts b/crates/flow/test_files/deno_instructions/transfer_sol.ts new file mode 100644 index 00000000..0b31dc35 --- /dev/null +++ b/crates/flow/test_files/deno_instructions/transfer_sol.ts @@ -0,0 +1,31 @@ +import * as lib from "jsr:@space-operator/flow-lib"; +import * as web3 from "npm:@solana/web3.js"; +import { Instructions } from "jsr:@space-operator/flow-lib/context"; + +export default class TransferSol implements lib.CommandTrait { + async run( + ctx: lib.Context, + params: Record + ): Promise> { + const fromPubkey = new web3.PublicKey(params.from); + + const result = await ctx.execute( + new Instructions( + fromPubkey, + [fromPubkey], + [ + web3.SystemProgram.transfer({ + fromPubkey, + toPubkey: new web3.PublicKey(params.to), + lamports: params.amount, + }), + ] + ), + {} + ); + + return { + signature: result.signature!, + }; + } +} diff --git a/crates/flow/test_files/deno_sig/deno.json b/crates/flow/test_files/deno_sig/deno.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/crates/flow/test_files/deno_sig/deno.json @@ -0,0 +1 @@ +{} diff --git a/crates/flow/test_files/deno_sig/deno.lock b/crates/flow/test_files/deno_sig/deno.lock new file mode 100644 index 00000000..fedf8088 --- /dev/null +++ b/crates/flow/test_files/deno_sig/deno.lock @@ -0,0 +1,391 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@oak/commons@0.7": "jsr:@oak/commons@0.7.0", + "jsr:@oak/oak@^14.2.0": "jsr:@oak/oak@14.2.0", + "jsr:@space-operator/deno-command-rpc@^0.4.0": "jsr:@space-operator/deno-command-rpc@0.4.0", + "jsr:@space-operator/flow-lib@^0.4.0": "jsr:@space-operator/flow-lib@0.4.0", + "jsr:@std/assert@0.218": "jsr:@std/assert@0.218.2", + "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", + "jsr:@std/bytes@0.218": "jsr:@std/bytes@0.218.2", + "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", + "jsr:@std/crypto@0.218": "jsr:@std/crypto@0.218.2", + "jsr:@std/encoding@0.218": "jsr:@std/encoding@0.218.2", + "jsr:@std/encoding@^0.218.2": "jsr:@std/encoding@0.218.2", + "jsr:@std/encoding@^0.220.1": "jsr:@std/encoding@0.220.1", + "jsr:@std/http@0.218": "jsr:@std/http@0.218.2", + "jsr:@std/io@0.218": "jsr:@std/io@0.218.2", + "jsr:@std/media-types@0.218": "jsr:@std/media-types@0.218.2", + "jsr:@std/path@0.218": "jsr:@std/path@0.218.2", + "npm:@solana/web3.js@^1.91.2": "npm:@solana/web3.js@1.91.2", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1" + }, + "jsr": { + "@oak/commons@0.7.0": { + "integrity": "4bd889b3dc9ddac1b602034d88c137f06de7078775961b51081beb5f175c120b" + }, + "@oak/oak@14.2.0": { + "integrity": "b683b089693004ac3bca80b52159b3e9ad214dc8246ff5dc61ba658da78bc166", + "dependencies": [ + "jsr:@oak/commons@0.7", + "jsr:@std/assert@0.218", + "jsr:@std/bytes@0.218", + "jsr:@std/crypto@0.218", + "jsr:@std/encoding@0.218", + "jsr:@std/http@0.218", + "jsr:@std/io@0.218", + "jsr:@std/media-types@0.218", + "jsr:@std/path@0.218", + "npm:path-to-regexp@6.2.1" + ] + }, + "@space-operator/deno-command-rpc@0.4.0": { + "integrity": "add0b32e00c5d7f11ab371ffb10d768788c728c0d345ac5c1221e4614643bea0", + "dependencies": [ + "jsr:@oak/oak@^14.2.0" + ] + }, + "@space-operator/flow-lib@0.4.0": { + "integrity": "d7f778c6f7544b6c9f06f0f4d52e00fae7e9cd9cb796abfc7b7de8b111d1645d", + "dependencies": [ + "jsr:@std/encoding@^0.220.1", + "npm:@solana/web3.js@^1.91.2" + ] + }, + "@std/assert@0.218.2": { + "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" + }, + "@std/bytes@0.218.2": { + "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" + }, + "@std/crypto@0.218.2": { + "integrity": "8c5031a3a1c3ac3bed3c0d4bed2fe7e7faedcb673bbfa0edd10570c8452f5cd2", + "dependencies": [ + "jsr:@std/assert@^0.218.2", + "jsr:@std/encoding@^0.218.2" + ] + }, + "@std/encoding@0.218.2": { + "integrity": "da55a763c29bf0dbf06fd286430b358266eb99c28789d89fe9a3e28edecb8d8e" + }, + "@std/encoding@0.220.1": { + "integrity": "8dc38dd72e36cd68857a5837e24eb09a64bb296b96c295239c75eec17d45d23f" + }, + "@std/http@0.218.2": { + "integrity": "54223b62702e665b9dab6373ea2e51235e093ef47228d21cfa0469ee5ac75c9b", + "dependencies": [ + "jsr:@std/assert@^0.218.2", + "jsr:@std/encoding@^0.218.2" + ] + }, + "@std/io@0.218.2": { + "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", + "dependencies": [ + "jsr:@std/bytes@^0.218.2" + ] + }, + "@std/media-types@0.218.2": { + "integrity": "1ed3bd2a05e44bad3fc2bab1767d0ce7f2fd68baee62a980751ce51633acb788" + }, + "@std/path@0.218.2": { + "integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662", + "dependencies": [ + "jsr:@std/assert@^0.218.2" + ] + } + }, + "npm": { + "@babel/runtime@7.24.1": { + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "@noble/hashes@1.4.0" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "buffer@6.0.3" + } + }, + "@solana/web3.js@1.91.2": { + "integrity": "sha512-WXPl5VXtfNKWM2RkGj7mvX6dKcZURDKe1lWBFAt/RqDBI9Rjr9hr7Y+U+yz2+TyViMmoinfJVlkS4gk2FPDG/g==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.1", + "@noble/curves": "@noble/curves@1.4.0", + "@noble/hashes": "@noble/hashes@1.4.0", + "@solana/buffer-layout": "@solana/buffer-layout@4.0.1", + "agentkeepalive": "agentkeepalive@4.5.0", + "bigint-buffer": "bigint-buffer@1.1.5", + "bn.js": "bn.js@5.2.1", + "borsh": "borsh@0.7.0", + "bs58": "bs58@4.0.1", + "buffer": "buffer@6.0.3", + "fast-stable-stringify": "fast-stable-stringify@1.0.0", + "jayson": "jayson@4.1.0_ws@7.5.9", + "node-fetch": "node-fetch@2.7.0", + "rpc-websockets": "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10", + "superstruct": "superstruct@0.14.2" + } + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dependencies": {} + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "@types/node@18.16.19" + } + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "jsonparse@1.3.1", + "through": "through@2.3.8" + } + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "humanize-ms@1.2.1" + } + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "safe-buffer@5.2.1" + } + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dependencies": {} + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": { + "bindings": "bindings@1.5.0" + } + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "file-uri-to-path@1.0.0" + } + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dependencies": {} + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "bn.js@5.2.1", + "bs58": "bs58@4.0.1", + "text-encoding-utf-8": "text-encoding-utf-8@1.0.2" + } + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "base-x@3.0.9" + } + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": { + "base64-js": "base64-js@1.5.1", + "ieee754": "ieee754@1.2.1" + } + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dependencies": {} + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "dependencies": {} + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dependencies": {} + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "es6-promise@4.2.8" + } + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dependencies": {} + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "dependencies": {} + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "dependencies": {} + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dependencies": {} + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "ms@2.1.3" + } + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dependencies": {} + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": { + "ws": "ws@7.5.9" + } + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "@types/connect@3.4.38", + "@types/node": "@types/node@12.20.55", + "@types/ws": "@types/ws@7.4.7", + "JSONStream": "JSONStream@1.3.5", + "commander": "commander@2.20.3", + "delay": "delay@5.0.0", + "es6-promisify": "es6-promisify@5.0.0", + "eyes": "eyes@0.1.8", + "isomorphic-ws": "isomorphic-ws@4.0.1_ws@7.5.9", + "json-stringify-safe": "json-stringify-safe@5.0.1", + "uuid": "uuid@8.3.2", + "ws": "ws@7.5.9" + } + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dependencies": {} + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dependencies": {} + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dependencies": {} + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "whatwg-url@5.0.0" + } + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dependencies": {} + }, + "path-to-regexp@6.2.1": { + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dependencies": {} + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "rpc-websockets@7.9.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.1", + "bufferutil": "bufferutil@4.0.8", + "eventemitter3": "eventemitter3@4.0.7", + "utf-8-validate": "utf-8-validate@5.0.10", + "uuid": "uuid@8.3.2", + "ws": "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + } + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dependencies": {} + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "dependencies": {} + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==", + "dependencies": {} + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dependencies": {} + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dependencies": {} + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": { + "node-gyp-build": "node-gyp-build@4.8.0" + } + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dependencies": {} + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "tr46@0.0.3", + "webidl-conversions": "webidl-conversions@3.0.1" + } + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dependencies": {} + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": { + "bufferutil": "bufferutil@4.0.8", + "utf-8-validate": "utf-8-validate@5.0.10" + } + } + } + }, + "remote": {} +} diff --git a/crates/flow/test_files/deno_sig/node-definition.json b/crates/flow/test_files/deno_sig/node-definition.json new file mode 100644 index 00000000..1a1b6089 --- /dev/null +++ b/crates/flow/test_files/deno_sig/node-definition.json @@ -0,0 +1,92 @@ +{ + "type": "deno", + "data": { + "node_definition_version": "0.1", + "unique_id": "", + "node_id": "duy_transfer_sol", + "version": "0.1", + "display_name": "Transfer SOL with Deno", + "description": "", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "" + } + ], + "targets": [ + { + "name": "from", + "type_bounds": [ + "pubkey" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "to", + "type_bounds": [ + "pubkey" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + }, + { + "name": "amount", + "type_bounds": [ + "f64" + ], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false + } + ], + "targets_form.json_schema": {}, + "targets_form.ui_schema": {} +} \ No newline at end of file diff --git a/crates/flow/test_files/deno_sig/transfer_sol.ts b/crates/flow/test_files/deno_sig/transfer_sol.ts new file mode 100644 index 00000000..56ef687e --- /dev/null +++ b/crates/flow/test_files/deno_sig/transfer_sol.ts @@ -0,0 +1,41 @@ +import * as lib from "jsr:@space-operator/flow-lib"; +import * as web3 from "npm:@solana/web3.js"; +import { encodeBase58 } from "jsr:@std/encoding@^0.220.1/base58"; + +export default class TransferSol implements lib.CommandTrait { + async run( + ctx: lib.Context, + params: Record + ): Promise> { + const fromPubkey = new web3.PublicKey(params.from); + + // build the message + const message = new web3.TransactionMessage({ + payerKey: fromPubkey, + recentBlockhash: (await ctx.solana.getLatestBlockhash()).blockhash, + instructions: [ + web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000 }), + web3.SystemProgram.transfer({ + fromPubkey, + toPubkey: new web3.PublicKey(params.to), + lamports: params.amount, + }), + ], + }).compileToLegacyMessage(); + + // request signature from user + const { signature, new_message } = await ctx.requestSignature( + fromPubkey, + message.serialize() + ); + + // submit + const tx = web3.Transaction.populate( + new_message ? web3.Message.from(new_message) : message, + [encodeBase58(signature)] + ); + return { + signature: await ctx.solana.sendRawTransaction(tx.serialize()), + }; + } +} diff --git a/crates/flow/test_files/nft.json b/crates/flow/test_files/nft.json new file mode 100644 index 00000000..f7e3fbc9 --- /dev/null +++ b/crates/flow/test_files/nft.json @@ -0,0 +1,1967 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "Bundling NFT", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 80 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Create an NFT", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-03-24", + "parent_flow": null, + "viewport": { + "x": 424.49235618247883, + "y": 447.7630881588299, + "zoom": 0.3789291416276035 + }, + "nodes": [ + { + "width": 300, + "height": 155, + "selected": false, + "id": "48e9220e-0190-4978-8a0a-98ee765c9727", + "type": "native", + "position": { + "x": -1425, + "y": -75 + }, + "style": { + "height": 155, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "26994e10-f687-4e19-9f39-3477877372d8", + "unique_node_id": "wallet.0.1", + "node_id": "wallet", + "version": "0.1", + "description": "", + "name": "Wallet", + "backgroundColorDark": "#000000", + "backgroundColor": "#baf2b1", + "sources": [ + { + "name": "pubkey", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "2b24b9b8-5021-490c-8528-0b9ae7a694f2" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": "", + "tooltip": "", + "id": "a5355d4b-c499-4a73-9f0a-8fefa8112d83" + } + ], + "targets": [ + { + "name": "name", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "66b476c8-4698-40c5-80e4-1a6bf1094b42" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "public_key": "HJbqSuV94woJfyxFNnJyfQdACvvJYaNWsW1x6wmJ8kiq", + "wallet_type": "ADAPTER" + }, + "extra": { + "supabase_id": 152 + } + } + }, + "positionAbsolute": { + "x": -1425, + "y": -75 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "265dd31e-e612-4427-ba04-7dbe844e7715", + "type": "native", + "position": { + "x": -1425, + "y": 90 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "36b974f1-55b7-4166-99ca-b4b32e57200d", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "decimal", + "defaultValue": "", + "tooltip": "", + "id": "1bc18588-c866-4c15-a676-f419707c6481" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "D": "0" + }, + "type": "Decimal" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1425, + "y": 90 + }, + "dragging": false, + "draggable": true + }, + { + "width": 250, + "height": 200, + "selected": false, + "id": "dfbeff0c-9272-4986-9a41-4c2148be7a81", + "type": "native", + "position": { + "x": -1380, + "y": 285 + }, + "style": { + "height": 200, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "c9940f16-4e8b-4e6c-9b8e-fe205aed092f", + "unique_node_id": "generate_keypair.0.1", + "node_id": "generate_keypair", + "version": "0.1", + "description": "Generate or load a keypair and it's pubkey.\n\nWill generate a random keypair every run if no inputs are provided. This is useful for testing purpose.", + "name": "Generate Keypair", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "pubkey", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "e1d7dcf5-93c8-4666-863c-64ab5559c0e3" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": null, + "tooltip": "", + "id": "06d0b4d1-3a3c-4074-8f26-b97a35ece188" + } + ], + "targets": [ + { + "name": "seed", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "12 word BIP39 mnemonic seed phrase", + "passthrough": false, + "id": "b2925e9a-e4df-49d0-850c-bf69f400dd18" + }, + { + "name": "private_key", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "Load using a base 58 string, ignores seed/passphrase", + "passthrough": false, + "id": "87886882-af0f-41aa-b5b7-1ae08cc61382" + }, + { + "name": "passphrase", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "d9f51c3c-a798-4710-97ff-f23b921d9853" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 110 + } + } + }, + "positionAbsolute": { + "x": -1380, + "y": 285 + }, + "dragging": false + }, + { + "width": 265, + "height": 400, + "selected": false, + "id": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "type": "native", + "position": { + "x": -960, + "y": -90 + }, + "style": { + "height": 400, + "width": 265, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "c4ba6ee9-44a9-4357-814c-8b233bc05cc3", + "unique_node_id": "transfer_token.0.1", + "node_id": "create_mint_account", + "version": "0.1", + "description": "", + "name": "Create Mint Account", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "id": "066a0c4f-77a1-493c-9d7f-a60542ad23a7" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Who pays for account rent and transaction fees", + "passthrough": true, + "id": "ec4c83aa-0ff6-47f9-8009-9808118e0851" + }, + { + "name": "decimals", + "type_bounds": ["u8"], + "required": true, + "defaultValue": null, + "tooltip": "NFTs should have decimal = 0\nUS dollars have 2 decimals\nFrom Metaplex documentation:\n'If the token has a master edition it is a NonFungible. If the token has no master edition(ensuring its supply can be > 1) and decimals of 0 it is a FungibleAsset. If the token has no master edition(ensuring its supply can be > 1) and decimals of > 0 it is a Fungible. If the token is a limited edition of a MasterEditon it is a NonFungibleEdition.'", + "passthrough": false, + "id": "42a42512-aed3-4590-9273-da1844ebb74e" + }, + { + "name": "mint_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "Mint authority - who can mint more tokens", + "passthrough": true, + "id": "cf909643-3dd8-45d0-b6c3-90e29ee2bb0e" + }, + { + "name": "freeze_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "59360957-e2a6-418c-833f-88cdd64b249b" + }, + { + "name": "mint_account", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "acc313e1-f602-4fbe-815e-29bfae9e0992" + }, + { + "name": "memo", + "type_bounds": ["string"], + "required": false, + "defaultValue": "", + "tooltip": "Additional notes", + "passthrough": false, + "id": "795421e4-b1aa-42a8-b400-8ff91be2c449" + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "a8b14cfb-3d9e-47ec-8174-2200794d98c7" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 107 + } + } + }, + "positionAbsolute": { + "x": -960, + "y": -90 + }, + "dragging": false + }, + { + "width": 320, + "height": 300, + "selected": false, + "id": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "type": "native", + "position": { + "x": -570, + "y": -180 + }, + "style": { + "height": 300, + "width": 320, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "bd65823a-d407-4fc0-a017-f6781cd4c31f", + "unique_node_id": "associated_token_account.0.1", + "node_id": "associated_token_account", + "version": "0.1", + "description": "", + "name": "Associated Token Account", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "associated_token_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "782e3ec6-a953-4526-b870-5997318a496e" + }, + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "id": "3bf3b99d-0a07-4417-a117-589e92742acf" + } + ], + "targets": [ + { + "name": "owner", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "333a3e3e-a6aa-4a75-8e2d-1876217aacca" + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "e1997b9a-6808-4eca-b049-759da4ae946c" + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "634900ee-4a76-4bc8-818e-ee14c1cc74f7" + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "b40e5b52-daad-428d-bdf7-99f1d361ee92" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 386 + } + } + }, + "positionAbsolute": { + "x": -570, + "y": -180 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "93eff255-3b27-450c-9d4f-f0a9af3c37a3", + "type": "native", + "position": { + "x": -555, + "y": 195 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "c69b3301-ba3b-4d44-adce-1bc921b3f81b", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "decimal", + "defaultValue": "", + "tooltip": "", + "id": "ef9affce-2589-4d01-9b42-a8e1b6081de5" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "D": "1" + }, + "type": "Decimal" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -555, + "y": 195 + }, + "dragging": false, + "draggable": true + }, + { + "width": 255, + "height": 400, + "selected": false, + "id": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "type": "native", + "position": { + "x": -30, + "y": -180 + }, + "style": { + "height": 400, + "width": 255, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "77753c6e-8cfc-4b56-ac36-f1f31aeacb9d", + "unique_node_id": "mint_token.0.1", + "node_id": "mint_token", + "version": "0.1", + "description": "Identifies the token, determines who can mint, and how many", + "name": "Mint Token", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "id": "7699caf5-c436-4d6b-b879-29fcc18a5f90" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "adb834b2-a746-4f2b-9c5e-7b3adf9afda6" + }, + { + "name": "mint_authority", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "fd8d4517-173f-4c94-bcf9-cbf338da672f" + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "f516f6f8-1fa6-4814-9ddc-2c85ff85bce6" + }, + { + "name": "recipient", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "4e589bf1-ef57-4c49-bedf-93287c4fdbce" + }, + { + "name": "amount", + "type_bounds": ["f64"], + "required": true, + "defaultValue": null, + "tooltip": "NFTs should have amount = 1", + "passthrough": false, + "id": "6477ff46-be51-44a6-a494-aa556f4d2188" + }, + { + "name": "decimals", + "type_bounds": ["u8"], + "required": false, + "defaultValue": null, + "tooltip": "NFTs should have decimals = 0", + "passthrough": false, + "id": "fb6179ba-2d0e-4dcf-b733-8c5bbd0fabc7" + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "e86a5a01-b5f8-456d-a633-ac7d4ed45fd6" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 113 + } + } + }, + "positionAbsolute": { + "x": -30, + "y": -180 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "4f4a55db-0bde-4c6f-a269-2d7d825db34c", + "type": "native", + "position": { + "x": -540, + "y": 675 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "ac01b201-85ba-4cfa-afb5-f2e40c5d6338", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "pubkey", + "defaultValue": "", + "tooltip": "", + "id": "db4486e2-7973-4f6f-963d-36ba44913622" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "B3": "thisdoesntmatteriiiiiiiiiiiiiiiiiiiiiiiiiii" + }, + "type": "Pubkey" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "positionAbsolute": { + "x": -540, + "y": 675 + }, + "draggable": true + }, + { + "width": 300, + "height": 120, + "selected": false, + "id": "52641dce-61c8-4ac4-b842-de3d68e115a2", + "type": "native", + "position": { + "x": -540, + "y": 855 + }, + "style": { + "height": 120, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "630b9c21-ea5f-453f-9eff-7125070c7797", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "bool", + "defaultValue": "", + "tooltip": "", + "id": "0ed9dbcc-5d76-46f7-8330-f8112549079c" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "B": true + }, + "type": "BoolTrue" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -540, + "y": 855 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "50c873d4-508c-4d66-9603-5878fc4a028c", + "type": "native", + "position": { + "x": -540, + "y": 975 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "b2ba3cb4-3051-4836-9116-1d94b02d3c84", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "1de4f7d3-5f86-43bf-b552-a52f0d3885a7" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "{\n \"name\": \"SO #11111\",\n \"symbol\": \"SPOP\",\n \"description\": \"Space Operator is a dynamic PFP collection\",\n \"seller_fee_basis_points\": 250,\n \"image\": \"https://arweave.net/vb1tD7tfAyrhZceA1MOYvvyqzZWgzHGDVZF37yDNH1Q\",\n \"attributes\": [\n {\n \"trait_type\": \"Season\",\n \"value\": \"Fall\"\n },\n {\n \"trait_type\": \"Light Color\",\n \"value\": \"Orange\"\n }\n ],\n \"properties\": {\n \"files\": [\n {\n \"uri\": \"https://arweave.net/vb1tD7tfAyrhZceA1MOYvvyqzZWgzHGDVZF37yDNH1Q\",\n \"type\": \"image/jpeg\"\n }\n ],\n \"category\": null\n }\n}", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "draggable": true, + "positionAbsolute": { + "x": -540, + "y": 975 + } + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "bced4640-b481-4306-a416-cd64cbd60d4e", + "type": "native", + "position": { + "x": -540, + "y": 1320 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "a59baaab-9a05-4cac-acd6-ce61d0972468", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "2280b59d-4232-479f-8330-b5558de154e2" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "S": "https://arweave.net/3FxpIIbpySnfTTXIrpojhF2KHHjevI8Mrt3pACmEbSY" + }, + "type": "String" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -540, + "y": 1320 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "dbdf31eb-1cfd-4cee-a220-d017cf4eab96", + "type": "native", + "position": { + "x": -540, + "y": 1545 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "cee37c4f-796e-4b77-aedf-915a09609810", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "0a64a373-ea8c-4e57-89ff-174fe26dcefa" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "{\n\"use_method\":\"Burn\", \"remaining\":500,\n\"total\":500\n}", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -540, + "y": 1545 + }, + "dragging": false, + "draggable": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "40650e03-3613-4fc4-8168-9e00cb03cf7b", + "type": "native", + "position": { + "x": -540, + "y": 1815 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "006737e8-df76-4e85-b2d0-997e9692bbcc", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "8eb7708d-386e-4b5d-8d45-17b22d3d528e" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "[{\n\"address\":\"DpfvhHU7z1CK8eP5xbEz8c4WBNHUfqUVtAE7opP2kJBc\",\"share\":100\n}]", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -540, + "y": 1815 + }, + "dragging": false, + "draggable": true + }, + { + "width": 345, + "height": 700, + "selected": false, + "id": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "type": "native", + "position": { + "x": 510, + "y": -240 + }, + "style": { + "height": 700, + "width": 345, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "3ce9b6bf-9c06-4e8d-a1a4-7d942d945a72", + "unique_node_id": "create_metadata_account.0.1", + "node_id": "create_metadata_account", + "version": "0.1", + "description": "", + "name": "Create Metadata Account", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "id": "cb34e27e-c348-4630-ab8e-4213ee09fb81" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "8922ffd8-3694-4fdd-a98d-d74a72680825" + } + ], + "targets": [ + { + "name": "proxy_as_update_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "Whether a Proxy Authority is signing the Update Authority", + "passthrough": true, + "id": "189507f9-7bc3-405f-9051-f9cc369c328e" + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "Who can update the on-chain metadata", + "passthrough": false, + "id": "177ebfc7-bcf6-475b-a21a-6abcf1610856" + }, + { + "name": "is_mutable", + "type_bounds": ["bool"], + "required": true, + "defaultValue": null, + "tooltip": "Whether Metadata Account can be updated", + "passthrough": true, + "id": "33fb83aa-f6ed-468d-b28b-2c28a146d203" + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "Token Mint Account", + "passthrough": false, + "id": "aaa65d26-79c5-4c83-bf59-ab5c9fc3db99" + }, + { + "name": "mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "e626ccea-0fce-4c02-8f55-1ba71b305748" + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "c30d0382-c2bb-4752-b7d9-adf499109272" + }, + { + "name": "metadata", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "1358a2fe-3f6c-4bf0-be6c-aa5d0d4a8f10" + }, + { + "name": "metadata_uri", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "4a515a39-1a39-4379-9027-84c49ceb08b9" + }, + { + "name": "uses", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "How many and which type of uses each NFT will have.\nUses:\nBurn is a single-time use and is burned after use.\nSingle is a single-time use and does not burn the token.\nMultiple allows up to the specified number of uses", + "passthrough": false, + "id": "e2095192-6e8e-4669-a520-d20c1720b0aa" + }, + { + "name": "collection_mint_account", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "9826ceec-5556-4d52-81db-495aa492c3ae" + }, + { + "name": "collection_details", + "type_bounds": ["u64"], + "required": false, + "defaultValue": null, + "tooltip": "Only applies to Collection NFTs and is automatically set. To facility migration, set the collection size manually.", + "passthrough": false, + "id": "1683321c-02e9-46db-876d-f0a5e1c69cd1" + }, + { + "name": "creators", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "Creators and their share of royalties. Limited to 5 creators", + "passthrough": false, + "id": "756bd81f-5cf8-4a93-96ca-cba48f5961dd" + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "06049348-e56f-45f6-b558-13d9dc68499d" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 124 + } + } + }, + "positionAbsolute": { + "x": 510, + "y": -240 + }, + "dragging": false + }, + { + "width": 375, + "height": 400, + "selected": false, + "id": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "type": "native", + "position": { + "x": 1005, + "y": -195 + }, + "style": { + "height": 400, + "width": 375, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "085dc469-04d5-4619-9001-8b7ffce892e6", + "unique_node_id": "create_master_edition.0.1", + "node_id": "create_master_edition", + "version": "0.1", + "description": "", + "name": "Create Master Edition", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "signature", + "type": "signature", + "defaultValue": null, + "tooltip": "", + "id": "627678a0-5fa5-47e8-a392-46207b552241" + }, + { + "name": "metadata_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "56eecb95-16c2-49c1-b61c-8cccd8069146" + }, + { + "name": "master_edition_account", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "a05a3810-00d5-4d23-9ac5-c5b6c581722f" + } + ], + "targets": [ + { + "name": "proxy_as_update_authority", + "type_bounds": ["pubkey"], + "required": false, + "defaultValue": null, + "tooltip": "Whether a Proxy Authority is signing the Update Authority", + "passthrough": true, + "id": "71b4b44f-df44-4269-a4bd-eebae8ec1828" + }, + { + "name": "update_authority", + "type_bounds": ["keypair"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "ae811dfc-8a25-44e8-b272-a5f413dadbca" + }, + { + "name": "mint_account", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "688d2ef3-3579-418d-a9db-e29b9a10ab58" + }, + { + "name": "mint_authority", + "type_bounds": ["pubkey"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "b2040c56-449d-4baf-8851-7bbe4d398031" + }, + { + "name": "fee_payer", + "type_bounds": ["keypair"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": true, + "id": "bc59de87-d186-4ce5-a84a-9c6b312809a3" + }, + { + "name": "max_supply", + "type_bounds": ["u64"], + "required": true, + "defaultValue": null, + "tooltip": "How many copies you can print. Leave empty for unlimited\n1/1 NFTs should have supply 0", + "passthrough": false, + "id": "2ab51b92-7cd7-4318-a989-66deb379833c" + }, + { + "name": "submit", + "type_bounds": ["bool"], + "required": false, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "dd84bc4a-e337-42e0-ba0d-ef72527c3d04" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 123 + } + } + }, + "positionAbsolute": { + "x": 1005, + "y": -195 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "4752f3b0-63e7-4395-bcda-152a5f6ae989", + "type": "native", + "position": { + "x": 585, + "y": 525 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "5b131953-b6ae-4874-bf86-abdcf04c9a58", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "decimal", + "defaultValue": "", + "tooltip": "", + "id": "b44b8f0a-fc2d-4cce-96ec-8a299a9f7062" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "D": "0" + }, + "type": "Decimal" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "positionAbsolute": { + "x": 585, + "y": 525 + } + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "e2f47397-67ed-42f4-8e8d-312a80875e80", + "type": "native", + "position": { + "x": 1530, + "y": -150 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "fa9e141a-dc1f-4748-804c-4c29465fbccf", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "c8af03a3-36ae-4480-aac3-744a4e64dac3" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "pubkey" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 1530, + "y": -150 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "e365c49a-eb07-4619-8794-ce76195b69bd", + "type": "native", + "position": { + "x": 1530, + "y": 60 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "fa3cdbba-2e3a-45ef-941f-eb9776d1b033", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "34b10d45-56e1-41c6-bc8e-d03fda5be5ea" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "master_edition" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 1530, + "y": 60 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "ef8d8a40-c1ad-4b09-97b6-8a3b3588a09a", + "type": "native", + "position": { + "x": 1530, + "y": -45 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "0d12a910-8ee4-40d6-a06f-9f3c1629a10f", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "5f4072b3-33a9-4362-a567-23defb402dca" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "metadata_account" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 1530, + "y": -45 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "265dd31e-e612-4427-ba04-7dbe844e7715", + "sourceHandle": "1bc18588-c866-4c15-a676-f419707c6481", + "target": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "targetHandle": "42a42512-aed3-4590-9273-da1844ebb74e", + "id": "reactflow__edge-265dd31e-e612-4427-ba04-7dbe844e77151bc18588-c866-4c15-a676-f419707c6481-86674f72-cb64-4309-b22f-f4e6d415fa8e42a42512-aed3-4590-9273-da1844ebb74e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "dfbeff0c-9272-4986-9a41-4c2148be7a81", + "sourceHandle": "06d0b4d1-3a3c-4074-8f26-b97a35ece188", + "target": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "targetHandle": "acc313e1-f602-4fbe-815e-29bfae9e0992", + "id": "reactflow__edge-dfbeff0c-9272-4986-9a41-4c2148be7a8106d0b4d1-3a3c-4074-8f26-b97a35ece188-86674f72-cb64-4309-b22f-f4e6d415fa8eacc313e1-f602-4fbe-815e-29bfae9e0992" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "48e9220e-0190-4978-8a0a-98ee765c9727", + "sourceHandle": "a5355d4b-c499-4a73-9f0a-8fefa8112d83", + "target": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "targetHandle": "ec4c83aa-0ff6-47f9-8009-9808118e0851", + "id": "reactflow__edge-48e9220e-0190-4978-8a0a-98ee765c9727a5355d4b-c499-4a73-9f0a-8fefa8112d83-86674f72-cb64-4309-b22f-f4e6d415fa8eec4c83aa-0ff6-47f9-8009-9808118e0851" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "48e9220e-0190-4978-8a0a-98ee765c9727", + "sourceHandle": "a5355d4b-c499-4a73-9f0a-8fefa8112d83", + "target": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "targetHandle": "cf909643-3dd8-45d0-b6c3-90e29ee2bb0e", + "id": "reactflow__edge-48e9220e-0190-4978-8a0a-98ee765c9727a5355d4b-c499-4a73-9f0a-8fefa8112d83-86674f72-cb64-4309-b22f-f4e6d415fa8ecf909643-3dd8-45d0-b6c3-90e29ee2bb0e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "48e9220e-0190-4978-8a0a-98ee765c9727", + "sourceHandle": "a5355d4b-c499-4a73-9f0a-8fefa8112d83", + "target": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "targetHandle": "59360957-e2a6-418c-833f-88cdd64b249b", + "id": "reactflow__edge-48e9220e-0190-4978-8a0a-98ee765c9727a5355d4b-c499-4a73-9f0a-8fefa8112d83-86674f72-cb64-4309-b22f-f4e6d415fa8e59360957-e2a6-418c-833f-88cdd64b249b" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "sourceHandle": "passthrough-ec4c83aa-0ff6-47f9-8009-9808118e0851", + "target": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "targetHandle": "333a3e3e-a6aa-4a75-8e2d-1876217aacca", + "id": "reactflow__edge-86674f72-cb64-4309-b22f-f4e6d415fa8epassthrough-ec4c83aa-0ff6-47f9-8009-9808118e0851-6095bf21-70c9-49de-8a03-d8d54b533ca6333a3e3e-a6aa-4a75-8e2d-1876217aacca" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "sourceHandle": "passthrough-ec4c83aa-0ff6-47f9-8009-9808118e0851", + "target": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "targetHandle": "e1997b9a-6808-4eca-b049-759da4ae946c", + "id": "reactflow__edge-86674f72-cb64-4309-b22f-f4e6d415fa8epassthrough-ec4c83aa-0ff6-47f9-8009-9808118e0851-6095bf21-70c9-49de-8a03-d8d54b533ca6e1997b9a-6808-4eca-b049-759da4ae946c" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "sourceHandle": "passthrough-e1997b9a-6808-4eca-b049-759da4ae946c", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "adb834b2-a746-4f2b-9c5e-7b3adf9afda6", + "id": "reactflow__edge-6095bf21-70c9-49de-8a03-d8d54b533ca6passthrough-e1997b9a-6808-4eca-b049-759da4ae946c-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcadb834b2-a746-4f2b-9c5e-7b3adf9afda6" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "sourceHandle": "782e3ec6-a953-4526-b870-5997318a496e", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "4e589bf1-ef57-4c49-bedf-93287c4fdbce", + "id": "reactflow__edge-6095bf21-70c9-49de-8a03-d8d54b533ca6782e3ec6-a953-4526-b870-5997318a496e-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc4e589bf1-ef57-4c49-bedf-93287c4fdbce", + "selected": false + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "93eff255-3b27-450c-9d4f-f0a9af3c37a3", + "sourceHandle": "ef9affce-2589-4d01-9b42-a8e1b6081de5", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "6477ff46-be51-44a6-a494-aa556f4d2188", + "id": "reactflow__edge-93eff255-3b27-450c-9d4f-f0a9af3c37a3ef9affce-2589-4d01-9b42-a8e1b6081de5-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc6477ff46-be51-44a6-a494-aa556f4d2188" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "265dd31e-e612-4427-ba04-7dbe844e7715", + "sourceHandle": "1bc18588-c866-4c15-a676-f419707c6481", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "fb6179ba-2d0e-4dcf-b733-8c5bbd0fabc7", + "id": "reactflow__edge-265dd31e-e612-4427-ba04-7dbe844e77151bc18588-c866-4c15-a676-f419707c6481-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcfb6179ba-2d0e-4dcf-b733-8c5bbd0fabc7" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "52641dce-61c8-4ac4-b842-de3d68e115a2", + "sourceHandle": "0ed9dbcc-5d76-46f7-8330-f8112549079c", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "33fb83aa-f6ed-468d-b28b-2c28a146d203", + "id": "reactflow__edge-52641dce-61c8-4ac4-b842-de3d68e115a20ed9dbcc-5d76-46f7-8330-f8112549079c-71589427-479e-49d1-aa99-a1c3eb2abbe033fb83aa-f6ed-468d-b28b-2c28a146d203" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "50c873d4-508c-4d66-9603-5878fc4a028c", + "sourceHandle": "1de4f7d3-5f86-43bf-b552-a52f0d3885a7", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "1358a2fe-3f6c-4bf0-be6c-aa5d0d4a8f10", + "id": "reactflow__edge-50c873d4-508c-4d66-9603-5878fc4a028c1de4f7d3-5f86-43bf-b552-a52f0d3885a7-71589427-479e-49d1-aa99-a1c3eb2abbe01358a2fe-3f6c-4bf0-be6c-aa5d0d4a8f10" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "bced4640-b481-4306-a416-cd64cbd60d4e", + "sourceHandle": "2280b59d-4232-479f-8330-b5558de154e2", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "4a515a39-1a39-4379-9027-84c49ceb08b9", + "id": "reactflow__edge-bced4640-b481-4306-a416-cd64cbd60d4e2280b59d-4232-479f-8330-b5558de154e2-71589427-479e-49d1-aa99-a1c3eb2abbe04a515a39-1a39-4379-9027-84c49ceb08b9" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "dbdf31eb-1cfd-4cee-a220-d017cf4eab96", + "sourceHandle": "0a64a373-ea8c-4e57-89ff-174fe26dcefa", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "e2095192-6e8e-4669-a520-d20c1720b0aa", + "id": "reactflow__edge-dbdf31eb-1cfd-4cee-a220-d017cf4eab960a64a373-ea8c-4e57-89ff-174fe26dcefa-71589427-479e-49d1-aa99-a1c3eb2abbe0e2095192-6e8e-4669-a520-d20c1720b0aa" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "40650e03-3613-4fc4-8168-9e00cb03cf7b", + "sourceHandle": "8eb7708d-386e-4b5d-8d45-17b22d3d528e", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "756bd81f-5cf8-4a93-96ca-cba48f5961dd", + "id": "reactflow__edge-40650e03-3613-4fc4-8168-9e00cb03cf7b8eb7708d-386e-4b5d-8d45-17b22d3d528e-71589427-479e-49d1-aa99-a1c3eb2abbe0756bd81f-5cf8-4a93-96ca-cba48f5961dd" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "sourceHandle": "passthrough-c30d0382-c2bb-4752-b7d9-adf499109272", + "target": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "targetHandle": "bc59de87-d186-4ce5-a84a-9c6b312809a3", + "id": "reactflow__edge-71589427-479e-49d1-aa99-a1c3eb2abbe0passthrough-c30d0382-c2bb-4752-b7d9-adf499109272-2d2cf9c1-be1d-416e-8c46-ce75cad34389bc59de87-d186-4ce5-a84a-9c6b312809a3" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "sourceHandle": "passthrough-e626ccea-0fce-4c02-8f55-1ba71b305748", + "target": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "targetHandle": "b2040c56-449d-4baf-8851-7bbe4d398031", + "id": "reactflow__edge-71589427-479e-49d1-aa99-a1c3eb2abbe0passthrough-e626ccea-0fce-4c02-8f55-1ba71b305748-2d2cf9c1-be1d-416e-8c46-ce75cad34389b2040c56-449d-4baf-8851-7bbe4d398031", + "selected": false + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "sourceHandle": "a05a3810-00d5-4d23-9ac5-c5b6c581722f", + "target": "e365c49a-eb07-4619-8794-ce76195b69bd", + "targetHandle": "34b10d45-56e1-41c6-bc8e-d03fda5be5ea", + "id": "reactflow__edge-2d2cf9c1-be1d-416e-8c46-ce75cad34389a05a3810-00d5-4d23-9ac5-c5b6c581722f-e365c49a-eb07-4619-8794-ce76195b69bd34b10d45-56e1-41c6-bc8e-d03fda5be5ea" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "sourceHandle": "56eecb95-16c2-49c1-b61c-8cccd8069146", + "target": "ef8d8a40-c1ad-4b09-97b6-8a3b3588a09a", + "targetHandle": "5f4072b3-33a9-4362-a567-23defb402dca", + "id": "reactflow__edge-2d2cf9c1-be1d-416e-8c46-ce75cad3438956eecb95-16c2-49c1-b61c-8cccd8069146-ef8d8a40-c1ad-4b09-97b6-8a3b3588a09a5f4072b3-33a9-4362-a567-23defb402dca" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "sourceHandle": "passthrough-e1997b9a-6808-4eca-b049-759da4ae946c", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "fd8d4517-173f-4c94-bcf9-cbf338da672f", + "id": "reactflow__edge-6095bf21-70c9-49de-8a03-d8d54b533ca6passthrough-e1997b9a-6808-4eca-b049-759da4ae946c-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcfd8d4517-173f-4c94-bcf9-cbf338da672f" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "sourceHandle": "passthrough-f516f6f8-1fa6-4814-9ddc-2c85ff85bce6", + "target": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "targetHandle": "688d2ef3-3579-418d-a9db-e29b9a10ab58", + "id": "reactflow__edge-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcpassthrough-f516f6f8-1fa6-4814-9ddc-2c85ff85bce6-2d2cf9c1-be1d-416e-8c46-ce75cad34389688d2ef3-3579-418d-a9db-e29b9a10ab58", + "selected": false + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "86674f72-cb64-4309-b22f-f4e6d415fa8e", + "sourceHandle": "passthrough-acc313e1-f602-4fbe-815e-29bfae9e0992", + "target": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "targetHandle": "634900ee-4a76-4bc8-818e-ee14c1cc74f7", + "id": "reactflow__edge-86674f72-cb64-4309-b22f-f4e6d415fa8epassthrough-acc313e1-f602-4fbe-815e-29bfae9e0992-6095bf21-70c9-49de-8a03-d8d54b533ca6634900ee-4a76-4bc8-818e-ee14c1cc74f7" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "4f4a55db-0bde-4c6f-a269-2d7d825db34c", + "sourceHandle": "db4486e2-7973-4f6f-963d-36ba44913622", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "189507f9-7bc3-405f-9051-f9cc369c328e", + "id": "reactflow__edge-4f4a55db-0bde-4c6f-a269-2d7d825db34cdb4486e2-7973-4f6f-963d-36ba44913622-71589427-479e-49d1-aa99-a1c3eb2abbe0189507f9-7bc3-405f-9051-f9cc369c328e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "4752f3b0-63e7-4395-bcda-152a5f6ae989", + "sourceHandle": "b44b8f0a-fc2d-4cce-96ec-8a299a9f7062", + "target": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "targetHandle": "2ab51b92-7cd7-4318-a989-66deb379833c", + "id": "reactflow__edge-4752f3b0-63e7-4395-bcda-152a5f6ae989b44b8f0a-fc2d-4cce-96ec-8a299a9f7062-2d2cf9c1-be1d-416e-8c46-ce75cad343892ab51b92-7cd7-4318-a989-66deb379833c" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "sourceHandle": "passthrough-f516f6f8-1fa6-4814-9ddc-2c85ff85bce6", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "aaa65d26-79c5-4c83-bf59-ab5c9fc3db99", + "id": "reactflow__edge-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcpassthrough-f516f6f8-1fa6-4814-9ddc-2c85ff85bce6-71589427-479e-49d1-aa99-a1c3eb2abbe0aaa65d26-79c5-4c83-bf59-ab5c9fc3db99" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "sourceHandle": "passthrough-adb834b2-a746-4f2b-9c5e-7b3adf9afda6", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "e626ccea-0fce-4c02-8f55-1ba71b305748", + "id": "reactflow__edge-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcpassthrough-adb834b2-a746-4f2b-9c5e-7b3adf9afda6-71589427-479e-49d1-aa99-a1c3eb2abbe0e626ccea-0fce-4c02-8f55-1ba71b305748" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "6095bf21-70c9-49de-8a03-d8d54b533ca6", + "sourceHandle": "passthrough-634900ee-4a76-4bc8-818e-ee14c1cc74f7", + "target": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "targetHandle": "f516f6f8-1fa6-4814-9ddc-2c85ff85bce6", + "id": "reactflow__edge-6095bf21-70c9-49de-8a03-d8d54b533ca6passthrough-634900ee-4a76-4bc8-818e-ee14c1cc74f7-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcf516f6f8-1fa6-4814-9ddc-2c85ff85bce6" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "sourceHandle": "passthrough-189507f9-7bc3-405f-9051-f9cc369c328e", + "target": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "targetHandle": "71b4b44f-df44-4269-a4bd-eebae8ec1828", + "id": "reactflow__edge-71589427-479e-49d1-aa99-a1c3eb2abbe0passthrough-189507f9-7bc3-405f-9051-f9cc369c328e-2d2cf9c1-be1d-416e-8c46-ce75cad3438971b4b44f-df44-4269-a4bd-eebae8ec1828" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "3ea2d568-b188-4a4a-a2a3-27a0f0aba3fc", + "sourceHandle": "passthrough-adb834b2-a746-4f2b-9c5e-7b3adf9afda6", + "target": "71589427-479e-49d1-aa99-a1c3eb2abbe0", + "targetHandle": "c30d0382-c2bb-4752-b7d9-adf499109272", + "id": "reactflow__edge-3ea2d568-b188-4a4a-a2a3-27a0f0aba3fcpassthrough-adb834b2-a746-4f2b-9c5e-7b3adf9afda6-71589427-479e-49d1-aa99-a1c3eb2abbe0c30d0382-c2bb-4752-b7d9-adf499109272" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "2d2cf9c1-be1d-416e-8c46-ce75cad34389", + "sourceHandle": "passthrough-688d2ef3-3579-418d-a9db-e29b9a10ab58", + "target": "e2f47397-67ed-42f4-8e8d-312a80875e80", + "targetHandle": "c8af03a3-36ae-4480-aac3-744a4e64dac3", + "id": "reactflow__edge-2d2cf9c1-be1d-416e-8c46-ce75cad34389passthrough-688d2ef3-3579-418d-a9db-e29b9a10ab58-e2f47397-67ed-42f4-8e8d-312a80875e80c8af03a3-36ae-4480-aac3-744a4e64dac3" + } + ], + "uuid": "41d883f9-6900-4462-b727-480e96d40b45", + "network": "devnet", + "updated_at": "2023-04-03T15:42:57.451925", + "lastest_flow_run_id": "3073f5fc-0d0a-4f41-8da9-8d3ad3d5b41a", + "environment": null, + "current_rpc": null, + "custom_rpc": null, + "custom_networks": [], + "current_network": { + "id": "01000000-0000-8000-8000-000000000000", + "url": "https://api.devnet.solana.com", + "type": "default", + "wallet": "Solana", + "cluster": "devnet" + }, + "used_by": [], + "instructions_bundling": "Automatic" + }, + "bookmarks": [] +} diff --git a/crates/flow/test_files/uneven_loop.json b/crates/flow/test_files/uneven_loop.json new file mode 100644 index 00000000..d921a096 --- /dev/null +++ b/crates/flow/test_files/uneven_loop.json @@ -0,0 +1,840 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "uneven_loop", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 80 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-01-11", + "parent_flow": null, + "viewport": { + "x": 690.9774364719983, + "y": 274.45477401181836, + "zoom": 0.9592641193252645 + }, + "nodes": [ + { + "width": 300, + "height": 180, + "selected": false, + "id": "927d6831-86c5-4a5b-a8b1-4211c32f5b1e", + "type": "native", + "position": { + "x": -1170, + "y": -90 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "2790b9ef-5da1-40fa-bf90-acce65c1f65b", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "ca77ccdb-069d-472f-9189-3eab39605d00" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "[1, 2, 3]", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "draggable": true, + "positionAbsolute": { + "x": -1170, + "y": -90 + } + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "f161b5ce-7ebf-471d-979b-9f9cae2ecedf", + "type": "native", + "position": { + "x": -1170, + "y": 105 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "d0761d38-1f9a-489d-b7fe-caf8c0dfd1f0", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "8afe950d-12f4-426c-bb10-ff70fe4e1277" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "[\n[\"0,0\", \"0,1\"],\n[\"1,0\", \"1,1\"]\n]", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1170, + "y": 105 + }, + "dragging": false, + "draggable": true + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "560231e6-64d9-42f9-a41e-e82158348d8b", + "type": "native", + "position": { + "x": -840, + "y": 105 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "cd445463-8d5b-4336-afc1-b11f33ebf5fa", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "4a877a69-5fdc-4902-b507-fabb348df57c" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "99e3591c-fabf-4ffd-8fb6-5b788f7e6c4f" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": -840, + "y": 105 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "4c672eae-484e-44cb-80b9-f2c74835d939", + "type": "native", + "position": { + "x": -555, + "y": 105 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "16f1bf94-f353-4cb2-b74e-59a61c43f53f", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "84db1dfa-400b-4298-9249-cb43e9706f50" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "dbd7a2b0-9bc7-4072-a327-386523e0f671" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": -555, + "y": 105 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "35dc3e4a-4677-434b-aff6-e4a9fef35d55", + "type": "native", + "position": { + "x": -555, + "y": -90 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "d73f4a65-e56d-4b49-bc33-fb495ecaba05", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "fc299529-abc9-4ebd-bf94-093a38536cef" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "c3a9ad1e-45e1-4584-b23a-64580bfb551c" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": -555, + "y": -90 + }, + "dragging": false + }, + { + "width": 250, + "height": 150, + "selected": false, + "id": "ff1a6e8a-66e6-4f10-95c7-467144450a84", + "type": "native", + "position": { + "x": -270, + "y": -15 + }, + "style": { + "height": 150, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "aceb1394-bd95-47dd-96cb-d825c6262977", + "unique_node_id": "wait.0.1", + "node_id": "wait", + "version": "0.1", + "description": "Wait for an output to complete before continuing", + "name": "Wait", + "backgroundColorDark": "#000000", + "backgroundColor": "#ffd9b3", + "sources": [], + "targets": [ + { + "name": "value", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "passthrough value", + "passthrough": true, + "id": "4c29c4a6-f085-4842-9e63-a48e5f27b2f6" + }, + { + "name": "wait_for", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "output to wait for", + "passthrough": true, + "id": "1f24f08b-aa59-4668-bd58-fd5512cc1793" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 280 + } + } + }, + "positionAbsolute": { + "x": -270, + "y": -15 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "f8d9bb7f-c564-4385-8ceb-c331db28386b", + "type": "native", + "position": { + "x": 30, + "y": -45 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "96d2bae5-dccd-4d9d-bd6b-6798a8f80fb6", + "unique_node_id": "collect.0.1", + "node_id": "collect", + "version": "0.1", + "description": "Collect inputs into an array", + "name": "Collect", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "array", + "type": "free", + "defaultValue": null, + "tooltip": "", + "id": "ddaa3006-c7ef-4994-a034-9c3caa0dfd1b" + } + ], + "targets": [ + { + "name": "element", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "7a36ca6d-bb20-42f3-a0fa-c15e54f8091a" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 303 + } + } + }, + "positionAbsolute": { + "x": 30, + "y": -45 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "b579cdd9-6ec1-4d74-89fe-8c95fef6989e", + "type": "native", + "position": { + "x": 30, + "y": 75 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "4706390c-947a-4994-b727-ff1b29b88e7f", + "unique_node_id": "collect.0.1", + "node_id": "collect", + "version": "0.1", + "description": "Collect inputs into an array", + "name": "Collect", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "array", + "type": "free", + "defaultValue": null, + "tooltip": "", + "id": "0620aacc-398f-4209-8fde-910a0a9a2e41" + } + ], + "targets": [ + { + "name": "element", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "0af7143f-057a-4e74-b24a-fa078529c0ce" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 303 + } + } + }, + "dragging": false, + "positionAbsolute": { + "x": 30, + "y": 75 + } + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "d6eb97df-72d6-49c7-8be4-8371432317e1", + "type": "native", + "position": { + "x": 315, + "y": -45 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "cee3a5c5-2182-4f1b-ae09-72e9cff5b8de", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "1", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "d6dc1c31-100e-4fe6-8c28-26f515068010" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "1" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 315, + "y": -45 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "dc1595e4-6eb3-4f46-b967-24116d93c167", + "type": "native", + "position": { + "x": 315, + "y": 75 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "a4b3b69e-233a-49fc-a44d-63e348ed450b", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "2", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "9d02d108-d1f4-41d1-bc93-97343e93a81a" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "2" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 315, + "y": 75 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "f161b5ce-7ebf-471d-979b-9f9cae2ecedf", + "sourceHandle": "8afe950d-12f4-426c-bb10-ff70fe4e1277", + "target": "560231e6-64d9-42f9-a41e-e82158348d8b", + "targetHandle": "99e3591c-fabf-4ffd-8fb6-5b788f7e6c4f", + "id": "reactflow__edge-f161b5ce-7ebf-471d-979b-9f9cae2ecedf8afe950d-12f4-426c-bb10-ff70fe4e1277-560231e6-64d9-42f9-a41e-e82158348d8b99e3591c-fabf-4ffd-8fb6-5b788f7e6c4f" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "560231e6-64d9-42f9-a41e-e82158348d8b", + "sourceHandle": "4a877a69-5fdc-4902-b507-fabb348df57c", + "target": "4c672eae-484e-44cb-80b9-f2c74835d939", + "targetHandle": "dbd7a2b0-9bc7-4072-a327-386523e0f671", + "id": "reactflow__edge-560231e6-64d9-42f9-a41e-e82158348d8b4a877a69-5fdc-4902-b507-fabb348df57c-4c672eae-484e-44cb-80b9-f2c74835d939dbd7a2b0-9bc7-4072-a327-386523e0f671" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "927d6831-86c5-4a5b-a8b1-4211c32f5b1e", + "sourceHandle": "ca77ccdb-069d-472f-9189-3eab39605d00", + "target": "35dc3e4a-4677-434b-aff6-e4a9fef35d55", + "targetHandle": "c3a9ad1e-45e1-4584-b23a-64580bfb551c", + "id": "reactflow__edge-927d6831-86c5-4a5b-a8b1-4211c32f5b1eca77ccdb-069d-472f-9189-3eab39605d00-35dc3e4a-4677-434b-aff6-e4a9fef35d55c3a9ad1e-45e1-4584-b23a-64580bfb551c" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "35dc3e4a-4677-434b-aff6-e4a9fef35d55", + "sourceHandle": "fc299529-abc9-4ebd-bf94-093a38536cef", + "target": "ff1a6e8a-66e6-4f10-95c7-467144450a84", + "targetHandle": "4c29c4a6-f085-4842-9e63-a48e5f27b2f6", + "id": "reactflow__edge-35dc3e4a-4677-434b-aff6-e4a9fef35d55fc299529-abc9-4ebd-bf94-093a38536cef-ff1a6e8a-66e6-4f10-95c7-467144450a844c29c4a6-f085-4842-9e63-a48e5f27b2f6" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "4c672eae-484e-44cb-80b9-f2c74835d939", + "sourceHandle": "84db1dfa-400b-4298-9249-cb43e9706f50", + "target": "ff1a6e8a-66e6-4f10-95c7-467144450a84", + "targetHandle": "1f24f08b-aa59-4668-bd58-fd5512cc1793", + "id": "reactflow__edge-4c672eae-484e-44cb-80b9-f2c74835d93984db1dfa-400b-4298-9249-cb43e9706f50-ff1a6e8a-66e6-4f10-95c7-467144450a841f24f08b-aa59-4668-bd58-fd5512cc1793" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "ff1a6e8a-66e6-4f10-95c7-467144450a84", + "sourceHandle": "passthrough-4c29c4a6-f085-4842-9e63-a48e5f27b2f6", + "target": "f8d9bb7f-c564-4385-8ceb-c331db28386b", + "targetHandle": "7a36ca6d-bb20-42f3-a0fa-c15e54f8091a", + "id": "reactflow__edge-ff1a6e8a-66e6-4f10-95c7-467144450a84passthrough-4c29c4a6-f085-4842-9e63-a48e5f27b2f6-f8d9bb7f-c564-4385-8ceb-c331db28386b7a36ca6d-bb20-42f3-a0fa-c15e54f8091a" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "ff1a6e8a-66e6-4f10-95c7-467144450a84", + "sourceHandle": "passthrough-1f24f08b-aa59-4668-bd58-fd5512cc1793", + "target": "b579cdd9-6ec1-4d74-89fe-8c95fef6989e", + "targetHandle": "0af7143f-057a-4e74-b24a-fa078529c0ce", + "id": "reactflow__edge-ff1a6e8a-66e6-4f10-95c7-467144450a84passthrough-1f24f08b-aa59-4668-bd58-fd5512cc1793-b579cdd9-6ec1-4d74-89fe-8c95fef6989e0af7143f-057a-4e74-b24a-fa078529c0ce" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "f8d9bb7f-c564-4385-8ceb-c331db28386b", + "sourceHandle": "ddaa3006-c7ef-4994-a034-9c3caa0dfd1b", + "target": "d6eb97df-72d6-49c7-8be4-8371432317e1", + "targetHandle": "d6dc1c31-100e-4fe6-8c28-26f515068010", + "id": "reactflow__edge-f8d9bb7f-c564-4385-8ceb-c331db28386bddaa3006-c7ef-4994-a034-9c3caa0dfd1b-d6eb97df-72d6-49c7-8be4-8371432317e1d6dc1c31-100e-4fe6-8c28-26f515068010" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "b579cdd9-6ec1-4d74-89fe-8c95fef6989e", + "sourceHandle": "0620aacc-398f-4209-8fde-910a0a9a2e41", + "target": "dc1595e4-6eb3-4f46-b967-24116d93c167", + "targetHandle": "9d02d108-d1f4-41d1-bc93-97343e93a81a", + "id": "reactflow__edge-b579cdd9-6ec1-4d74-89fe-8c95fef6989e0620aacc-398f-4209-8fde-910a0a9a2e41-dc1595e4-6eb3-4f46-b967-24116d93c1679d02d108-d1f4-41d1-bc93-97343e93a81a" + } + ], + "uuid": "295ccef0-e14d-45a1-94ae-46cbaa8bc426", + "network": "devnet", + "updated_at": "2023-01-11T14:48:32.988609", + "lastest_flow_run_id": null, + "environment": null, + "current_rpc": null, + "custom_rpc": null + }, + "bookmarks": [] +} diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml new file mode 100644 index 00000000..82523d3d --- /dev/null +++ b/crates/integration-tests/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "integration-tests" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +cargo_metadata = "0.19.1" +dotenv = "0.15.0" +xshell = "0.2.7" diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs new file mode 100644 index 00000000..32c6e5fa --- /dev/null +++ b/crates/integration-tests/src/main.rs @@ -0,0 +1,65 @@ +#![allow(clippy::print_stderr, clippy::print_stdout)] + +use xshell::{cmd, Shell}; + +fn get_tag(sh: &Shell) -> anyhow::Result { + let dirty = cmd!(sh, "git describe --always --dirty") + .read()? + .trim() + .ends_with("-dirty") + .then_some("-dirty") + .unwrap_or(""); + + let commit = cmd!(sh, "git rev-parse --verify HEAD").read()?; + + Ok(format!("{}{}", commit.trim(), dirty)) +} + +fn run(sh: &Shell) -> anyhow::Result<()> { + let meta = cargo_metadata::MetadataCommand::new().no_deps().exec()?; + sh.change_dir(&meta.workspace_root); + + cmd!(sh, "env PROFILE=dev ./scripts/build_images.bash docker").run()?; + + sh.change_dir("docker/"); + cmd!(sh, "./gen-secrets.ts").run()?; + let tag = get_tag(sh)?; + cmd!( + sh, + "env IMAGE=space-operator/flow-server:{tag} docker compose up -d --wait" + ) + .run()?; + dotenv::from_path(meta.workspace_root.join("docker/.env"))?; + cmd!(sh, "./import-data.ts").run()?; + + sh.change_dir(&meta.workspace_root); + sh.change_dir("@space-operator/client"); + + cmd!(sh, "deno -A tests/auth.ts").run()?; + cmd!(sh, "deno -A tests/deploy.ts").run()?; + + Ok(()) +} + +fn main() { + let meta = cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .unwrap(); + + let sh = Shell::new().unwrap(); + dotenv::dotenv().ok(); + let result = run(&sh); + + sh.change_dir(&meta.workspace_root); + sh.change_dir("docker/"); + cmd!(sh, "docker compose logs flow-server").run().ok(); + cmd!(sh, "docker compose down -v").run().ok(); + cmd!(sh, "docker compose down -v").run().ok(); + cmd!(sh, "docker image prune -f").run().ok(); + + if let Err(error) = result { + eprintln!("{:?}", error); + std::process::exit(1); + } +} diff --git a/crates/pdg-common/Cargo.toml b/crates/pdg-common/Cargo.toml new file mode 100644 index 00000000..2d4fabf9 --- /dev/null +++ b/crates/pdg-common/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pdg-common" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.159", features = ["derive"] } +serde_json = "1.0.95" +serde_repr = "0.1.12" +strum = { version = "0.24.1", features = ["derive"] } +thiserror = "1.0.40" +uuid = { version = "1.3.1", features = ["serde"] } +rand = "0.8.4" +derive_more = "0.99.17" +indexmap = { version = "2.1.0", features = ["serde"] } diff --git a/crates/pdg-common/src/lib.rs b/crates/pdg-common/src/lib.rs new file mode 100644 index 00000000..9812fbf3 --- /dev/null +++ b/crates/pdg-common/src/lib.rs @@ -0,0 +1,210 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use thiserror::Error as ThisError; +use uuid::Uuid; + +pub mod nft_metadata; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[allow(non_snake_case)] +pub struct ResultBool +where + T: std::fmt::Debug, + E: std::fmt::Debug, +{ + pub bSuccess: bool, + #[serde(flatten)] + pub success: Option, + #[serde(flatten)] + pub error: Option, +} + +impl From> for ResultBool +where + T: std::fmt::Debug, + E: std::fmt::Debug, +{ + fn from(value: Result) -> Self { + match value { + Ok(value) => Self { + bSuccess: true, + success: Some(value), + error: None, + }, + Err(error) => Self { + bSuccess: false, + success: None, + error: Some(error), + }, + } + } +} + +#[derive(ThisError, Debug)] +#[error("invalid result: {:?}", .0)] +pub struct Malformed(pub T); + +impl ResultBool { + pub fn into_result(self) -> Result, Malformed> { + if self.bSuccess && self.success.is_some() { + Ok(Ok(self.success.unwrap())) + } else if !self.bSuccess && self.error.is_some() { + Ok(Err(self.error.unwrap())) + } else { + Err(Malformed(self)) + } + } +} + +impl ResultBool<(), E> { + pub fn error_text(e: E) -> String { + serde_json::to_string(&ResultBool::<(), E> { + bSuccess: false, + success: None, + error: Some(e), + }) + .unwrap_or_else(|error| { + serde_json::to_string(&ResultBool::<(), PDGError> { + bSuccess: false, + success: None, + error: Some(PDGError { + error: "SerializeError".to_owned(), + errorDetails: Some(error.to_string()), + }), + }) + .unwrap() + }) + } +} + +impl ResultBool { + pub fn success_text(t: T) -> String { + serde_json::to_string(&ResultBool:: { + bSuccess: true, + success: Some(t), + error: None, + }) + .unwrap_or_else(|error| { + serde_json::to_string(&ResultBool::<(), PDGError> { + bSuccess: false, + success: None, + error: Some(PDGError { + error: "SerializeError".to_owned(), + errorDetails: Some(error.to_string()), + }), + }) + .unwrap() + }) + } +} + +#[derive(Serialize, Deserialize, ThisError, Debug, Clone)] +#[allow(non_snake_case)] +#[error("{:?} {}", error, errorDetails.as_ref().map(String::as_str).unwrap_or_default())] +pub struct PDGError { + pub error: String, + pub errorDetails: Option, +} + +/// Success reply from PDG POST request +#[derive(Serialize, Deserialize, Debug)] +pub struct PostReply { + pub request_uuid: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RenderSuccess { + pub request_uuid: Uuid, + pub main_image_url: String, + pub sketch_image_url: String, + pub metadata_url: String, +} + +/// The request clients send to `WsRender`. +#[derive(Serialize, Deserialize, Debug)] +pub struct RenderRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub rand_seed: Option, + pub version: String, + #[serde(default)] + pub workitem: WorkItem, +} + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +pub struct WorkItem { + pub attributes: HashMap, + pub batchIndex: i32, + pub batchParentId: i32, + pub cloneTargetId: i32, + pub cookType: i32, + pub customData: String, + pub customDataType: String, + pub executionType: i32, + pub frame: i32, + pub frameStep: i32, + pub hasFrame: bool, + pub id: i32, + pub index: i32, + pub isCloneResultData: bool, + pub isFrozen: bool, + pub isNoGenerate: bool, + pub isPostCook: bool, + pub isStatic: bool, + pub loopBeginStackIds: Vec, + pub loopBeginStackIters: Vec, + pub loopBeginStackNumbers: Vec, + pub loopBeginStackSizes: Vec, + pub nodeName: String, + pub priority: i32, + pub state: i32, + pub request_type: i32, +} + +impl Default for WorkItem { + fn default() -> Self { + Self { + attributes: <_>::default(), + batchIndex: -1, + batchParentId: -1, + cloneTargetId: -1, + cookType: 0, + customData: "".to_owned(), + customDataType: "genericdata".to_owned(), + executionType: 0, + frame: 0, + frameStep: 1, + hasFrame: false, + id: 0, + index: 0, + isCloneResultData: false, + isFrozen: false, + isNoGenerate: false, + isPostCook: false, + isStatic: false, + loopBeginStackIds: Vec::new(), + loopBeginStackIters: Vec::new(), + loopBeginStackNumbers: Vec::new(), + loopBeginStackSizes: Vec::new(), + nodeName: "csvoutput2".to_owned(), + priority: 0, + state: 5, + request_type: 0, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Attribute { + pub concat: bool, + pub flag: u32, + pub own: bool, + pub r#type: u32, + pub value: Vec, +} + +/// The request clients send to `WsWait`. +#[derive(Serialize, Deserialize)] +pub struct WaitRequest { + pub request_uuid: Uuid, +} diff --git a/crates/pdg-common/src/nft_metadata/generate.rs b/crates/pdg-common/src/nft_metadata/generate.rs new file mode 100644 index 00000000..4412721c --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/generate.rs @@ -0,0 +1,654 @@ +use super::{ + metaplex::COLOR_NAMES, BodyMaterialVariations, BodyType, EnumRandExt, EnvLight, Fx0, Fx1, Fx1a, + Fx2, Fx3, Fx4, Fx5, Fx6, FxJellyfish, FxLineartHelper, GlowingLogo, HelmetLight, HelmetType, + LightReflectionMult, MarbleVariation, Pose, RenderParams, WoodVariation, +}; +use indexmap::IndexSet; +use rand::{seq::SliceRandom, Rng}; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; + +pub fn random_hue(rng: &mut R) -> f64 { + const NUM_COLORS: usize = COLOR_NAMES.len(); + rng.gen_range(0..NUM_COLORS) as f64 * (360.0 / NUM_COLORS as f64) +} + +/// Effects that an NFT can gain +#[derive( + derive_more::From, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + Eq, + PartialEq, + Serialize, + Deserialize, + Hash, +)] +#[serde(tag = "type", content = "value")] +pub enum Effect { + #[strum(props(EffectType = "Pose"))] + #[from] + Pose(Pose), + #[strum(props(EffectType = "Fx0"))] + #[from] + Fx0(Fx0), + #[strum(props(EffectType = "Fx1"))] + #[from] + Fx1(Fx1), + #[strum(props(EffectType = "Fx2"))] + #[from] + Fx2(Fx2), + #[strum(props(EffectType = "Fx3"))] + #[from] + Fx3(Fx3), + #[strum(props(EffectType = "Fx4"))] + #[from] + Fx4(Fx4), + #[strum(props(EffectType = "Fx5"))] + #[from] + Fx5(Fx5), + #[strum(props(EffectType = "Fx6"))] + #[from] + Fx6(Fx6), + #[strum(props(EffectType = "Fx1a"))] + #[from] + Fx1a(Fx1a), + #[strum(props(EffectType = "FxJellyfish"))] + #[from] + FxJellyfish(FxJellyfish), + #[strum(props(EffectType = "FxLineartHelper"))] + #[from] + FxLineartHelper(FxLineartHelper), +} + +pub struct EffectsList { + pub effects: IndexSet, +} + +impl From> for EffectsList { + fn from(effects: IndexSet) -> Self { + Self { effects } + } +} + +impl From> for EffectsList { + fn from(effects: Vec) -> Self { + Self { + effects: effects.into_iter().collect(), + } + } +} + +impl From<[Effect; N]> for EffectsList { + fn from(effects: [Effect; N]) -> Self { + Self { + effects: effects.into(), + } + } +} + +impl From for EffectsList { + fn from(value: RenderParams) -> Self { + let RenderParams { + body_type: _, + pose, + helmet_type: _, + helmet_light: _, + fx0, + fx1, + fx1a, + fx2, + fx3, + fx4, + fx5, + fx6, + fx0_bodyoff: _, + fx0_bodyoff_glass: _, + body_material_variation: _, + marble_variation: _, + wood_variation: _, + fx_jellifish, + fx_lineart_helper, + env_light: _, + env_reflection: _, + light_reflection_mult: _, + glowing_logo: _, + logo_hue: _, + logo_name: _, + butterfly_amount: _, + disintegration_amount: _, + melt_amount: _, + fall_amount: _, + firefly_amount: _, + frozen_amount: _, + fungi_amount: _, + gold_silver_amount: _, + grow_flower_amount: _, + hologram_amount: _, + eyes_light_intensity_amount: _, + ladybag_amount: _, + lineart_amount: _, + melting_glow_amount: _, + pixel_amount: _, + rain_amount: _, + smoke_amount: _, + soap_bubble_intensity_amount: _, + soap_bubble_roughness_amount: _, + spring_amount: _, + underwater_fog_amount: _, + xray_body_amount: _, + xray_skeleton_particles_amount: _, + background_color_random_hue: _, + background_underwater_color_hue: _, + dress_color_hue: _, + eye_color_random_hue: _, + random_value: _, + wedgeindex: _, + render_noise_threshold: _, + render_resolution: _, + wedgeattribs: _, + } = value; + Self { + effects: [ + pose.into(), + fx0.into(), + fx1.into(), + fx2.into(), + fx3.into(), + fx4.into(), + fx5.into(), + fx6.into(), + fx1a.into(), + fx_jellifish.into(), + fx_lineart_helper.into(), + ] + .into(), + } + } +} + +impl EffectsList { + pub fn effect_lottery( + &self, + mut choose_from: Vec, + rng: &mut R, + ) -> Option { + choose_from.retain(|e| !self.effects.contains(e)); + choose_from.choose(rng).cloned() + } + + pub fn push(&mut self, effect: Effect) -> bool { + self.effects.insert(effect) + } +} + +impl Effect { + pub fn all_effects() -> Vec { + let mut list = Vec::new(); + for e in Effect::iter() { + match e { + Effect::Pose(_) => list.extend(Pose::iter().map(Effect::from)), + Effect::Fx0(_) => list.extend(Fx0::iter().map(Effect::from)), + Effect::Fx1(_) => list.extend(Fx1::iter().map(Effect::from)), + Effect::Fx2(_) => list.extend(Fx2::iter().map(Effect::from)), + Effect::Fx3(_) => list.extend(Fx3::iter().map(Effect::from)), + Effect::Fx4(_) => list.extend(Fx4::iter().map(Effect::from)), + Effect::Fx5(_) => list.extend(Fx5::iter().map(Effect::from)), + Effect::Fx6(_) => list.extend(Fx6::iter().map(Effect::from)), + Effect::Fx1a(_) => list.extend(Fx1a::iter().map(Effect::from)), + Effect::FxJellyfish(_) => list.extend(FxJellyfish::iter().map(Effect::from)), + Effect::FxLineartHelper(_) => { + list.extend(FxLineartHelper::iter().map(Effect::from)) + } + } + } + list + } +} + +impl RenderParams { + pub fn add_effect(&mut self, effect: Effect) { + match effect { + Effect::Pose(x) => self.pose = x, + Effect::Fx0(x) => self.fx0 = x, + Effect::Fx1(x) => self.fx1 = x, + Effect::Fx2(x) => self.fx2 = x, + Effect::Fx3(x) => self.fx3 = x, + Effect::Fx4(x) => self.fx4 = x, + Effect::Fx5(x) => self.fx5 = x, + Effect::Fx6(x) => self.fx6 = x, + Effect::Fx1a(x) => self.fx1a = x, + Effect::FxJellyfish(x) => self.fx_jellifish = x, + Effect::FxLineartHelper(x) => self.fx_lineart_helper = x, + } + } + + pub fn generate_base(rng: &mut R) -> Self { + let body_type = BodyType::choose(rng); + let pose = Pose::choose(rng); + let helmet_type = HelmetType::choose(rng); + let helmet_light = HelmetLight::choose(rng); + let fx0 = Fx0::choose(rng); + + Self { + body_type, + pose, + helmet_type, + helmet_light, + fx0, + ..<_>::default() + } + .adjust_base(rng) + .generate_line_art(rng) + .generate_fx(rng) + .generate_underwater(rng) + .generate_background_color(rng) + .generate_dress_hue(rng) + .generate_helmet_lights(rng) + .generate_wedge(rng) + .generate_body_material_variation(rng) + .generate_marble_variation(rng) + .generate_wood_variation(rng) + .glowing_logo(rng) + .generate_smoke(rng) + .generate_random_value(rng) + } + + pub fn adjust_base(mut self, rng: &mut R) -> Self { + match self.fx0 { + Fx0::Hologram => { + self.env_light = EnvLight::Night; + self.hologram_amount = rng.gen_range(25.0..=100.0) + } + Fx0::Xray => { + self.env_light = EnvLight::day_or_night(rng); + self.xray_skeleton_particles_amount = rng.gen_range(25.0..=100.0); + self.xray_body_amount = rng.gen_range(25.0..=100.0); + } + Fx0::SoapBubble => { + self.env_light = EnvLight::day_or_night(rng); + self.soap_bubble_intensity_amount = rng.gen_range(25.0..=100.0); + self.soap_bubble_roughness_amount = rng.gen_range(25.0..=100.0); + self.light_reflection_mult = LightReflectionMult::Two; + } + Fx0::Pixel => { + self.env_light = EnvLight::day_or_night(rng); + self.pixel_amount = rng.gen::() * rng.gen_range(20.0..=40.0); + } + _ => { + self.env_light = EnvLight::day_or_night(rng); + } + } + self + } + + pub fn generate_line_art(mut self, rng: &mut R) -> Self { + match self.fx0 { + Fx0::LineartBase => { + let line_art = Fx1a::choose(rng); + self.lineart_amount = rng.gen_range(0.0..100.0); + + self.fx1a = line_art; + match line_art { + Fx1a::LineartMinimalistic => { + let fx_lineart_helper = FxLineartHelper::Zero; + self.fx_lineart_helper = fx_lineart_helper; + } + Fx1a::LineartHeavy => { + let fx_lineart_helper = FxLineartHelper::Zero; + self.fx_lineart_helper = fx_lineart_helper; + self.helmet_light = HelmetLight::some_lights(rng); + } + Fx1a::No => { + let fx_lineart_helper = FxLineartHelper::Zero; + self.fx_lineart_helper = fx_lineart_helper; + } + } + } + _ => { + let line_art = Fx1a::none_or_minimal(rng); + self.fx1a = line_art; + match line_art { + Fx1a::No => { + let fx_lineart_helper = FxLineartHelper::choose(rng); + self.fx_lineart_helper = fx_lineart_helper; + } + Fx1a::LineartMinimalistic => { + let fx_lineart_helper = FxLineartHelper::One; + self.fx_lineart_helper = fx_lineart_helper; + } + Fx1a::LineartHeavy => {} + } + } + } + self + } + + pub fn generate_fx(mut self, rng: &mut R) -> Self { + let fx1 = Fx1::choose(rng); + + self.fx1 = fx1; + match fx1 { + Fx1::No => {} + Fx1::Melted => { + self.melt_amount = rng.gen::() * 30.0; + self.melting_glow_amount = 50.0; + } + Fx1::Disintegration => self.disintegration_amount = rng.gen::() * 30.0, + } + + let fx2 = Fx2::choose(rng); + self.fx2 = fx2; + match fx2 { + Fx2::No => {} + Fx2::Butterflies => self.butterfly_amount = rng.gen::() * 30.0, + Fx2::Underwater => {} // underwater is handled separately + Fx2::Fireflyies => self.firefly_amount = rng.gen::() * 30.0, + Fx2::Fall => self.fall_amount = rng.gen::() * 30.0, + Fx2::Ladybag => self.ladybag_amount = rng.gen::() * 30.0, + Fx2::Spring => self.spring_amount = rng.gen::() * 30.0, + } + + let fx4 = Fx4::choose(rng); + self.fx4 = fx4; + match fx4 { + Fx4::No => {} + Fx4::Frozen => self.frozen_amount = rng.gen::() * 30.0, + Fx4::Rain => self.rain_amount = rng.gen::() * 40.0, + } + + let fx5 = Fx5::choose(rng); + self.fx5 = fx5; + match fx5 { + Fx5::No => {} + Fx5::Fungi => self.fungi_amount = rng.gen_range(10.0..=30.0), + Fx5::GrowFlower => self.grow_flower_amount = rng.gen_range(10.0..=30.0), + } + + let fx6 = Fx6::choose(rng); + self.fx6 = fx6; + match fx6 { + Fx6::No => {} + Fx6::Gold => self.gold_silver_amount = rng.gen_range(5.0..=15.0), + Fx6::Silver => self.gold_silver_amount = rng.gen_range(5.0..=15.0), + Fx6::RoseGold => self.gold_silver_amount = rng.gen_range(5.0..=15.0), + Fx6::Bronze => self.gold_silver_amount = rng.gen_range(5.0..=15.0), + Fx6::Copper => self.gold_silver_amount = rng.gen_range(5.0..=15.0), + } + + self + } + + pub fn generate_underwater(mut self, rng: &mut R) -> Self { + if self.fx2 == Fx2::Underwater { + let jellyfish = FxJellyfish::choose(rng); + self.fx_jellifish = jellyfish; + + self.underwater_fog_amount = rng.gen::() * 30.0; + self.background_underwater_color_hue = 38.8; + + let env_light = if self.fx0 == Fx0::Hologram { + EnvLight::UnderwaterHologram + } else { + EnvLight::Underwater + }; + self.env_light = env_light; + } + self + } + + pub fn generate_helmet_lights(mut self, rng: &mut R) -> Self { + match self.helmet_light { + HelmetLight::Dots | HelmetLight::GlowingEyes => { + self.eye_color_random_hue = random_hue(rng); + self.eyes_light_intensity_amount = 100.0; + } + _ => {} + } + self + } + + pub fn generate_wedge(mut self, rng: &mut R) -> Self { + self.wedgeindex = rng.gen_range(25i64..=1000000000i64); + self + } + + pub fn generate_background_color(mut self, rng: &mut R) -> Self { + self.background_color_random_hue = random_hue(rng); + self + } + + pub fn generate_dress_hue(mut self, rng: &mut R) -> Self { + self.dress_color_hue = random_hue(rng); + self + } + + pub fn generate_body_material_variation(mut self, rng: &mut R) -> Self { + if self.fx0 == Fx0::No { + self.body_material_variation = Some(BodyMaterialVariations::choose(rng)); + } + self + } + + pub fn generate_marble_variation(mut self, rng: &mut R) -> Self { + if self.fx0 == Fx0::Marble { + self.marble_variation = Some(MarbleVariation::choose(rng)); + } + self + } + + pub fn generate_wood_variation(mut self, rng: &mut R) -> Self { + if self.fx0 == Fx0::Wood { + self.wood_variation = Some(WoodVariation::choose(rng)); + } + self + } + + pub fn glowing_logo(mut self, rng: &mut R) -> Self { + self.glowing_logo = Some(GlowingLogo::choose(rng)); + if self.glowing_logo == Some(GlowingLogo::Yes) { + self.logo_hue = Some(random_hue(rng)); + } + self + } + + pub fn generate_random_value(mut self, rng: &mut R) -> Self { + self.random_value = rng.gen::() * 360.0; + self + } + + pub fn generate_smoke(mut self, rng: &mut R) -> Self { + match self.env_light { + EnvLight::Day | EnvLight::Underwater | EnvLight::UnderwaterHologram => { + self.smoke_amount = 0.0; + self.fx3 = Fx3::No; + } + EnvLight::Night => { + self.fx3 = Fx3::smoke_or_not(rng); + match self.fx3 { + Fx3::Smoke => self.smoke_amount = rng.gen_range(25.0..=50.0), + Fx3::No => self.smoke_amount = 0.0, + } + } + } + self + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use serde_json::json; + use strum::IntoEnumIterator; + + use crate::nft_metadata::EnvReflection; + + use super::*; + + #[test] + fn test() { + // generate a base + let base = RenderParams::generate_base(&mut rand::thread_rng()); + dbg!(&base); + + // store user poses + let mut poses: HashSet = HashSet::new(); + poses.insert(base.pose); + + // get a new random pose not in user poses + fn get_new_pose(poses: HashSet) -> Pose { + let mut new_poses: Vec = Pose::iter().collect(); + new_poses.retain(|p| !poses.contains(p)); + new_poses[rand::thread_rng().gen_range(0..new_poses.len())] + } + dbg!(get_new_pose(poses)); + + // adjust pose + #[allow(dead_code)] + fn adjust_pose(poses: HashSet, selected_pose: Pose) -> Option { + // check poses is not empty + if poses.is_empty() | !poses.contains(&selected_pose) { + None + } else { + Some(selected_pose) + } + } + + // tune owned fxs + // adjust amount + // toggle on/off + + let mut user_effects: HashMap = HashMap::new(); + user_effects.insert("fx0".to_string(), json!([base.fx0.to_string()])); + user_effects.insert("fx1".to_string(), json!([base.fx1.to_string()])); + user_effects.insert("fx2".to_string(), json!([base.fx2.to_string()])); + user_effects.insert("fx4".to_string(), json!([base.fx4.to_string()])); + user_effects.insert("fx5".to_string(), json!([base.fx5.to_string()])); + user_effects.insert("fx6".to_string(), json!([base.fx6.to_string()])); + user_effects.insert( + "fx_jellifish".to_string(), + json!([base.fx_jellifish.to_string()]), + ); + user_effects.insert( + "fx_lineart_helper".to_string(), + json!([base.fx_lineart_helper.to_string()]), + ); + user_effects.insert("fx1a".to_string(), json!([base.fx1a.to_string()])); + dbg!(&user_effects); + + // convert user_effects to json + let user_effects_json = serde_json::to_value(&user_effects).unwrap(); + dbg!(&user_effects_json); + + // effect_lottery + // read user effects and get a new random effect not in user effects + fn get_new_effect(user_effects: &HashMap) -> String { + fn get_effect_names( + user_effects: &HashMap, + key: &str, + ) -> Vec { + user_effects + .get(key) + .map(|effect| { + effect + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap().to_string()) + .collect::>() + }) + .unwrap_or_default() + } + + // get fx0 not in user_effects + let mut fx0: Vec = Fx0::iter() + .filter(|fx| *fx != Fx0::No) + .map(|fx| fx.to_string()) + .collect::>(); + + let effects = get_effect_names(user_effects, "fx0"); + fx0.retain(|fx| !effects.contains(fx)); + fx0[rand::thread_rng().gen_range(0..fx0.len())].to_string(); + + // get fx1 not in user_effects + let mut fx1: Vec = Fx1::iter() + .filter(|fx| *fx != Fx1::No) + .map(|fx| fx.to_string()) + .collect::>(); + let effects = get_effect_names(user_effects, "fx1"); + fx1.retain(|fx| !effects.contains(fx)); + fx1[rand::thread_rng().gen_range(0..fx1.len())].to_string(); + + //join the two new_effects + let new_effects = [fx0, fx1].concat(); + + new_effects[rand::thread_rng().gen_range(0..new_effects.len())].to_string() + } + + let _new_effect = dbg!(get_new_effect(&user_effects)); + // + + // add new fx + #[allow(dead_code)] + fn add_new_effect_to_base(mut base: RenderParams, new_effect: &str) -> RenderParams { + //find the new_effect in the RenderParams enum + match new_effect { + "Hologram" => base.fx0 = Fx0::Hologram, + "Xray" => base.fx0 = Fx0::Xray, + "SoapBubble" => base.fx0 = Fx0::SoapBubble, + "Pixel" => base.fx0 = Fx0::Pixel, + "Melted" => base.fx1 = Fx1::Melted, + "Disintegration" => base.fx1 = Fx1::Disintegration, + "Butterflies" => base.fx2 = Fx2::Butterflies, + "Underwater" => base.fx2 = Fx2::Underwater, + "Fireflyies" => base.fx2 = Fx2::Fireflyies, + "Fall" => base.fx2 = Fx2::Fall, + "Ladybag" => base.fx2 = Fx2::Ladybag, + "Spring" => base.fx2 = Fx2::Spring, + "Frozen" => base.fx4 = Fx4::Frozen, + "Rain" => base.fx4 = Fx4::Rain, + "Fungi" => base.fx5 = Fx5::Fungi, + "GrowFlower" => base.fx5 = Fx5::GrowFlower, + "Gold" => base.fx6 = Fx6::Gold, + "Silver" => base.fx6 = Fx6::Silver, + "LineartMinimalistic" => base.fx1a = Fx1a::LineartMinimalistic, + "LineartHeavy" => base.fx1a = Fx1a::LineartHeavy, + _ => {} + } + + //add the new_effect to the base + //return the base + base + } + + // dbg!(add_new_effect_to_base(base, &new_effect)); + // togg + // dbg!(base); + } + + #[test] + fn iterate() { + let count = BodyType::iter().count() + * Pose::iter().count() + * HelmetType::iter().count() + * HelmetLight::iter().count() + * (Fx0::iter().count() - 2) + * Fx1::iter().count() + * Fx1a::iter().count() + * Fx2::iter().count() + * Fx3::iter().count() + * Fx4::iter().count() + * Fx5::iter().count() + * Fx6::iter().count() + * FxJellyfish::iter().count() + * FxLineartHelper::iter().count() + * EnvLight::iter().count() + * EnvReflection::iter().count() + * LightReflectionMult::iter().count(); + println!("{}", count); + } +} diff --git a/crates/pdg-common/src/nft_metadata/metaplex.rs b/crates/pdg-common/src/nft_metadata/metaplex.rs new file mode 100644 index 00000000..e3aef883 --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/metaplex.rs @@ -0,0 +1,291 @@ +use super::{generate::EffectsList, EnumExt, PropertyNotFound, RenderParams}; +use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; + +// Using this wheel for names +// https://i.pinimg.com/originals/bb/61/4e/bb614ebff2617fcd9e273ccc2d98201b.jpg +pub const COLOR_NAMES: &[&str] = &[ + "Red", "Brick", "Orange", "Gold", "Yellow", "Lime", "Green", "Teal", "Blue", "Indigo", + "Purple", "Violet", +]; +pub fn hue_to_color_name(mut hue: f64) -> String { + // https://i.stack.imgur.com/pSUUV.jpg + hue = (hue + 15.0) % 360f64; + if hue < 0.0 { + hue += 360.0; + } + let index = (hue / 30.0).floor() as usize; + COLOR_NAMES[index].to_owned() +} + +/// Traits that will be included when uploading to Metaplex +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NftTraits { + pub body: super::BodyType, + pub helmet: super::HelmetType, + pub helmet_light: super::HelmetLight, + pub dress_color: String, + pub pose: super::Pose, + pub effects_count: usize, + pub composition: super::Fx0, + pub transformation: super::Fx1, + pub season: super::Fx2, + pub weather: super::Fx4, + pub smoke: super::Fx3, + pub growth: super::Fx5, + pub wrapping: super::Fx6, + pub animal: Animal, +} + +#[derive(strum::EnumProperty, strum::EnumIter, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Animal { + #[strum(props(MetaplexName = "No"))] + No, + #[strum(props(MetaplexName = "Jellyfish"))] + Jellyfish, + #[strum(props(MetaplexName = "Firefly"))] + Firefly, + #[strum(props(MetaplexName = "Ladybug"))] + Ladybug, + #[strum(props(MetaplexName = "Butterfly"))] + Butterfly, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct MetaplexAttribute { + pub trait_type: String, + pub value: String, +} + +#[derive(ThisError, Debug)] +#[error("{:?}", self)] +pub enum ParseMetaflexError { + TraitNotFound { + trait_type: String, + }, + UnknownVariant { + ty: &'static str, + value: String, + }, + PropertyNotFound(#[from] PropertyNotFound), + ParsingError { + trait_type: String, + value: String, + error: String, + }, +} + +impl NftTraits { + pub fn new(r: &RenderParams, effects: &EffectsList) -> Self { + Self { + body: r.body_type, + helmet: r.helmet_type, + helmet_light: r.helmet_light, + dress_color: hue_to_color_name(r.dress_color_hue), + pose: r.pose, + effects_count: effects.effects.len(), + composition: r.fx0, + transformation: r.fx1, + season: match r.fx2 { + super::Fx2::Spring => super::Fx2::Spring, + super::Fx2::Fall => super::Fx2::Fall, + super::Fx2::No => super::Fx2::No, + super::Fx2::Butterflies => super::Fx2::No, + super::Fx2::Underwater => super::Fx2::No, + super::Fx2::Fireflyies => super::Fx2::No, + super::Fx2::Ladybag => super::Fx2::No, + }, + weather: r.fx4, + smoke: r.fx3, + growth: r.fx5, + wrapping: r.fx6, + animal: match r.fx2 { + super::Fx2::Ladybag => Animal::Ladybug, + super::Fx2::Butterflies => Animal::Butterfly, + super::Fx2::Fireflyies => Animal::Firefly, + super::Fx2::Underwater => match r.fx_jellifish { + super::FxJellyfish::Yes => Animal::Jellyfish, + super::FxJellyfish::No => Animal::No, + }, + super::Fx2::No => Animal::No, + super::Fx2::Fall => Animal::No, + super::Fx2::Spring => Animal::No, + }, + } + } + + /// Read from an `attributes` array + /// + /// https://docs.metaplex.com/programs/token-metadata/token-standard#the-programmable-non-fungible-standard + pub fn parse_metaplex_attrs(v: &[MetaplexAttribute]) -> Result { + fn find_str<'a>( + v: &'a [MetaplexAttribute], + trait_type: &str, + ) -> Result<&'a str, ParseMetaflexError> { + v.iter() + .find(|a| a.trait_type == trait_type) + .map(|a| a.value.as_str()) + .ok_or_else(|| ParseMetaflexError::TraitNotFound { + trait_type: trait_type.to_owned(), + }) + } + use std::str::FromStr; + fn find_from_str( + v: &[MetaplexAttribute], + trait_type: &str, + ) -> Result + where + I: FromStr, + I::Err: ToString, + { + v.iter() + .find(|a| a.trait_type == trait_type) + .map(|a| a.value.as_str()) + .ok_or_else(|| ParseMetaflexError::TraitNotFound { + trait_type: trait_type.to_owned(), + }) + .and_then(|s| { + s.parse() + .map_err(|e: I::Err| ParseMetaflexError::ParsingError { + trait_type: trait_type.to_owned(), + value: s.to_owned(), + error: e.to_string(), + }) + }) + } + fn find_enum( + v: &[MetaplexAttribute], + trait_type: &str, + ) -> Result { + let s = find_str(v, trait_type)?; + for variant in E::iter() { + if variant.metaplex_name()? == s { + return Ok(variant); + } + } + + Err(ParseMetaflexError::UnknownVariant { + ty: std::any::type_name::(), + value: s.to_owned(), + }) + } + Ok(Self { + body: find_enum(v, "Body")?, + helmet: find_enum(v, "Helmet")?, + helmet_light: find_enum(v, "Helmet Light")?, + dress_color: find_from_str(v, "Dress Color Hue")?, + pose: find_enum(v, "Pose")?, + effects_count: find_from_str(v, "Gained Effects")?, + composition: find_enum(v, "Composition")?, + transformation: find_enum(v, "Transformation")?, + season: find_enum(v, "Season")?, + weather: find_enum(v, "Weather")?, + smoke: find_enum(v, "Smoke")?, + growth: find_enum(v, "Growth")?, + wrapping: find_enum(v, "Wrapping")?, + animal: find_enum(v, "Animal")?, + }) + } + + /// Convert into `attributes` array + /// + /// https://docs.metaplex.com/programs/token-metadata/token-standard#the-programmable-non-fungible-standard + pub fn gen_metaplex_attrs(&self) -> Result, PropertyNotFound> { + let Self { + body, + helmet, + helmet_light, + dress_color, + pose, + effects_count, + composition, + transformation, + season, + weather, + smoke, + growth, + wrapping, + animal, + } = self; + + fn push(v: &mut Vec, ty: &str, value: impl Into) { + assert!(v.iter().all(|a| a.trait_type != ty)); + v.push(MetaplexAttribute { + trait_type: ty.to_owned(), + value: value.into(), + }); + } + + let mut v = Vec::new(); + + push(&mut v, "Body", body.metaplex_name()?); + push(&mut v, "Helmet", helmet.metaplex_name()?); + push(&mut v, "Helmet Light", helmet_light.metaplex_name()?); + push(&mut v, "Dress Color Hue", dress_color.clone()); + push(&mut v, "Pose", pose.metaplex_name()?); + push(&mut v, "Gained Effects", effects_count.to_string()); + push(&mut v, "Composition", composition.metaplex_name()?); + push(&mut v, "Transformation", transformation.metaplex_name()?); + push(&mut v, "Season", season.metaplex_name()?); + push(&mut v, "Weather", weather.metaplex_name()?); + push(&mut v, "Smoke", smoke.metaplex_name()?); + push(&mut v, "Growth", growth.metaplex_name()?); + push(&mut v, "Wrapping", wrapping.metaplex_name()?); + push(&mut v, "Animal", animal.metaplex_name()?); + + Ok(v) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nft_metadata::{ + BodyType, EnumExt, Fx0, Fx1, Fx2, Fx3, Fx4, Fx5, Fx6, HelmetLight, HelmetType, Pose, + }; + use strum::{EnumProperty, IntoEnumIterator}; + + #[test] + fn test_name_available() { + fn test() + where + E: IntoEnumIterator + EnumProperty + std::fmt::Debug, + { + for variant in E::iter() { + variant.metaplex_name().unwrap(); + } + } + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + test::(); + } + + /* + * TODO: add this test back + #[test] + fn test_gen_metaplex_attrs() { + let mut json = + serde_json::from_str::(include_str!("tests/123.json")).unwrap(); + let params = RenderParams::from_pdg_metadata(&mut json, true, &<_>::default()).unwrap(); + let effects = EffectsList::from(params.clone()); + let meta = NftTraits::new(¶ms, &effects); + let attrs = meta.gen_metaplex_attrs().unwrap(); + let json = serde_json::to_string_pretty(&attrs).unwrap(); + println!("{}", json); + let meta1 = NftTraits::parse_metaplex_attrs( + &serde_json::from_str::>(&json).unwrap(), + ) + .unwrap(); + assert_eq!(meta, meta1); + } + */ +} diff --git a/crates/pdg-common/src/nft_metadata/mod.rs b/crates/pdg-common/src/nft_metadata/mod.rs new file mode 100644 index 00000000..e6949428 --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/mod.rs @@ -0,0 +1,2121 @@ +use self::pdg::{Attr, AttrCfg}; +use rand::seq::{IteratorRandom, SliceRandom}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::{borrow::Cow, collections::HashMap, fmt::Debug}; +use strum::{Display, IntoEnumIterator}; +use thiserror::Error as ThisError; + +pub mod generate; +pub mod metaplex; +pub mod pdg; + +#[derive(ThisError, Debug)] +#[error("{:?}", self)] +pub struct PropertyNotFound { + pub attr: &'static str, + pub ty: &'static str, + pub variant: String, +} + +#[derive(ThisError, Debug)] +pub enum WeightError { + #[error(transparent)] + PropertyNotFound(#[from] PropertyNotFound), + #[error("invalid weight {} on type {}", value, ty)] + InvalidValue { value: &'static str, ty: String }, +} + +pub trait EnumExt { + fn pdg_name(&self) -> Result<&'static str, PropertyNotFound>; + fn metaplex_name(&self) -> Result<&'static str, PropertyNotFound>; + fn effect_type(&self) -> Result<&'static str, PropertyNotFound>; + fn weight(&self) -> Result; +} + +pub trait EnumRandExt { + fn choose(rng: &mut R) -> Self; + fn choose_uniform(rng: &mut R) -> Self; + fn choose_weighted(rng: &mut R) -> Self; +} + +impl EnumExt for T +where + T: strum::EnumProperty + std::fmt::Debug, +{ + fn pdg_name(&self) -> Result<&'static str, PropertyNotFound> { + self.get_str("PDGName").ok_or_else(|| PropertyNotFound { + attr: "PDGName", + ty: std::any::type_name::(), + variant: format!("{:?}", self), + }) + } + fn metaplex_name(&self) -> Result<&'static str, PropertyNotFound> { + self.get_str("MetaplexName") + .ok_or_else(|| PropertyNotFound { + attr: "MetaplexName", + ty: std::any::type_name::(), + variant: format!("{:?}", self), + }) + } + fn effect_type(&self) -> Result<&'static str, PropertyNotFound> { + self.get_str("EffectType").ok_or_else(|| PropertyNotFound { + attr: "EffectType", + ty: std::any::type_name::(), + variant: format!("{:?}", self), + }) + } + fn weight(&self) -> Result { + let value = self.get_str("weight").ok_or_else(|| PropertyNotFound { + attr: "weight", + ty: std::any::type_name::(), + variant: format!("{:?}", self), + })?; + value.parse().map_err(|_| WeightError::InvalidValue { + value, + ty: format!("{:?}", self), + }) + } +} + +impl EnumRandExt for T +where + T: EnumExt + IntoEnumIterator + Clone, +{ + fn choose(rng: &mut R) -> Self { + let has_weight = T::iter().next().unwrap().weight().is_ok(); + if has_weight { + T::choose_weighted(rng) + } else { + T::choose_uniform(rng) + } + } + + fn choose_uniform(rng: &mut R) -> Self { + T::iter().choose(rng).unwrap().clone() + } + + fn choose_weighted(rng: &mut R) -> Self { + T::iter() + .collect::>() + .choose_weighted(rng, |v| v.weight().unwrap_or(0.0)) + .unwrap() + .clone() + } +} + +/* +const DEFAULT_SPLIT: i64 = 1; +const DEFAULT_WEDGECOUNT: i64 = 30; +const DEFAULT_WEDGENUM: i64 = 0; +const DEFAULT_WEDGETOTAL: i64 = 30; + +const DEFAULT_WEDGEATTRIBS: Attr<&[&str]> = Attr { + cfg: AttrCfg::new_type(2), + value: &[ + "Body_type", + "Butterfly_amount", + "Desintegration_amount", + "Env_Light", + "Env_reflection", + "Eyes_light_intensity_amount", + "FX_lineart_helper", + "Fall_amount", + "Firefly_amount", + "Frozen_amount", + "Fungi_amount", + "Fx_Jellifish", + "Fx_switcher_layer_0", + "Fx_switcher_layer_1", + "Fx_switcher_layer_1a", + "Fx_switcher_layer_2", + "Fx_switcher_layer_3", + "Fx_switcher_layer_4", + "Fx_switcher_layer_5", + "Fx_switcher_layer_6", + "Gold_silver_amount", + "Grow_flower_amount", + "Helmet_light", + "Helmet_type", + "Hologram_amount", + "Ladybag_amount", + "Lineart_amount", + "Melt_amount", + "Melting_glow_amount", + "Pixel_amount", + "Pose", + "Rain_amount", + "Render_noise_threshold", + "Render_resolution", + "Smoke_amount", + "Soap_bubble_intensity_amount", + "Soap_bubble_roughness_amount", + "Spring_amount", + "Underwater_fog_amount", + "Xray_body_amount", + "Xray_skeleton_particles_amount", + "background_color_random_hue", + "background_underwater_color_hue", + "dress_color_hue", + "eye_color_random_hue", + "light_reflection_mult", + "random_value", + ], +}; +*/ + +const fn default_logo_name() -> &'static str { + "solana.png" +} + +/// Condensed metadata +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RenderParams { + pub body_type: BodyType, + pub pose: Pose, + pub helmet_type: HelmetType, + pub helmet_light: HelmetLight, + pub fx0: Fx0, + pub fx1: Fx1, + pub fx1a: Fx1a, + pub fx2: Fx2, + pub fx3: Fx3, + pub fx4: Fx4, + pub fx5: Fx5, + pub fx6: Fx6, + + pub fx0_bodyoff: Option, + pub fx0_bodyoff_glass: Option, + pub body_material_variation: Option, + pub marble_variation: Option, + pub wood_variation: Option, + + pub fx_jellifish: FxJellyfish, + pub fx_lineart_helper: FxLineartHelper, + pub env_light: EnvLight, + pub env_reflection: EnvReflection, + pub light_reflection_mult: LightReflectionMult, + + pub glowing_logo: Option, + pub logo_hue: Option, + pub logo_name: Option, + + pub butterfly_amount: f64, + pub disintegration_amount: f64, + pub melt_amount: f64, + pub fall_amount: f64, + pub firefly_amount: f64, + pub frozen_amount: f64, + pub fungi_amount: f64, + pub gold_silver_amount: f64, + pub grow_flower_amount: f64, + pub hologram_amount: f64, + pub eyes_light_intensity_amount: f64, + pub ladybag_amount: f64, + pub lineart_amount: f64, + pub melting_glow_amount: f64, + pub pixel_amount: f64, + pub rain_amount: f64, + pub smoke_amount: f64, + pub soap_bubble_intensity_amount: f64, + pub soap_bubble_roughness_amount: f64, + pub spring_amount: f64, + pub underwater_fog_amount: f64, + pub xray_body_amount: f64, + pub xray_skeleton_particles_amount: f64, + + pub background_color_random_hue: f64, + pub background_underwater_color_hue: f64, + pub dress_color_hue: f64, + pub eye_color_random_hue: f64, + + pub random_value: f64, + pub wedgeindex: i64, + + pub render_noise_threshold: f64, + pub render_resolution: u32, + pub wedgeattribs: Vec, +} + +// add a empty default +impl Default for RenderParams { + fn default() -> Self { + Self { + body_type: BodyType::default(), + pose: Pose::default(), + helmet_type: HelmetType::default(), + helmet_light: HelmetLight::default(), + fx0: Fx0::default(), + fx1: Fx1::default(), + fx1a: Fx1a::default(), + fx2: Fx2::default(), + fx3: Fx3::default(), + fx4: Fx4::default(), + fx5: Fx5::default(), + fx6: Fx6::default(), + fx0_bodyoff: None, + fx0_bodyoff_glass: None, + body_material_variation: None, + marble_variation: None, + wood_variation: None, + fx_jellifish: FxJellyfish::default(), + fx_lineart_helper: FxLineartHelper::default(), + env_light: EnvLight::default(), + env_reflection: EnvReflection::default(), + light_reflection_mult: LightReflectionMult::default(), + glowing_logo: None, + logo_hue: None, + logo_name: None, + butterfly_amount: 0.0, + disintegration_amount: 0.0, + melt_amount: 0.0, + fall_amount: 0.0, + firefly_amount: 0.0, + frozen_amount: 0.0, + fungi_amount: 0.0, + gold_silver_amount: 0.0, + grow_flower_amount: 0.0, + hologram_amount: 0.0, + eyes_light_intensity_amount: 0.0, + ladybag_amount: 0.0, + lineart_amount: 0.0, + melting_glow_amount: 0.0, + pixel_amount: 0.0, + rain_amount: 0.0, + smoke_amount: 0.0, + soap_bubble_intensity_amount: 0.0, + soap_bubble_roughness_amount: 0.0, + spring_amount: 0.0, + underwater_fog_amount: 0.0, + xray_body_amount: 0.0, + xray_skeleton_particles_amount: 0.0, + background_color_random_hue: 0.0, + background_underwater_color_hue: 0.0, + dress_color_hue: 0.0, + eye_color_random_hue: 0.0, + random_value: 0.0, + wedgeindex: 0, + render_noise_threshold: 0.6, + render_resolution: 1024, + wedgeattribs: [ + "Body_type".to_owned(), + "Butterfly_amount".to_owned(), + "Desintegration_amount".to_owned(), + "Env_Light".to_owned(), + "Env_reflection".to_owned(), + "Eyes_light_intensity_amount".to_owned(), + "FX_lineart_helper".to_owned(), + "Fall_amount".to_owned(), + "Firefly_amount".to_owned(), + "Frozen_amount".to_owned(), + "Fungi_amount".to_owned(), + "Fx_Jellifish".to_owned(), + "Fx_switcher_layer_0".to_owned(), + "Fx_switcher_layer_1".to_owned(), + "Fx_switcher_layer_1a".to_owned(), + "Fx_switcher_layer_2".to_owned(), + "Fx_switcher_layer_3".to_owned(), + "Fx_switcher_layer_4".to_owned(), + "Fx_switcher_layer_5".to_owned(), + "Fx_switcher_layer_6".to_owned(), + "Gold_silver_amount".to_owned(), + "Grow_flower_amount".to_owned(), + "Helmet_light".to_owned(), + "Helmet_type".to_owned(), + "Hologram_amount".to_owned(), + "Ladybag_amount".to_owned(), + "Lineart_amount".to_owned(), + "Melt_amount".to_owned(), + "Melting_glow_amount".to_owned(), + "Pixel_amount".to_owned(), + "Pose".to_owned(), + "Rain_amount".to_owned(), + "Render_noise_threshold".to_owned(), + "Render_resolution".to_owned(), + "Smoke_amount".to_owned(), + "Soap_bubble_intensity_amount".to_owned(), + "Soap_bubble_roughness_amount".to_owned(), + "Spring_amount".to_owned(), + "Underwater_fog_amount".to_owned(), + "Xray_body_amount".to_owned(), + "Xray_skeleton_particles_amount".to_owned(), + "background_color_random_hue".to_owned(), + "background_underwater_color_hue".to_owned(), + "dress_color_hue".to_owned(), + "eye_color_random_hue".to_owned(), + "light_reflection_mult".to_owned(), + "random_value".to_owned(), + ] + .into(), + } + } +} + +fn not_found(path: impl Into>) -> FromPDGError { + FromPDGError::NotFound(path.into()) +} + +fn unknown_variant(ty: &'static str, var: u32) -> FromPDGError { + FromPDGError::UnknownVariant(ty, var) +} + +#[derive(ThisError, Debug)] +#[error("{:?}", self)] +pub enum FromPDGError { + DifferentConfig(AttrCfg), + ExpectedObject, + NotFound(Cow<'static, str>), + UnknownVariant(&'static str, u32), + Json(#[from] serde_json::Error), + WrongName { + path: &'static str, + expected: &'static str, + got: String, + }, + UnexpectedValue { + path: &'static str, + expected: String, + got: String, + }, + PropertyNotFound(#[from] PropertyNotFound), +} + +impl RenderParams { + pub fn from_pdg_metadata( + m: &mut serde_json::Value, + check_human_readable: bool, + defaults: &HashMap, + ) -> Result { + fn try_get_enum>( + m: &mut serde_json::Value, + path: &'static str, + defaults: &HashMap, + ) -> Result { + let v = match m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove(path) + { + Some(json) => { + let attr = serde_json::from_value::>(json)?; + if attr.cfg != AttrCfg::new_type(0) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + attr.value.0 + } + None => serde_json::from_value( + defaults.get(path).cloned().ok_or_else(|| not_found(path))?, + )?, + }; + E::try_from(v) + } + + fn check_enum_name( + m: &mut serde_json::Value, + path: &'static str, + variant_name: &'static str, + ) -> Result<(), FromPDGError> { + let json = match m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove(path) + { + None => return Ok(()), + Some(json) => json, + }; + let attr = serde_json::from_value::>(json)?; + if attr.cfg != AttrCfg::new_type(2) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + if attr.value.0 != variant_name { + return Err(FromPDGError::WrongName { + path, + expected: variant_name, + got: attr.value.0, + }); + } + Ok(()) + } + + fn try_get_f64( + m: &mut serde_json::Value, + path: &'static str, + defaults: &HashMap, + ) -> Result { + let v = match m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove(path) + { + Some(json) => { + let attr = serde_json::from_value::>(json)?; + if attr.cfg != AttrCfg::new_type(1) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + attr.value.0 + } + None => serde_json::from_value( + defaults.get(path).cloned().ok_or_else(|| not_found(path))?, + )?, + }; + Ok(v) + } + + fn try_get_int( + m: &mut serde_json::Value, + path: &'static str, + defaults: &HashMap, + ) -> Result { + let v = match m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove(path) + { + Some(json) => { + let attr = serde_json::from_value::>(json)?; + if attr.cfg != AttrCfg::new_type(0) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + attr.value.0 + } + None => serde_json::from_value( + defaults.get(path).cloned().ok_or_else(|| not_found(path))?, + )?, + }; + Ok(v) + } + fn try_get_string( + m: &mut serde_json::Value, + path: &'static str, + defaults: &HashMap, + ) -> Result { + let v = match m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove(path) + { + Some(json) => { + let attr = serde_json::from_value::>(json)?; + if attr.cfg != AttrCfg::new_type(2) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + attr.value.0 + } + None => serde_json::from_value( + defaults.get(path).cloned().ok_or_else(|| not_found(path))?, + )?, + }; + Ok(v) + } + + fn optional(r: Result) -> Result, FromPDGError> { + match r { + Ok(t) => Ok(Some(t)), + Err(FromPDGError::NotFound(_)) => Ok(None), + Err(error) => Err(error), + } + } + + let body_type = try_get_enum::(m, "Body_type", defaults)?; + if check_human_readable { + check_enum_name(m, "Body_name", body_type.pdg_name()?)?; + } + let pose = try_get_enum::(m, "Pose", defaults)?; + if check_human_readable { + check_enum_name(m, "Pose_name", pose.pdg_name()?)?; + } + let helmet_type = try_get_enum::(m, "Helmet_type", defaults)?; + if check_human_readable { + check_enum_name(m, "Helmet_name", helmet_type.pdg_name()?)?; + } + let helmet_light = try_get_enum::(m, "Helmet_light", defaults)?; + if check_human_readable { + check_enum_name(m, "Helmet_Light_name", helmet_light.pdg_name()?)?; + } + + let fx0 = try_get_enum::(m, "Fx_switcher_layer_0", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_0", fx0.pdg_name()?)?; + } + + let fx1 = try_get_enum::(m, "Fx_switcher_layer_1", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_1", fx1.pdg_name()?)?; + } + + let fx1a = try_get_enum::(m, "Fx_switcher_layer_1a", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_1a", fx1a.pdg_name()?)?; + } + + let fx2 = try_get_enum::(m, "Fx_switcher_layer_2", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_2", fx2.pdg_name()?)?; + } + + let fx3 = try_get_enum::(m, "Fx_switcher_layer_3", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_3", fx3.pdg_name()?)?; + } + + let fx4 = try_get_enum::(m, "Fx_switcher_layer_4", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_4", fx4.pdg_name()?)?; + } + + let fx5 = try_get_enum::(m, "Fx_switcher_layer_5", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_5", fx5.pdg_name()?)?; + } + + let fx6 = try_get_enum::(m, "Fx_switcher_layer_6", defaults)?; + if check_human_readable { + check_enum_name(m, "Fx_6", fx6.pdg_name()?)?; + } + + let fx0_bodyoff = optional(try_get_enum::( + m, + "Fx_bodyoff_layer_0_1_1a", + defaults, + ))?; + let fx0_bodyoff_glass = optional(try_get_enum::( + m, + "Fx_bodyoff_layer_0_1_1a_glass", + defaults, + ))?; + + let body_material_variation = optional(try_get_enum::( + m, + "Body_material_variation", + defaults, + ))?; + + let marble_variation = optional(try_get_enum::( + m, + "Marble_variation", + defaults, + ))?; + + let wood_variation = + optional(try_get_enum::(m, "Wood_variation", defaults))?; + + let fx_jellifish = try_get_enum::(m, "Fx_Jellifish", defaults)?; + if check_human_readable { + check_enum_name(m, "Jellifish", fx_jellifish.pdg_name()?)?; + } + + let fx_lineart_helper = try_get_enum::(m, "FX_lineart_helper", defaults)?; + + let env_light = try_get_enum::(m, "Env_Light", defaults)?; + + let env_reflection = try_get_enum::(m, "Env_reflection", defaults)?; + + let light_reflection_mult = + try_get_enum::(m, "light_reflection_mult", defaults)?; + + let glowing_logo = optional(try_get_enum::(m, "Glowing_logo", defaults))?; + let logo_hue = optional(try_get_f64(m, "Logo_hue", defaults))?; + let logo_name = optional(try_get_string(m, "logo_name", defaults))?; + + let butterfly_amount = try_get_f64(m, "Butterfly_amount", defaults)?; + let disintegration_amount = try_get_f64(m, "Desintegration_amount", defaults)?; + let melt_amount = try_get_f64(m, "Melt_amount", defaults)?; + let fall_amount = try_get_f64(m, "Fall_amount", defaults)?; + let firefly_amount = try_get_f64(m, "Firefly_amount", defaults)?; + let frozen_amount = try_get_f64(m, "Frozen_amount", defaults)?; + let fungi_amount = try_get_f64(m, "Fungi_amount", defaults)?; + let gold_silver_amount = try_get_f64(m, "Gold_silver_amount", defaults)?; + let grow_flower_amount = try_get_f64(m, "Grow_flower_amount", defaults)?; + let hologram_amount = try_get_f64(m, "Hologram_amount", defaults)?; + let eyes_light_intensity_amount = try_get_f64(m, "Eyes_light_intensity_amount", defaults)?; + let ladybag_amount = try_get_f64(m, "Ladybag_amount", defaults)?; + let lineart_amount = try_get_f64(m, "Lineart_amount", defaults)?; + let melting_glow_amount = try_get_f64(m, "Melting_glow_amount", defaults)?; + let pixel_amount = try_get_f64(m, "Pixel_amount", defaults)?; + let rain_amount = try_get_f64(m, "Rain_amount", defaults)?; + let smoke_amount = try_get_f64(m, "Smoke_amount", defaults)?; + let soap_bubble_intensity_amount = + try_get_f64(m, "Soap_bubble_intensity_amount", defaults)?; + let soap_bubble_roughness_amount = + try_get_f64(m, "Soap_bubble_roughness_amount", defaults)?; + let spring_amount = try_get_f64(m, "Spring_amount", defaults)?; + let underwater_fog_amount = try_get_f64(m, "Underwater_fog_amount", defaults)?; + let xray_body_amount = try_get_f64(m, "Xray_body_amount", defaults)?; + let xray_skeleton_particles_amount = + try_get_f64(m, "Xray_skeleton_particles_amount", defaults)?; + + let background_color_random_hue = try_get_f64(m, "background_color_random_hue", defaults)?; + let background_underwater_color_hue = + try_get_f64(m, "background_underwater_color_hue", defaults)?; + let dress_color_hue = try_get_f64(m, "dress_color_hue", defaults)?; + let eye_color_random_hue = try_get_f64(m, "eye_color_random_hue", defaults)?; + + let random_value = try_get_f64(m, "random_value", defaults)?; + + let wedgeindex = try_get_int::(m, "wedgeindex", defaults)?; + + let render_noise_threshold = try_get_f64(m, "Render_noise_threshold", defaults)?; + let render_resolution = try_get_int::(m, "Render_resolution", defaults)?; + + /* + fn check_int( + m: &mut serde_json::Value, + path: &'static str, + expected: I, + ) -> Result<(), FromPDGError> { + let got = try_get_int::(m, path)?; + if got != expected { + Err(FromPDGError::UnexpectedValue { + path, + expected: expected.to_string(), + got: got.to_string(), + }) + } else { + Ok(()) + } + } + + let split = check_int::(m, "split", DEFAULT_SPLIT)?; + check_int::(m, "wedgecount", DEFAULT_WEDGECOUNT)?; + check_int::(m, "wedgenum", DEFAULT_WEDGENUM)?; + check_int::(m, "wedgetotal", DEFAULT_WEDGETOTAL)?; + + let wedgeattribs = { + let json = m + .as_object_mut() + .ok_or_else(|| FromPDGError::ExpectedObject)? + .remove("wedgeattribs") + .ok_or_else(|| not_found("wedgeattribs"))?; + let attr = serde_json::from_value::>>(json)?; + if attr.cfg != AttrCfg::new_type(2) { + return Err(FromPDGError::DifferentConfig(attr.cfg)); + } + attr.value + }; + if wedgeattribs != DEFAULT_WEDGEATTRIBS.value { + return Err(FromPDGError::UnexpectedValue { + path: "wedgeattribs", + expected: format!("{:?}", DEFAULT_WEDGEATTRIBS), + got: format!("{:?}", wedgeattribs), + }); + } + */ + + Ok(Self { + body_type, + pose, + helmet_type, + helmet_light, + fx0, + fx1, + fx1a, + fx2, + fx3, + fx4, + fx5, + fx6, + fx0_bodyoff, + fx0_bodyoff_glass, + body_material_variation, + marble_variation, + wood_variation, + fx_jellifish, + fx_lineart_helper, + env_light, + env_reflection, + light_reflection_mult, + glowing_logo, + logo_hue, + logo_name, + butterfly_amount, + disintegration_amount, + melt_amount, + fall_amount, + firefly_amount, + frozen_amount, + fungi_amount, + gold_silver_amount, + grow_flower_amount, + hologram_amount, + eyes_light_intensity_amount, + ladybag_amount, + lineart_amount, + melting_glow_amount, + pixel_amount, + rain_amount, + smoke_amount, + soap_bubble_intensity_amount, + soap_bubble_roughness_amount, + spring_amount, + underwater_fog_amount, + xray_body_amount, + xray_skeleton_particles_amount, + background_color_random_hue, + background_underwater_color_hue, + dress_color_hue, + eye_color_random_hue, + random_value, + wedgeindex, + render_noise_threshold, + render_resolution, + ..<_>::default() + }) + } + + pub fn to_pdg_metadata(&self, human_readable: bool) -> Result { + fn push_string_attr( + m: &mut serde_json::Map, + path: &str, + value: &str, + ) { + m.insert( + path.to_owned(), + serde_json::to_value(Attr::<(String,)> { + cfg: AttrCfg::new_type(2), + value: (value.to_owned(),), + }) + .unwrap(), + ); + } + + fn push_string_attr_no_array( + m: &mut serde_json::Map, + path: &str, + value: &str, + ) { + m.insert( + path.to_owned(), + serde_json::to_value(Attr:: { + cfg: AttrCfg::new_type(2), + value: value.to_owned(), + }) + .unwrap(), + ); + } + + /* + fn push_string_array_attr( + m: &mut serde_json::Map, + path: &str, + value: &[String], + ) { + m.insert( + path.to_owned(), + serde_json::to_value(Attr::<(Vec,)> { + cfg: AttrCfg::new_type(2), + value: (value.to_vec(),), + }) + .unwrap(), + ); + } + */ + + fn push_int_attr( + m: &mut serde_json::Map, + path: &str, + value: impl Into, + ) { + m.insert( + path.to_owned(), + serde_json::to_value(Attr::<(i64,)> { + cfg: AttrCfg::new_type(0), + value: (value.into(),), + }) + .unwrap(), + ); + } + + fn push_float_attr( + m: &mut serde_json::Map, + path: &str, + value: f64, + ) { + m.insert( + path.to_owned(), + serde_json::to_value(Attr::<(f64,)> { + cfg: AttrCfg::new_type(1), + value: (value,), + }) + .unwrap(), + ); + } + + let Self { + body_type, + pose, + helmet_type, + helmet_light, + fx0, + fx1, + fx1a, + fx2, + fx3, + fx4, + fx5, + fx6, + fx0_bodyoff, + fx0_bodyoff_glass, + body_material_variation, + marble_variation, + wood_variation, + fx_jellifish, + fx_lineart_helper, + env_light, + env_reflection, + light_reflection_mult, + glowing_logo, + logo_hue, + logo_name, + butterfly_amount, + disintegration_amount, + melt_amount, + fall_amount, + firefly_amount, + frozen_amount, + fungi_amount, + gold_silver_amount, + grow_flower_amount, + hologram_amount, + eyes_light_intensity_amount, + ladybag_amount, + lineart_amount, + melting_glow_amount, + pixel_amount, + rain_amount, + smoke_amount, + soap_bubble_intensity_amount, + soap_bubble_roughness_amount, + spring_amount, + underwater_fog_amount, + xray_body_amount, + xray_skeleton_particles_amount, + background_color_random_hue, + background_underwater_color_hue, + dress_color_hue, + eye_color_random_hue, + random_value, + wedgeindex, + render_noise_threshold, + render_resolution, + wedgeattribs: _, + } = &self; + + let mut m = serde_json::Map::new(); + + // push_string_array_attr(&mut m, "wedgeattribs", &wedgeattribs[..]); + + push_int_attr(&mut m, "Body_type", *body_type as u32); + if human_readable { + push_string_attr(&mut m, "Body_name", body_type.pdg_name()?); + } + + push_int_attr(&mut m, "Pose", *pose as u32); + if human_readable { + push_string_attr(&mut m, "Pose_name", pose.pdg_name()?); + } + + push_int_attr(&mut m, "Helmet_type", *helmet_type as u32); + if human_readable { + push_string_attr(&mut m, "Helmet_name", helmet_type.pdg_name()?); + } + + push_int_attr(&mut m, "Helmet_light", *helmet_light as u32); + if human_readable { + push_string_attr(&mut m, "Helmet_Light_name", helmet_light.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_0", *fx0 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_0", fx0.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_1", *fx1 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_1", fx1.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_1a", *fx1a as u32); + if human_readable { + push_string_attr(&mut m, "Fx_1a", fx1a.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_2", *fx2 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_2", fx2.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_3", *fx3 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_3", fx3.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_4", *fx4 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_4", fx4.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_5", *fx5 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_5", fx5.pdg_name()?); + } + + push_int_attr(&mut m, "Fx_switcher_layer_6", *fx6 as u32); + if human_readable { + push_string_attr(&mut m, "Fx_6", fx6.pdg_name()?); + } + + { + let fx0_bodyoff = fx0_bodyoff.unwrap_or_default(); + push_int_attr(&mut m, "Fx_bodyoff_layer_0_1_1a", fx0_bodyoff as u32); + if human_readable { + push_string_attr(&mut m, "Fx_bodyoff", fx0_bodyoff.pdg_name()?); + } + } + + { + let fx0_bodyoff_glass = fx0_bodyoff_glass.unwrap_or_default(); + // Doesn't have human readable attribute + push_int_attr( + &mut m, + "Fx_bodyoff_layer_0_1_1a_glass", + fx0_bodyoff_glass as u32, + ); + } + + { + let body_material_variation = body_material_variation.unwrap_or_default(); + // Doesn't have human readable attribute + push_int_attr( + &mut m, + "Body_material_variation", + body_material_variation as u32, + ); + } + + { + let marble_variation = marble_variation.unwrap_or_default(); + // Doesn't have human readable attribute + push_int_attr(&mut m, "Marble_variation", marble_variation as u32); + } + + { + let wood_variation = wood_variation.unwrap_or_default(); + // Doesn't have human readable attribute + push_int_attr(&mut m, "Wood_variation", wood_variation as u32); + } + + push_int_attr(&mut m, "Fx_Jellifish", *fx_jellifish as u32); + if human_readable { + push_string_attr(&mut m, "Jellifish", fx_jellifish.pdg_name()?); + } + + push_int_attr(&mut m, "FX_lineart_helper", *fx_lineart_helper as u32); + + push_int_attr(&mut m, "Env_Light", *env_light as u32); + + push_int_attr(&mut m, "Env_reflection", *env_reflection as u32); + + push_int_attr( + &mut m, + "light_reflection_mult", + *light_reflection_mult as u32, + ); + + { + let glowing_logo = glowing_logo.unwrap_or_default(); + push_int_attr(&mut m, "Glowing_logo", glowing_logo as u32); + } + { + let logo_hue = logo_hue.unwrap_or_default(); + push_float_attr(&mut m, "Logo_hue", logo_hue); + } + + { + let logo_name = logo_name + .as_ref() + .map(String::as_str) + .unwrap_or(default_logo_name()); + push_string_attr_no_array(&mut m, "logo_name", logo_name); + } + + push_float_attr(&mut m, "Butterfly_amount", *butterfly_amount); + push_float_attr(&mut m, "Desintegration_amount", *disintegration_amount); + push_float_attr(&mut m, "Melt_amount", *melt_amount); + push_float_attr(&mut m, "Fall_amount", *fall_amount); + push_float_attr(&mut m, "Firefly_amount", *firefly_amount); + push_float_attr(&mut m, "Frozen_amount", *frozen_amount); + push_float_attr(&mut m, "Fungi_amount", *fungi_amount); + push_float_attr(&mut m, "Gold_silver_amount", *gold_silver_amount); + push_float_attr(&mut m, "Grow_flower_amount", *grow_flower_amount); + push_float_attr(&mut m, "Hologram_amount", *hologram_amount); + push_float_attr( + &mut m, + "Eyes_light_intensity_amount", + *eyes_light_intensity_amount, + ); + push_float_attr(&mut m, "Ladybag_amount", *ladybag_amount); + push_float_attr(&mut m, "Lineart_amount", *lineart_amount); + push_float_attr(&mut m, "Melting_glow_amount", *melting_glow_amount); + push_float_attr(&mut m, "Pixel_amount", *pixel_amount); + push_float_attr(&mut m, "Rain_amount", *rain_amount); + push_float_attr(&mut m, "Smoke_amount", *smoke_amount); + push_float_attr( + &mut m, + "Soap_bubble_intensity_amount", + *soap_bubble_intensity_amount, + ); + push_float_attr( + &mut m, + "Soap_bubble_roughness_amount", + *soap_bubble_roughness_amount, + ); + push_float_attr(&mut m, "Spring_amount", *spring_amount); + push_float_attr(&mut m, "Underwater_fog_amount", *underwater_fog_amount); + push_float_attr(&mut m, "Xray_body_amount", *xray_body_amount); + push_float_attr( + &mut m, + "Xray_skeleton_particles_amount", + *xray_skeleton_particles_amount, + ); + push_float_attr( + &mut m, + "background_color_random_hue", + *background_color_random_hue, + ); + push_float_attr( + &mut m, + "background_underwater_color_hue", + *background_underwater_color_hue, + ); + push_float_attr(&mut m, "dress_color_hue", *dress_color_hue); + push_float_attr(&mut m, "eye_color_random_hue", *eye_color_random_hue); + push_float_attr(&mut m, "random_value", *random_value); + + push_int_attr(&mut m, "wedgeindex", *wedgeindex); + + push_float_attr(&mut m, "Render_noise_threshold", *render_noise_threshold); + push_int_attr(&mut m, "Render_resolution", *render_resolution); + + /* + push_int_attr(&mut m, "split", DEFAULT_SPLIT); + push_int_attr(&mut m, "wedgecount", DEFAULT_WEDGECOUNT); + push_int_attr(&mut m, "wedgenum", DEFAULT_WEDGENUM); + push_int_attr(&mut m, "wedgetotal", DEFAULT_WEDGETOTAL); + + m.insert( + "wedgeattribs".to_owned(), + serde_json::to_value(&DEFAULT_WEDGEATTRIBS).unwrap(), + ); + */ + + Ok(m.into()) + } + + pub fn correction(&mut self) { + fn correct_percent_value(v: &mut f64) { + *v = (*v).clamp(0.0, 100.0); + } + correct_percent_value(&mut self.butterfly_amount); + correct_percent_value(&mut self.disintegration_amount); + correct_percent_value(&mut self.melt_amount); + correct_percent_value(&mut self.fall_amount); + correct_percent_value(&mut self.firefly_amount); + correct_percent_value(&mut self.frozen_amount); + correct_percent_value(&mut self.fungi_amount); + correct_percent_value(&mut self.gold_silver_amount); + correct_percent_value(&mut self.grow_flower_amount); + correct_percent_value(&mut self.hologram_amount); + correct_percent_value(&mut self.eyes_light_intensity_amount); + correct_percent_value(&mut self.ladybag_amount); + correct_percent_value(&mut self.lineart_amount); + correct_percent_value(&mut self.melting_glow_amount); + correct_percent_value(&mut self.pixel_amount); + correct_percent_value(&mut self.rain_amount); + correct_percent_value(&mut self.smoke_amount); + correct_percent_value(&mut self.soap_bubble_intensity_amount); + correct_percent_value(&mut self.soap_bubble_roughness_amount); + correct_percent_value(&mut self.spring_amount); + correct_percent_value(&mut self.underwater_fog_amount); + correct_percent_value(&mut self.xray_body_amount); + correct_percent_value(&mut self.xray_skeleton_particles_amount); + + fn correct_hue_value(v: &mut f64) { + *v %= 360.0; + if *v < 0.0 { + *v += 360.0; + } + } + correct_hue_value(&mut self.background_color_random_hue); + correct_hue_value(&mut self.background_underwater_color_hue); + correct_hue_value(&mut self.dress_color_hue); + correct_hue_value(&mut self.eye_color_random_hue); + } +} + +macro_rules! impl_try_from_u32 { + ($t:ident) => { + impl std::convert::TryFrom for $t { + type Error = FromPDGError; + fn try_from(value: u32) -> Result { + Self::from_repr(value) + .ok_or_else(|| unknown_variant(std::any::type_name::(), value)) + } + } + }; +} + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Hash, +)] +#[repr(u32)] +pub enum BodyType { + #[strum(props(PDGName = "Spacesuit"))] + #[strum(props(MetaplexName = "Spacesuit"))] + #[strum(props(weight = "20"))] + #[default] + Spacesuit = 0, + #[strum(props(PDGName = "Sci Fi Police"))] + #[strum(props(MetaplexName = "Sci-Fi Police"))] + #[strum(props(weight = "15"))] + SciFiPolice = 1, + #[strum(props(PDGName = "Diver"))] + #[strum(props(MetaplexName = "Diver"))] + #[strum(props(weight = "20"))] + Diver = 2, + #[strum(props(PDGName = "Cyborg"))] + #[strum(props(MetaplexName = "Cyborg"))] + #[strum(props(weight = "5"))] + Cyborg = 3, + #[strum(props(PDGName = "Sci Fi sport woman"))] + #[strum(props(MetaplexName = "Sci-Fi Sport Woman"))] + #[strum(props(weight = "20"))] + SciFiSportWoman = 4, + #[strum(props(PDGName = "Sci Fi Exo costume"))] + #[strum(props(MetaplexName = "Sci-Fi Exo Costume"))] + #[strum(props(weight = "20"))] + SciFiExoCostume = 5, +} + +impl_try_from_u32!(BodyType); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Hash, +)] +#[repr(u32)] +pub enum HelmetType { + #[strum(props(PDGName = "Spacesuit"))] + #[strum(props(MetaplexName = "Spacesuit"))] + #[default] + Spacesuit = 0, + #[strum(props(PDGName = "Diver"))] + #[strum(props(MetaplexName = "Diver"))] + Diver = 1, + #[strum(props(PDGName = "Diving Old"))] + #[strum(props(MetaplexName = "Diving Old"))] + DivingOld = 2, + #[strum(props(PDGName = "Cyborg"))] + #[strum(props(MetaplexName = "Cyborg"))] + Cyborg = 3, + #[strum(props(PDGName = "Pilot"))] + #[strum(props(MetaplexName = "Pilot"))] + Pilot = 4, + #[strum(props(PDGName = "Pilot Old"))] + #[strum(props(MetaplexName = "Pilot Old"))] + PilotOld = 5, + #[strum(props(PDGName = "Steam punk"))] + #[strum(props(MetaplexName = "SteamPunk"))] + Steampunk = 6, + #[strum(props(PDGName = "Knight1"))] + #[strum(props(MetaplexName = "Knight 1"))] + Knight1 = 7, + #[strum(props(PDGName = "Knight2"))] + #[strum(props(MetaplexName = "Knight 2"))] + Knight2 = 8, + #[strum(props(PDGName = "Space mercury"))] + #[strum(props(MetaplexName = "Space Mercury"))] + SpaceMercury = 9, + #[strum(props(PDGName = "Space soviet"))] + #[strum(props(MetaplexName = "Space Soviet"))] + SpaceSoviet = 10, + #[strum(props(PDGName = "Sci_Fi_sport_woman"))] + #[strum(props(MetaplexName = "Sci-Fi Sport Woman"))] + SciFiSportWoman = 11, + #[strum(props(PDGName = "Iron centaur"))] + #[strum(props(MetaplexName = "Iron Centaur"))] + IronCentaur = 12, + #[strum(props(PDGName = "Sci Fi Exo costume"))] + #[strum(props(MetaplexName = "Sci-Fi Exo Costume"))] + SciFiExoCostume = 13, + #[strum(props(PDGName = "Ghouls"))] + #[strum(props(MetaplexName = "Ghouls"))] + Ghouls = 14, + #[strum(props(PDGName = "Gladiator"))] + #[strum(props(MetaplexName = "Gladiator"))] + Gladiator = 15, + #[strum(props(PDGName = "Sci Fi racer helmet"))] + #[strum(props(MetaplexName = "Sci-Fi Racer Helmet"))] + SciFiRacerHelmet = 16, + #[strum(props(PDGName = "Samurai"))] + #[strum(props(MetaplexName = "Samurai"))] + Samurai = 17, +} + +impl_try_from_u32!(HelmetType); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Display, + Default, + Hash, +)] +#[repr(u32)] +pub enum Pose { + #[strum(props(PDGName = "Surprised"))] + #[strum(props(MetaplexName = "Susprised"))] + Surprised = 0, + #[strum(props(PDGName = "Really?"))] + #[strum(props(MetaplexName = "Really?"))] + Really = 1, + #[strum(props(PDGName = "Overconfident"))] + #[strum(props(MetaplexName = "Overconfident"))] + Overconfident = 2, + #[strum(props(PDGName = "Instigate"))] + #[strum(props(MetaplexName = "Instigate"))] + Instigate = 3, + #[strum(props(PDGName = "Inspired"))] + #[strum(props(MetaplexName = "Inspired"))] + Inspired = 4, + #[strum(props(PDGName = "Listening"))] + #[strum(props(MetaplexName = "Listening"))] + Listening = 5, + #[strum(props(PDGName = "Side eye"))] + #[strum(props(MetaplexName = "Side Eye"))] + SideEye = 6, + #[strum(props(PDGName = "Mugshot"))] + #[strum(props(MetaplexName = "Mugshot"))] + Mugshot = 7, + #[strum(props(PDGName = "OG (original NFT)"))] + #[strum(props(MetaplexName = "OG (Original NFT)"))] + #[default] + Original = 8, + #[strum(props(PDGName = "Suspicious"))] + #[strum(props(MetaplexName = "Suspicious"))] + Suspicious = 9, + #[strum(props(PDGName = "Thinking"))] + #[strum(props(MetaplexName = "Thinking"))] + Thinking = 10, + #[strum(props(PDGName = "Busy"))] + #[strum(props(MetaplexName = "Busy"))] + Busy = 11, + #[strum(props(PDGName = "Ready"))] + #[strum(props(MetaplexName = "Ready"))] + Ready = 12, + #[strum(props(PDGName = "Stare"))] + #[strum(props(MetaplexName = "Stare"))] + Stare = 13, + #[strum(props(PDGName = "Introspection"))] + #[strum(props(MetaplexName = "Introspection"))] + Introspection = 14, + #[strum(props(PDGName = "Look up left"))] + #[strum(props(MetaplexName = "Look Up Left"))] + LookUpLeft = 15, + #[strum(props(PDGName = "Look up"))] + #[strum(props(MetaplexName = "Look Up"))] + LookUp = 16, + #[strum(props(PDGName = "Look up right"))] + #[strum(props(MetaplexName = "Look Up Right"))] + LookUpRight = 17, + #[strum(props(PDGName = "Look left"))] + #[strum(props(MetaplexName = "Look Left"))] + LookLeft = 18, + #[strum(props(PDGName = "Default"))] + #[strum(props(MetaplexName = "Default"))] + Default = 19, + #[strum(props(PDGName = "Look right"))] + #[strum(props(MetaplexName = "Look Right"))] + LookRight = 20, + #[strum(props(PDGName = "Look down left"))] + #[strum(props(MetaplexName = "Look Down Left"))] + LookDownLeft = 21, + #[strum(props(PDGName = "Look down"))] + #[strum(props(MetaplexName = "Look Down"))] + LookDown = 22, + #[strum(props(PDGName = "Look down right"))] + #[strum(props(MetaplexName = "Look Down Right"))] + LookDownRight = 23, +} + +impl_try_from_u32!(Pose); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum HelmetLight { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "50"))] + #[default] + Off = 0, + #[strum(props(PDGName = "Dots"))] + #[strum(props(MetaplexName = "Dots"))] + #[strum(props(weight = "25"))] + Dots = 1, + #[strum(props(PDGName = "Glowing eyes"))] + #[strum(props(MetaplexName = "Glowing Eyes"))] + #[strum(props(weight = "15"))] + GlowingEyes = 2, + #[strum(props(PDGName = "Solana"))] + #[strum(props(MetaplexName = "Solana"))] + #[strum(props(weight = "10"))] + Solana = 3, +} + +impl_try_from_u32!(HelmetLight); + +impl HelmetLight { + pub fn some_lights(rng: &mut R) -> Self { + *[ + HelmetLight::Dots, + HelmetLight::GlowingEyes, + HelmetLight::Solana, + ] + .choose(rng) + .unwrap() + } +} + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx0 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "40"))] + #[default] + No = 0, + #[strum(props(PDGName = "Marble"))] + #[strum(props(MetaplexName = "Marble"))] + #[strum(props(weight = "10"))] + Marble = 1, + #[strum(props(PDGName = "Pixel"))] + #[strum(props(MetaplexName = "Pixel"))] + #[strum(props(weight = "10"))] + Pixel = 2, + #[strum(props(PDGName = "Lineart base"))] + #[strum(props(MetaplexName = "Lineart Base"))] + #[strum(props(weight = "10"))] + LineartBase = 3, + #[strum(props(PDGName = "Wood"))] + #[strum(props(MetaplexName = "Wood"))] + #[strum(props(weight = "10"))] + Wood = 4, + #[strum(props(PDGName = "Hologram"))] + #[strum(props(MetaplexName = "Hologram"))] + #[strum(props(weight = "5"))] + Hologram = 5, + #[strum(props(PDGName = "Xray"))] + #[strum(props(MetaplexName = "X-ray"))] + #[strum(props(weight = "5"))] + Xray = 6, + #[strum(props(PDGName = "Soap bubble"))] + #[strum(props(MetaplexName = "Soap Bubble"))] + #[strum(props(weight = "10"))] + SoapBubble = 7, +} + +impl_try_from_u32!(Fx0); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx1 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "80"))] + #[default] + No = 0, + #[strum(props(PDGName = "Melted"))] + #[strum(props(MetaplexName = "Melted"))] + #[strum(props(weight = "5"))] + Melted = 1, + #[strum(props(PDGName = "Disintegration"))] + #[strum(props(MetaplexName = "Disintegration"))] + #[strum(props(weight = "15"))] + Disintegration = 2, +} + +impl_try_from_u32!(Fx1); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx1a { + #[strum(props(PDGName = "No"))] + #[default] + #[strum(props(weight = "0"))] + No = 0, + #[strum(props(PDGName = "Lineart minimalistic"))] + #[strum(props(weight = "50"))] + LineartMinimalistic = 1, + #[strum(props(PDGName = "Lineart Heavy"))] + #[strum(props(weight = "50"))] + LineartHeavy = 2, +} + +impl_try_from_u32!(Fx1a); + +impl Fx1a { + pub fn none_or_minimal(rng: &mut R) -> Self { + *[Fx1a::No, Fx1a::LineartMinimalistic].choose(rng).unwrap() + } +} + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx2 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "45"))] + #[default] + No = 0, + #[strum(props(PDGName = "Butterflies"))] + #[strum(props(MetaplexName = "Butterfly"))] + #[strum(props(weight = "10"))] + Butterflies = 1, + #[strum(props(PDGName = "Underwater"))] + #[strum(props(MetaplexName = "Underwater"))] + #[strum(props(weight = "5"))] + Underwater = 2, + #[strum(props(PDGName = "Fireflyies"))] + #[strum(props(MetaplexName = "Firefly"))] + #[strum(props(weight = "10"))] + Fireflyies = 3, + #[strum(props(PDGName = "Fall"))] + #[strum(props(MetaplexName = "Fall"))] + #[strum(props(weight = "10"))] + Fall = 4, + #[strum(props(PDGName = "Ladybag"))] + #[strum(props(MetaplexName = "Ladybug"))] + #[strum(props(weight = "10"))] + Ladybag = 5, + #[strum(props(PDGName = "Spring"))] + #[strum(props(MetaplexName = "Spring"))] + #[strum(props(weight = "10"))] + Spring = 6, +} + +impl_try_from_u32!(Fx2); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx3 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[default] + No = 0, + #[strum(props(PDGName = "Smoke"))] + #[strum(props(MetaplexName = "Yes"))] + Smoke = 1, +} + +impl_try_from_u32!(Fx3); + +impl Fx3 { + pub fn smoke_or_not(rng: &mut R) -> Self { + *[Fx3::No, Fx3::Smoke].choose(rng).unwrap() + } +} + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx4 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "75"))] + #[default] + No = 0, + #[strum(props(PDGName = "Frozen"))] + #[strum(props(MetaplexName = "Frozen"))] + #[strum(props(weight = "10"))] + Frozen = 1, + #[strum(props(PDGName = "Rain"))] + #[strum(props(MetaplexName = "Rain"))] + #[strum(props(weight = "15"))] + Rain = 2, +} + +impl_try_from_u32!(Fx4); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx5 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "70"))] + #[default] + No = 0, + #[strum(props(PDGName = "Fungi"))] + #[strum(props(MetaplexName = "Fungi"))] + #[strum(props(weight = "15"))] + Fungi = 1, + #[strum(props(PDGName = "GrowFlower"))] + #[strum(props(MetaplexName = "Flower"))] + #[strum(props(weight = "15"))] + GrowFlower = 2, +} + +impl_try_from_u32!(Fx5); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum Fx6 { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No"))] + #[strum(props(weight = "69"))] + #[default] + No = 0, + #[strum(props(PDGName = "Gold"))] + #[strum(props(MetaplexName = "Gold"))] + #[strum(props(weight = "5"))] + Gold = 1, + #[strum(props(PDGName = "Silver"))] + #[strum(props(MetaplexName = "Silver"))] + #[strum(props(weight = "8"))] + Silver = 2, + #[strum(props(PDGName = "Rose Gold"))] + #[strum(props(MetaplexName = "Rose Gold"))] + #[strum(props(weight = "3"))] + RoseGold = 3, + #[strum(props(PDGName = "Copper"))] + #[strum(props(MetaplexName = "Copper"))] + #[strum(props(weight = "5"))] + Copper = 4, + #[strum(props(PDGName = "Bronze"))] + #[strum(props(MetaplexName = "Bronze"))] + #[strum(props(weight = "10"))] + Bronze = 5, +} + +impl_try_from_u32!(Fx6); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum Fx0BodyOff { + #[strum(props(PDGName = "Visible"))] + #[strum(props(MetaplexName = "Visible"))] + #[default] + On = 0, + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No Body"))] + No = 1, +} + +impl_try_from_u32!(Fx0BodyOff); + +#[derive( + strum::FromRepr, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum Fx0BodyOffGlass { + #[strum(props(PDGName = "No"))] + #[strum(props(MetaplexName = "No Glass"))] + #[strum(props(weight = "0"))] + No = 0, + #[strum(props(PDGName = "On"))] + #[strum(props(MetaplexName = "Visible"))] + #[strum(props(weight = "100"))] + #[default] + On = 1, +} + +impl_try_from_u32!(Fx0BodyOffGlass); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum BodyMaterialVariations { + #[default] + #[strum(props(weight = "50"))] + StandardTextures = 0, + #[strum(props(weight = "5"))] + Stripes = 1, + #[strum(props(weight = "10"))] + Dots = 2, + #[strum(props(weight = "35"))] + Felt = 3, +} + +impl_try_from_u32!(BodyMaterialVariations); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum MarbleVariation { + #[strum(props(MetaplexName = "Grey"))] + #[strum(props(weight = "50"))] + #[default] + Zero = 0, + #[strum(props(MetaplexName = "Concrete"))] + #[strum(props(weight = "5"))] + One = 1, + #[strum(props(MetaplexName = "Layered Rock"))] + #[strum(props(weight = "10"))] + Two = 2, + #[strum(props(MetaplexName = "Limestone"))] + #[strum(props(weight = "5"))] + Three = 3, + #[strum(props(MetaplexName = "Chiseled"))] + #[strum(props(weight = "15"))] + Four = 4, + #[strum(props(MetaplexName = "Zobra"))] + #[strum(props(weight = "15"))] + Five = 5, + #[strum(props(MetaplexName = "Roman"))] + #[strum(props(weight = "15"))] + Six = 6, + #[strum(props(MetaplexName = "Seychelles"))] + #[strum(props(weight = "15"))] + Seven = 7, +} + +impl_try_from_u32!(MarbleVariation); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum WoodVariation { + #[strum(props(MetaplexName = "Decaying"))] + #[strum(props(weight = "50"))] + #[default] + Zero = 0, + #[strum(props(MetaplexName = "Maple Bark"))] + #[strum(props(weight = "6"))] + One = 1, + #[strum(props(MetaplexName = "Polynesian Carving"))] + #[strum(props(weight = "6"))] + Two = 2, + #[strum(props(MetaplexName = "Smooth Birch"))] + #[strum(props(weight = "6"))] + Three = 3, + #[strum(props(MetaplexName = "Silver Birch"))] + #[strum(props(weight = "6"))] + Four = 4, + #[strum(props(MetaplexName = "Bark"))] + #[strum(props(weight = "6"))] + Five = 5, + #[strum(props(MetaplexName = "Old Bark"))] + #[strum(props(weight = "6"))] + Six = 6, + #[strum(props(MetaplexName = "Burl Walnut"))] + #[strum(props(weight = "7"))] + Seven = 7, + #[strum(props(MetaplexName = "Walnut"))] + #[strum(props(weight = "7"))] + Eight = 8, +} + +impl_try_from_u32!(WoodVariation); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum GlowingLogo { + #[strum(props(weight = "90"))] + #[default] + No = 0, + #[strum(props(weight = "10"))] + Yes = 1, +} + +impl_try_from_u32!(GlowingLogo); + +#[derive( + strum::FromRepr, + strum::EnumProperty, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum FxJellyfish { + #[strum(props(PDGName = "No"))] + #[strum(props(weight = "80"))] + #[default] + No = 0, + #[strum(props(PDGName = "Yes"))] + #[strum(props(weight = "20"))] + Yes = 1, +} + +impl_try_from_u32!(FxJellyfish); + +#[derive( + strum::FromRepr, + strum::EnumIter, + strum::EnumProperty, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, + Display, + Hash, +)] +#[repr(u32)] +pub enum FxLineartHelper { + #[default] + #[strum(props(weight = "50"))] + Zero = 0, + #[strum(props(weight = "50"))] + One = 1, +} + +impl_try_from_u32!(FxLineartHelper); + +#[derive( + strum::FromRepr, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum EnvLight { + #[default] + Day = 0, + Night = 1, + Underwater = 2, + UnderwaterHologram = 3, +} + +impl_try_from_u32!(EnvLight); + +impl EnvLight { + pub fn day_or_night(rng: &mut R) -> Self { + *[EnvLight::Day, EnvLight::Night].choose(rng).unwrap() + } +} + +#[derive( + strum::FromRepr, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum EnvReflection { + #[default] + Off = 0, + On = 1, +} + +impl_try_from_u32!(EnvReflection); + +#[derive( + strum::FromRepr, + strum::EnumIter, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize_repr, + Deserialize_repr, + Default, +)] +#[repr(u32)] +pub enum LightReflectionMult { + #[default] + One = 1, + Two = 2, +} + +impl_try_from_u32!(LightReflectionMult); + +/* + * TODO: add this test back +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_metadata() { + let mut json = + serde_json::from_str::(include_str!("tests/123.json")).unwrap(); + let meta = RenderParams::from_pdg_metadata(&mut json, true, &<_>::default()).unwrap(); + println!("{:#}", json); + println!( + "{:#?}", + json.as_object().unwrap().keys().collect::>() + ); + dbg!(&meta); + let mut pdg = meta.to_pdg_metadata(true).unwrap(); + println!("{:#}", pdg); + let meta1 = RenderParams::from_pdg_metadata(&mut pdg, true, &<_>::default()).unwrap(); + assert_eq!(meta, meta1); + assert_eq!( + pdg.as_object().unwrap().keys().next().unwrap(), + "wedgeattribs" + ); + } +} +*/ + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn print_poses_name() { + let mut poses = BTreeMap::new(); + for pose in Pose::iter() { + poses.insert(pose as u32, pose.metaplex_name().unwrap()); + } + println!("{}", serde_json::to_string(&poses).unwrap()); + } +} diff --git a/crates/pdg-common/src/nft_metadata/pdg.rs b/crates/pdg-common/src/nft_metadata/pdg.rs new file mode 100644 index 00000000..b18a85e4 --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/pdg.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Attr { + #[serde(flatten)] + pub cfg: AttrCfg, + pub value: T, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +pub struct AttrCfg { + pub concat: bool, + pub flag: u8, + pub own: bool, + pub r#type: u8, +} + +impl AttrCfg { + pub const fn new_type(ty: u8) -> Self { + Self { + concat: false, + flag: 0, + own: false, + r#type: ty, + } + } +} diff --git a/crates/pdg-common/src/nft_metadata/tests/123.json b/crates/pdg-common/src/nft_metadata/tests/123.json new file mode 100644 index 00000000..d9cb7292 --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/tests/123.json @@ -0,0 +1 @@ +{"Body_name":{"concat":false,"flag":0,"own":false,"type":2,"value":["Spacesuit"]},"Body_type":{"concat":false,"flag":0,"own":false,"type":0,"value":[0]},"Butterfly_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[48]},"Desintegration_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[50]},"Env_Light":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Env_reflection":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Eyes_light_intensity_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"FX_lineart_helper":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Fall_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[15]},"Firefly_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[65]},"Frozen_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[40]},"Fungi_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[3]},"Fx_0":{"concat":false,"flag":0,"own":false,"type":2,"value":["Wood"]},"Fx_1":{"concat":false,"flag":0,"own":false,"type":2,"value":["No"]},"Fx_1a":{"concat":false,"flag":0,"own":false,"type":2,"value":["No"]},"Fx_2":{"concat":false,"flag":0,"own":false,"type":2,"value":["Fireflyies"]},"Fx_3":{"concat":false,"flag":0,"own":false,"type":2,"value":["Smoke"]},"Fx_4":{"concat":false,"flag":0,"own":false,"type":2,"value":["Rain"]},"Fx_5":{"concat":false,"flag":0,"own":false,"type":2,"value":["Fungi"]},"Fx_6":{"concat":false,"flag":0,"own":false,"type":2,"value":["Gold"]},"Fx_Jellifish":{"concat":false,"flag":0,"own":false,"type":0,"value":[0]},"Fx_switcher_layer_0":{"concat":false,"flag":0,"own":false,"type":0,"value":[4]},"Fx_switcher_layer_1":{"concat":false,"flag":0,"own":false,"type":0,"value":[0]},"Fx_switcher_layer_1a":{"concat":false,"flag":0,"own":false,"type":0,"value":[0]},"Fx_switcher_layer_2":{"concat":false,"flag":0,"own":false,"type":0,"value":[3]},"Fx_switcher_layer_3":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Fx_switcher_layer_4":{"concat":false,"flag":0,"own":false,"type":0,"value":[2]},"Fx_switcher_layer_5":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Fx_switcher_layer_6":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"Gold_silver_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[2]},"Grow_flower_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[20]},"Helmet_Light_name":{"concat":false,"flag":0,"own":false,"type":2,"value":["Solana"]},"Helmet_light":{"concat":false,"flag":0,"own":false,"type":0,"value":[3]},"Helmet_name":{"concat":false,"flag":0,"own":false,"type":2,"value":["Knight2"]},"Helmet_type":{"concat":false,"flag":0,"own":false,"type":0,"value":[8]},"Hologram_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"Houdini_version":{"concat":false,"flag":0,"own":false,"type":2,"value":["19.5.303"]},"Jellifish":{"concat":false,"flag":0,"own":false,"type":2,"value":["No"]},"Ladybag_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[63]},"Lineart_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[80]},"Melt_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[20]},"Melting_glow_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"PDG_Version":{"concat":false,"flag":0,"own":false,"type":0,"value":[6]},"Pixel_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[45]},"Pose":{"concat":false,"flag":0,"own":false,"type":0,"value":[2]},"Pose_name":{"concat":false,"flag":0,"own":false,"type":2,"value":["Overconfident"]},"Rain_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[10]},"Render_noise_threshold":{"concat":false,"flag":0,"own":false,"type":1,"value":[0.6]},"Render_resolution":{"concat":false,"flag":0,"own":false,"type":0,"value":[1024]},"Smoke_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[33]},"Soap_bubble_intensity_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[50]},"Soap_bubble_roughness_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"Spring_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[65]},"Underwater_fog_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[50]},"Xray_body_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"Xray_skeleton_particles_amount":{"concat":false,"flag":0,"own":false,"type":1,"value":[100]},"__pdg_addedfiles":{"concat":false,"flag":9,"own":true,"type":3,"value":[{"data":"/home/ubuntu/hip/output/spaceoperator.cf095483-4305-48fb-8782-7d65a5175782.csv","hash":1681807511,"own":true,"size":0,"tag":"file/csv","type":0}]},"__pdg_outputfiles":{"concat":false,"flag":8,"own":true,"type":3,"value":[{"data":"/home/ubuntu/hip/output/spaceoperator.cf095483-4305-48fb-8782-7d65a5175782.csv","hash":1681807511,"own":true,"size":0,"tag":"file/csv","type":0}]},"background_color_random_hue":{"concat":false,"flag":0,"own":false,"type":1,"value":[119.83290567179495]},"background_underwater_color_hue":{"concat":false,"flag":0,"own":false,"type":1,"value":[38.8]},"dress_color_hue":{"concat":false,"flag":0,"own":false,"type":1,"value":[88.01364355382239]},"eye_color_random_hue":{"concat":false,"flag":0,"own":false,"type":1,"value":[121.06077990222991]},"light_reflection_mult":{"concat":false,"flag":0,"own":false,"type":0,"value":[2]},"random_value":{"concat":false,"flag":0,"own":false,"type":1,"value":[15966.23967219699]},"request_uuid":{"concat":false,"flag":0,"own":false,"type":2,"value":["cf095483-4305-48fb-8782-7d65a5175782"]},"split":{"concat":false,"flag":0,"own":false,"type":0,"value":[1]},"wedgeattribs":{"concat":false,"flag":0,"own":false,"type":2,"value":["Body_type","Butterfly_amount","Desintegration_amount","Env_Light","Env_reflection","Eyes_light_intensity_amount","FX_lineart_helper","Fall_amount","Firefly_amount","Frozen_amount","Fungi_amount","Fx_Jellifish","Fx_switcher_layer_0","Fx_switcher_layer_1","Fx_switcher_layer_1a","Fx_switcher_layer_2","Fx_switcher_layer_3","Fx_switcher_layer_4","Fx_switcher_layer_5","Fx_switcher_layer_6","Gold_silver_amount","Grow_flower_amount","Helmet_light","Helmet_type","Hologram_amount","Ladybag_amount","Lineart_amount","Melt_amount","Melting_glow_amount","Pixel_amount","Pose","Rain_amount","Render_noise_threshold","Render_resolution","Smoke_amount","Soap_bubble_intensity_amount","Soap_bubble_roughness_amount","Spring_amount","Underwater_fog_amount","Xray_body_amount","Xray_skeleton_particles_amount","background_color_random_hue","background_underwater_color_hue","dress_color_hue","eye_color_random_hue","light_reflection_mult","random_value"]},"wedgecount":{"concat":false,"flag":0,"own":false,"type":0,"value":[30]},"wedgeindex":{"concat":false,"flag":0,"own":false,"type":0,"value":[123]},"wedgenum":{"concat":false,"flag":0,"own":false,"type":0,"value":[0]},"wedgetotal":{"concat":false,"flag":0,"own":false,"type":0,"value":[30]}} diff --git a/crates/pdg-common/src/nft_metadata/tests/postman request.json b/crates/pdg-common/src/nft_metadata/tests/postman request.json new file mode 100644 index 00000000..dbc4e8b2 --- /dev/null +++ b/crates/pdg-common/src/nft_metadata/tests/postman request.json @@ -0,0 +1,513 @@ +{ + "workitem": { + "attributes": { + "Body_name": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Diver"] + }, + "Body_type": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [4] + }, + "Butterfly_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [2] + }, + "Desintegration_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Env_Light": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [1] + }, + "Env_reflection": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Eyes_light_intensity_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [88] + }, + "FX_lineart_helper": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Fall_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Firefly_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Frozen_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Fungi_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Fx_0": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Hologram"] + }, + "Fx_1": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Disintegration"] + }, + "Fx_1a": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Lineart minimalistic"] + }, + "Fx_2": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Butterflies"] + }, + "Fx_3": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["No"] + }, + "Fx_4": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Frozen"] + }, + "Fx_5": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["No"] + }, + "Fx_6": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["No"] + }, + "Fx_Jellifish": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Fx_switcher_layer_0": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [1] + }, + "Fx_switcher_layer_1": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Fx_switcher_layer_1a": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [2] + }, + "Fx_switcher_layer_2": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [3] + }, + "Fx_switcher_layer_3": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Fx_switcher_layer_4": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [0] + }, + "Fx_switcher_layer_5": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [2] + }, + "Fx_switcher_layer_6": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [2] + }, + "Gold_silver_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [8] + }, + "Grow_flower_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [24] + }, + "Helmet_Light_name": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["No"] + }, + "Helmet_light": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [3] + }, + "Helmet_name": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Sci_Fi_sport_woman"] + }, + "Helmet_type": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [6] + }, + "Hologram_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [89] + }, + "Jellifish": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["No"] + }, + "Ladybag_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Lineart_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Melt_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Melting_glow_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Pixel_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Pose": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [2] + }, + "Pose_name": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": ["Overconfident"] + }, + "Rain_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Render_noise_threshold": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0.6] + }, + "Render_resolution": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [300] + }, + "Smoke_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Soap_bubble_intensity_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Soap_bubble_roughness_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Spring_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Underwater_fog_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Xray_body_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "Xray_skeleton_particles_amount": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0] + }, + "background_color_random_hue": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [26.4917135334104] + }, + "background_underwater_color_hue": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0.0] + }, + "dress_color_hue": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [287.51463285357204] + }, + "eye_color_random_hue": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [50.0] + }, + "light_reflection_mult": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [1] + }, + "random_value": { + "concat": false, + "flag": 0, + "own": false, + "type": 1, + "value": [0.0] + }, + "wedgeattribs": { + "concat": false, + "flag": 0, + "own": false, + "type": 2, + "value": [ + "Body_type", + "Butterfly_amount", + "Desintegration_amount", + "Env_Light", + "Env_reflection", + "Eyes_light_intensity_amount", + "FX_lineart_helper", + "Fall_amount", + "Firefly_amount", + "Frozen_amount", + "Fungi_amount", + "Fx_Jellifish", + "Fx_switcher_layer_0", + "Fx_switcher_layer_1", + "Fx_switcher_layer_1a", + "Fx_switcher_layer_2", + "Fx_switcher_layer_3", + "Fx_switcher_layer_4", + "Fx_switcher_layer_5", + "Fx_switcher_layer_6", + "Gold_silver_amount", + "Grow_flower_amount", + "Helmet_light", + "Helmet_type", + "Hologram_amount", + "Ladybag_amount", + "Lineart_amount", + "Melt_amount", + "Melting_glow_amount", + "Pixel_amount", + "Pose", + "Rain_amount", + "Render_noise_threshold", + "Render_resolution", + "Smoke_amount", + "Soap_bubble_intensity_amount", + "Soap_bubble_roughness_amount", + "Spring_amount", + "Underwater_fog_amount", + "Xray_body_amount", + "Xray_skeleton_particles_amount", + "background_color_random_hue", + "background_underwater_color_hue", + "dress_color_hue", + "eye_color_random_hue", + "light_reflection_mult", + "random_value" + ] + }, + "wedgeindex": { + "concat": false, + "flag": 0, + "own": false, + "type": 0, + "value": [521533168994] + } + }, + "batchIndex": -1, + "batchParentId": -1, + "cloneTargetId": -1, + "cookType": 0, + "customData": "", + "customDataType": "genericdata", + "executionType": 0, + "frame": 0, + "frameStep": 1, + "hasFrame": false, + "id": 807, + "index": 0, + "isCloneResultData": false, + "isFrozen": false, + "isNoGenerate": false, + "isPostCook": false, + "isStatic": false, + "loopBeginStackIds": [], + "loopBeginStackIters": [], + "loopBeginStackNumbers": [], + "loopBeginStackSizes": [], + "nodeName": "csvoutput2", + "priority": 0, + "state": 5, + "type": 0 + } +} diff --git a/crates/rhai-script/Cargo.toml b/crates/rhai-script/Cargo.toml new file mode 100644 index 00000000..638840f6 --- /dev/null +++ b/crates/rhai-script/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rhai-script" +version = "0.0.0" +edition = "2021" + +[[bin]] +name = "gen" + +[dependencies] +anyhow.workspace = true +bs58.workspace = true +flow-lib.workspace = true +rhai = { version = "1.17.2", features = ["decimal", "indexmap"] } +rhai-rand = { version = "0.1.6", features = ["decimal"] } +serde.workspace = true +serde_json.workspace = true +thiserror = "1.0.50" +tracing = "0.1.40" +chrono = "0.4" +rust_decimal = "1.36.0" diff --git a/crates/rhai-script/node-definitions/rhai_script_0x1.json b/crates/rhai-script/node-definitions/rhai_script_0x1.json new file mode 100644 index 00000000..9ac821ce --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_0x1.json @@ -0,0 +1,89 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_0x1", + "display_name": "RHAI Script 0x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_0x2.json b/crates/rhai-script/node-definitions/rhai_script_0x2.json new file mode 100644 index 00000000..bcc773e9 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_0x2.json @@ -0,0 +1,96 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_0x2", + "display_name": "RHAI Script 0x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_0x3.json b/crates/rhai-script/node-definitions/rhai_script_0x3.json new file mode 100644 index 00000000..ff16bd60 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_0x3.json @@ -0,0 +1,103 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_0x3", + "display_name": "RHAI Script 0x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_0x4.json b/crates/rhai-script/node-definitions/rhai_script_0x4.json new file mode 100644 index 00000000..aba5ef27 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_0x4.json @@ -0,0 +1,110 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_0x4", + "display_name": "RHAI Script 0x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_0x5.json b/crates/rhai-script/node-definitions/rhai_script_0x5.json new file mode 100644 index 00000000..408bb7a8 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_0x5.json @@ -0,0 +1,117 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_0x5", + "display_name": "RHAI Script 0x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_1x1.json b/crates/rhai-script/node-definitions/rhai_script_1x1.json new file mode 100644 index 00000000..dfb532f4 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_1x1.json @@ -0,0 +1,99 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_1x1", + "display_name": "RHAI Script 1x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_1x2.json b/crates/rhai-script/node-definitions/rhai_script_1x2.json new file mode 100644 index 00000000..525d9029 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_1x2.json @@ -0,0 +1,106 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_1x2", + "display_name": "RHAI Script 1x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_1x3.json b/crates/rhai-script/node-definitions/rhai_script_1x3.json new file mode 100644 index 00000000..500222eb --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_1x3.json @@ -0,0 +1,113 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_1x3", + "display_name": "RHAI Script 1x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_1x4.json b/crates/rhai-script/node-definitions/rhai_script_1x4.json new file mode 100644 index 00000000..61560a61 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_1x4.json @@ -0,0 +1,120 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_1x4", + "display_name": "RHAI Script 1x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_1x5.json b/crates/rhai-script/node-definitions/rhai_script_1x5.json new file mode 100644 index 00000000..a3af96a4 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_1x5.json @@ -0,0 +1,127 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_1x5", + "display_name": "RHAI Script 1x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_2x1.json b/crates/rhai-script/node-definitions/rhai_script_2x1.json new file mode 100644 index 00000000..06547dd7 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_2x1.json @@ -0,0 +1,109 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_2x1", + "display_name": "RHAI Script 2x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_2x2.json b/crates/rhai-script/node-definitions/rhai_script_2x2.json new file mode 100644 index 00000000..a866ee71 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_2x2.json @@ -0,0 +1,116 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_2x2", + "display_name": "RHAI Script 2x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_2x3.json b/crates/rhai-script/node-definitions/rhai_script_2x3.json new file mode 100644 index 00000000..af1f3f57 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_2x3.json @@ -0,0 +1,123 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_2x3", + "display_name": "RHAI Script 2x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_2x4.json b/crates/rhai-script/node-definitions/rhai_script_2x4.json new file mode 100644 index 00000000..9b26d01a --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_2x4.json @@ -0,0 +1,130 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_2x4", + "display_name": "RHAI Script 2x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_2x5.json b/crates/rhai-script/node-definitions/rhai_script_2x5.json new file mode 100644 index 00000000..dbfc6d78 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_2x5.json @@ -0,0 +1,137 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_2x5", + "display_name": "RHAI Script 2x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_3x1.json b/crates/rhai-script/node-definitions/rhai_script_3x1.json new file mode 100644 index 00000000..16e5e771 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_3x1.json @@ -0,0 +1,119 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_3x1", + "display_name": "RHAI Script 3x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_3x2.json b/crates/rhai-script/node-definitions/rhai_script_3x2.json new file mode 100644 index 00000000..f045b0a5 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_3x2.json @@ -0,0 +1,126 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_3x2", + "display_name": "RHAI Script 3x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_3x3.json b/crates/rhai-script/node-definitions/rhai_script_3x3.json new file mode 100644 index 00000000..06974ba6 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_3x3.json @@ -0,0 +1,133 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_3x3", + "display_name": "RHAI Script 3x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_3x4.json b/crates/rhai-script/node-definitions/rhai_script_3x4.json new file mode 100644 index 00000000..5886c180 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_3x4.json @@ -0,0 +1,140 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_3x4", + "display_name": "RHAI Script 3x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_3x5.json b/crates/rhai-script/node-definitions/rhai_script_3x5.json new file mode 100644 index 00000000..da6c717e --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_3x5.json @@ -0,0 +1,147 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_3x5", + "display_name": "RHAI Script 3x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_4x1.json b/crates/rhai-script/node-definitions/rhai_script_4x1.json new file mode 100644 index 00000000..8a9f1d21 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_4x1.json @@ -0,0 +1,129 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_4x1", + "display_name": "RHAI Script 4x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_4x2.json b/crates/rhai-script/node-definitions/rhai_script_4x2.json new file mode 100644 index 00000000..1ae0811a --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_4x2.json @@ -0,0 +1,136 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_4x2", + "display_name": "RHAI Script 4x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_4x3.json b/crates/rhai-script/node-definitions/rhai_script_4x3.json new file mode 100644 index 00000000..7bfcd7d2 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_4x3.json @@ -0,0 +1,143 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_4x3", + "display_name": "RHAI Script 4x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_4x4.json b/crates/rhai-script/node-definitions/rhai_script_4x4.json new file mode 100644 index 00000000..27c8b400 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_4x4.json @@ -0,0 +1,150 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_4x4", + "display_name": "RHAI Script 4x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_4x5.json b/crates/rhai-script/node-definitions/rhai_script_4x5.json new file mode 100644 index 00000000..2fb37883 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_4x5.json @@ -0,0 +1,157 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_4x5", + "display_name": "RHAI Script 4x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_5x1.json b/crates/rhai-script/node-definitions/rhai_script_5x1.json new file mode 100644 index 00000000..c968c08e --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_5x1.json @@ -0,0 +1,139 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_5x1", + "display_name": "RHAI Script 5x1", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "e", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_5x2.json b/crates/rhai-script/node-definitions/rhai_script_5x2.json new file mode 100644 index 00000000..5f6a055d --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_5x2.json @@ -0,0 +1,146 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_5x2", + "display_name": "RHAI Script 5x2", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "e", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_5x3.json b/crates/rhai-script/node-definitions/rhai_script_5x3.json new file mode 100644 index 00000000..7e29e7d3 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_5x3.json @@ -0,0 +1,153 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_5x3", + "display_name": "RHAI Script 5x3", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "e", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_5x4.json b/crates/rhai-script/node-definitions/rhai_script_5x4.json new file mode 100644 index 00000000..2dbcdc8f --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_5x4.json @@ -0,0 +1,160 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_5x4", + "display_name": "RHAI Script 5x4", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "e", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/node-definitions/rhai_script_5x5.json b/crates/rhai-script/node-definitions/rhai_script_5x5.json new file mode 100644 index 00000000..724c6900 --- /dev/null +++ b/crates/rhai-script/node-definitions/rhai_script_5x5.json @@ -0,0 +1,167 @@ +{ + "type": "native", + "data": { + "node_id": "rhai_script_5x5", + "display_name": "RHAI Script 5x5", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + }, + { + "name": "a", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "b", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "c", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "d", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }, + { + "name": "e", + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "u", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "v", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "x", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "y", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }, + { + "name": "z", + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + } +} \ No newline at end of file diff --git a/crates/rhai-script/src/bin/gen.rs b/crates/rhai-script/src/bin/gen.rs new file mode 100644 index 00000000..b1d0d958 --- /dev/null +++ b/crates/rhai-script/src/bin/gen.rs @@ -0,0 +1,87 @@ +use rhai_script::COMMAND_ID_PREFIX; +use serde::Serialize; +use serde_json::{json, ser::PrettyFormatter}; + +const BASE: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../node-definition.json" +)); + +fn main() { + let mut base: serde_json::Value = serde_json::from_str(BASE).unwrap(); + base["targets"] = vec![json!( + { + "name": "source", + "type_bounds": [ + "string" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "Script's source code" + } + )] + .into(); + base["sources"] = serde_json::Value::Array(Vec::new()); + base["targets_form.json_schema"] = json!({ + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "title": "source", + "type": "string" + } + } + }); + base["targets_form.ui_schema"] = json!({ + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + }); + for i in 0..=5 { + for o in 1..=5 { + let mut def = base.clone(); + def["data"]["node_id"] = format!("{COMMAND_ID_PREFIX}{i}x{o}").into(); + def["data"]["display_name"] = format!("RHAI Script {i}x{o}").into(); + let targets = (0..i).map(|i| { + json!({ + "name": b"abcde"[i] as char, + "type_bounds": [ + "free" + ], + "required": false, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + }) + }); + def["targets"].as_array_mut().unwrap().extend(targets); + let sources = (0..o).map(|o| { + json!({ + "name": b"uvxyz"[o] as char, + "type": "free", + "optional": true, + "defaultValue": "", + "tooltip": "" + }) + }); + def["sources"].as_array_mut().unwrap().extend(sources); + let mut pretty = Vec::::new(); + def.serialize(&mut serde_json::ser::Serializer::with_formatter( + &mut pretty, + PrettyFormatter::with_indent(b" "), + )) + .unwrap(); + let base_path: String = + concat!(env!("CARGO_MANIFEST_DIR"), "/node-definitions/",).to_owned(); + std::fs::write( + &(base_path + format!("rhai_script_{i}x{o}.json").as_str()), + &pretty, + ) + .unwrap(); + } + } +} diff --git a/crates/rhai-script/src/convert.rs b/crates/rhai-script/src/convert.rs new file mode 100644 index 00000000..23371f99 --- /dev/null +++ b/crates/rhai-script/src/convert.rs @@ -0,0 +1,73 @@ +use flow_lib::{value::Decimal, Name, Value}; +use rhai::Dynamic; +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +pub enum ConvertError { + #[error("unknown type: {}", .0)] + UnknownType(&'static str), +} + +pub fn dynamic_to_value(v: Dynamic) -> Result { + if v.is::() { + Ok(v.cast::()) + } else if v.is_string() { + v.into_string() + .map(Value::String) + .map_err(ConvertError::UnknownType) + } else if v.is_map() { + let map = v.cast::(); + Ok(Value::Map( + map.into_iter() + .map(|(k, v)| Ok((Name::from(k), dynamic_to_value(v)?))) + .collect::>()?, + )) + } else if v.is_array() { + let array = v.cast::(); + Ok(Value::Array( + array + .into_iter() + .map(dynamic_to_value) + .collect::, _>>()?, + )) + } else if v.as_unit().is_ok() { + Ok(Value::Null) + } else if let Ok(d) = v.as_decimal() { + Ok(Value::Decimal(d)) + } else if let Ok(i) = v.as_int() { + Ok(Value::I64(i)) + } else if let Ok(f) = v.as_float() { + Ok(Value::F64(f)) + } else if let Ok(b) = v.as_bool() { + Ok(Value::Bool(b)) + } else { + Err(ConvertError::UnknownType(v.type_name())) + } +} + +pub fn value_to_dynamic(v: Value) -> Dynamic { + match v { + Value::Null => Dynamic::UNIT, + Value::String(x) => x.into(), + Value::Bool(x) => x.into(), + Value::U64(x) => Decimal::from(x).into(), + Value::I64(x) => x.into(), + Value::F64(x) => x.into(), + Value::Decimal(x) => x.into(), + Value::U128(x) => Dynamic::from(x), // TODO + Value::I128(x) => Dynamic::from(x), // TODO + Value::B32(x) => bs58::encode(&x).into_string().into(), + Value::B64(x) => bs58::encode(&x).into_string().into(), + Value::Bytes(x) => rhai::Blob::from(x).into(), + Value::Array(x) => x + .into_iter() + .map(value_to_dynamic) + .collect::() + .into(), + Value::Map(x) => x + .into_iter() + .map(|(k, v)| (k.into(), value_to_dynamic(v))) + .collect::() + .into(), + } +} diff --git a/crates/rhai-script/src/lib.rs b/crates/rhai-script/src/lib.rs new file mode 100644 index 00000000..4787fae8 --- /dev/null +++ b/crates/rhai-script/src/lib.rs @@ -0,0 +1,167 @@ +use anyhow::anyhow; +use chrono::Utc; +use convert::{dynamic_to_value, value_to_dynamic}; +use flow_lib::command::prelude::*; +use rhai::{ + packages::{Package, StandardPackage}, + Dynamic, EvalAltResult, +}; +use rhai_rand::RandomPackage; + +pub mod convert; + +pub use rhai::Engine; + +fn utc_now() -> String { + Utc::now().to_string() +} + +fn decimal(x: Dynamic) -> Result> { + let value = dynamic_to_value(x) + .map_err(|error| EvalAltResult::ErrorSystem("convert error".to_owned(), Box::new(error)))?; + let decimal = value::decimal::deserialize(value) + .map_err(|error| EvalAltResult::ErrorSystem("convert error".to_owned(), Box::new(error)))?; + Ok(decimal) +} + +pub fn setup_engine() -> Engine { + let mut engine = Engine::new(); + engine + .register_global_module(StandardPackage::new().as_shared_module()) + .register_static_module("rand", RandomPackage::new().as_shared_module()) + .register_fn("utc_now", utc_now) + .register_fn("Decimal", decimal) + .set_max_expr_depths(32, 32) + .set_max_call_levels(256) + .set_max_operations(10_000_000) + .set_max_string_size(50_000) + .set_max_array_size(10_000) + .set_max_map_size(10_000) + .set_max_variables(50); + engine +} + +pub const COMMAND_ID_PREFIX: &str = "rhai_script_"; + +pub fn is_rhai_script(s: &str) -> bool { + s.starts_with(COMMAND_ID_PREFIX) +} + +pub struct Command { + pub source_code_name: Name, + pub inputs: Vec, + pub outputs: Vec, +} + +impl Command { + pub fn run( + &self, + engine: &mut Engine, + ctx: Context, + mut input: ValueSet, + ) -> Result { + let code = String::deserialize( + input + .swap_remove(&self.source_code_name) + .ok_or_else(|| anyhow!("missing input: {}", self.source_code_name))?, + )?; + + let mut scope = rhai::Scope::new(); + + let rhai_env = ctx + .environment + .iter() + .map(|(k, v)| (k.into(), v.into())) + .collect::(); + scope.push_constant_dynamic("ENV", rhai_env.into()); + + for i in &self.inputs { + if i.name == self.source_code_name { + continue; + } + match input.swap_remove(&i.name) { + Some(value) => { + scope.push_dynamic(&i.name, value_to_dynamic(value)); + } + None => { + if i.required { + tracing::warn!("missing input: {}", i.name); + } else { + scope.push_dynamic(&i.name, Dynamic::UNIT); + } + } + } + } + let eval_result = engine + .eval_with_scope::(&mut scope, &code) + .map_err(|error| anyhow!(error.to_string()))?; + let mut outputs = ValueSet::new(); + for o in &self.outputs { + let dy = match scope.remove(&o.name) { + Some(x) => x, + None => { + if o.optional && self.outputs.len() > 1 { + tracing::debug!("missing output: {}", o.name); + } + continue; + } + }; + let value = dynamic_to_value(dy).map_err(|error| anyhow!("{:?}: {}", o.name, error))?; + if !matches!(value, Value::Null) { + outputs.insert(o.name.clone(), value); + } + } + if outputs.is_empty() && self.outputs.len() == 1 { + let name = self.outputs[0].name.clone(); + let value = dynamic_to_value(eval_result).map_err(|e| anyhow!("{:?}: {}", name, e))?; + if !matches!(value, Value::Null) { + outputs.insert(name, value); + } + } + Ok(outputs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rhai::Dynamic; + + #[test] + fn test_engine() { + let script = r#" + let y = x * 2; + x + "#; + let mut e = rhai::Engine::new(); + e.register_fn("*", |a: i128, b: i64| a * b as i128); + let mut scope = rhai::Scope::new(); + scope.push("x", 10i128); + let res = e.eval_with_scope::(&mut scope, script).unwrap(); + let y = scope.get("y").unwrap(); + let _ = dbg!(res); + dbg!(y); + } + + #[test] + fn test_string_format() { + let script = r#"`http://${x}.com`"#; + let e = rhai::Engine::new(); + let mut scope = rhai::Scope::new(); + scope.push_dynamic("x", value_to_dynamic(Value::from("google"))); + let res = e.eval_with_scope::(&mut scope, script).unwrap(); + let value = dynamic_to_value(res).unwrap(); + dbg!(value); + } + + #[test] + fn test_map() { + let script = r#"#{name: x, a: "12"}"#; + let e = rhai::Engine::new(); + let mut scope = rhai::Scope::new(); + scope.push_dynamic("x", value_to_dynamic(Value::from("google"))); + let res = e.eval_with_scope::(&mut scope, script).unwrap(); + let value = dynamic_to_value(res).unwrap(); + dbg!(value); + } +} diff --git a/crates/space-wasm/.gitignore b/crates/space-wasm/.gitignore new file mode 100644 index 00000000..7a6d1fbb --- /dev/null +++ b/crates/space-wasm/.gitignore @@ -0,0 +1,2 @@ +cache +target \ No newline at end of file diff --git a/crates/space-wasm/Cargo.toml b/crates/space-wasm/Cargo.toml new file mode 100644 index 00000000..6e6e6569 --- /dev/null +++ b/crates/space-wasm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "space-wasm" +version = "0.0.0" +edition = "2021" +authors = ["Knarkzel "] +description = "WebAssembly runtime Space Operator" + +[features] +default = ["cranelift"] +cranelift = ["wasmer/cranelift"] +singlepass = ["wasmer/singlepass"] + +[dependencies] +space-lib = { workspace = true } + +ureq = "2.6" +serde = "1.0" +anyhow = "1.0" +rmp-serde = "1.1" +byteorder = "1.4" +wasmer-cache = "3.1" +wasmer = { version = "3.1", default-features = false, features = ["sys"] } +wasmer-wasi = { version = "3.1", default-features = false, features = [ + "sys", + "host-fs", +] } +cranelift-codegen = "^0.91.1" + +[dev-dependencies] +serde_json = "1.0" +pretty_assertions = "1.3" diff --git a/crates/space-wasm/src/ffi.rs b/crates/space-wasm/src/ffi.rs new file mode 100644 index 00000000..bee96f3a --- /dev/null +++ b/crates/space-wasm/src/ffi.rs @@ -0,0 +1,89 @@ +use crate::read; +use serde::Serialize; +use space_lib::{ + common::{Method, RequestData}, + Result, +}; +use wasmer::{AsStoreRef, FunctionEnvMut, Memory, MemoryView}; + +// Environment +#[derive(Default)] +pub struct SpaceEnv { + memory: Option, +} + +impl SpaceEnv { + pub fn set_memory(&mut self, memory: Memory) { + self.memory = Some(memory); + } + + fn get_memory(&self) -> &Memory { + self.memory.as_ref().unwrap() + } + + fn view<'a>(&'a self, store: &'a impl AsStoreRef) -> MemoryView<'a> { + self.get_memory().view(store) + } +} + +// Utility functions +fn create_request(request_data: RequestData) -> ureq::Request { + let mut request = match request_data.method { + Method::GET => ureq::get(&request_data.url), + Method::POST => ureq::post(&request_data.url), + Method::DELETE => ureq::delete(&request_data.url), + Method::HEAD => ureq::head(&request_data.url), + Method::PATCH => ureq::patch(&request_data.url), + Method::PUT => ureq::put(&request_data.url), + }; + for chunk in request_data.headers.chunks(2) { + let (header, value) = (&chunk[0], &chunk[1]); + request = request.set(header, value); + } + for chunk in request_data.queries.chunks(2) { + let (param, value) = (&chunk[0], &chunk[1]); + request = request.query(param, value); + } + request +} + +fn write_serialized(mut ctx: FunctionEnvMut, value: T) -> Result { + // Read response into string + let data = rmp_serde::to_vec_named(&value)?; + + // Find offset to write data, possibly grow memory + let memory_size = ctx.data().view(&ctx).data_size(); + let offset = memory_size; + let total = data.len() as u64 + 4; + let delta = (offset + total - memory_size) / wasmer::WASM_PAGE_SIZE as u64 + 1; + let memory = ctx.data().get_memory().clone(); + memory.grow(&mut ctx, delta as u32)?; + + // Write bytes as [len, data] + let view = ctx.data().view(&ctx); + view.write(offset, &u32::to_le_bytes(data.len() as u32))?; + view.write(offset + 4, &data)?; + + // Return pointer to SpaceSlice + Ok(offset) +} + +// Host functions +pub fn http_call_request(ctx: FunctionEnvMut, bytes: u32, bytes_len: u32) -> u64 { + let stub = |ctx: FunctionEnvMut, bytes, bytes_len| -> Result { + // Setup environment + let env = ctx.data(); + let view = env.view(&ctx); + let raw_bytes = read::(&view, bytes as u64, bytes_len as u64)?; + let request_data = rmp_serde::from_slice::(&raw_bytes)?; + let request = create_request(request_data); + let mut response = Vec::new(); + let mut reader = request.call()?.into_reader(); + reader.read_to_end(&mut response)?; + write_serialized(ctx, response) + }; + match stub(ctx, bytes, bytes_len) { + Ok(pointer) => pointer, + _ => 0, + } +} diff --git a/crates/space-wasm/src/lib.rs b/crates/space-wasm/src/lib.rs new file mode 100644 index 00000000..9e46454a --- /dev/null +++ b/crates/space-wasm/src/lib.rs @@ -0,0 +1,127 @@ +use anyhow::{bail, Result}; +use byteorder::{LittleEndian, ReadBytesExt}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{cell::RefCell, collections::HashMap, io::Cursor}; +use wasmer::{ + Function, FunctionEnv, Instance, Memory, MemoryView, Module, Store, Value, ValueType, WasmSlice, +}; +use wasmer_cache::{Cache, FileSystemCache, Hash}; +use wasmer_wasi::WasiState; + +pub mod ffi; + +pub fn read(view: &MemoryView<'_>, offset: u64, length: u64) -> Result> { + Ok(WasmSlice::new(view, offset, length)?.read_to_vec()?) +} + +pub struct Wasm { + instance: Instance, + store: RefCell, +} + +impl Wasm { + pub fn new(bytes: &[u8], env: HashMap) -> Result { + // Load module using cache + let mut store = Store::default(); + let key = Hash::generate(bytes); + let mut cache = FileSystemCache::new("cache")?; + let module = match unsafe { cache.load(&store, key) } { + Ok(module) => module, + Err(_) => { + let module = Module::new(&store, bytes)?; + cache.store(key, &module)?; + module + } + }; + + // Initialize wasi + let wasi_env = WasiState::new("space").envs(env).finalize(&mut store)?; + let mut import_object = wasi_env.import_object(&mut store, &module)?; + + // Add environment + let function_env = FunctionEnv::new(&mut store, ffi::SpaceEnv::default()); + import_object.define( + "env", + "http_call_request", + Function::new_typed_with_env(&mut store, &function_env, ffi::http_call_request), + ); + + // Create instance + let instance = Instance::new(&mut store, &module, &import_object)?; + let memory = instance.exports.get_memory("memory")?; + + // Give reference to memory + wasi_env.data_mut(&mut store).set_memory(memory.clone()); + function_env.as_mut(&mut store).set_memory(memory.clone()); + + Ok(Self { + instance, + store: RefCell::new(store), + }) + } + + fn call(&self, name: &str, values: &[Value]) -> Result> { + let method = self.instance.exports.get_function(name)?; + Ok(method.call(&mut *self.store.borrow_mut(), values)?) + } + + fn memory(&self) -> Result<&Memory> { + Ok(self.instance.exports.get::("memory")?) + } + + fn view(&self) -> Result { + let store = self.store.borrow_mut(); + Ok(self.memory()?.view(&*store)) + } + + fn memory_grow(&self, size: usize) -> Result<()> { + let memory = self.memory()?; + let pages = (size / wasmer::WASM_PAGE_SIZE) + 1; + memory.grow(&mut *self.store.borrow_mut(), pages as u32)?; + Ok(()) + } + + pub fn run(&self, name: &str, input: &T) -> Result { + // Serialize data + let serialized = rmp_serde::to_vec(input)?; + let input_len = (serialized.len() as u32).to_le_bytes(); + let input_bytes = [&input_len[..], &serialized].concat(); + + // Write to memory + let heap_start = match self + .instance + .exports + .get::("__heap_base") + .map(|it| it.get(&mut *self.store.borrow_mut())) + { + Ok(Value::I32(heap_start)) => heap_start, + _ => 0x110000, + }; + self.memory_grow(input_bytes.len())?; + self.view()?.write(heap_start as u64, &input_bytes)?; + + // Call module and pass pointer + let values = self.call(name, &[Value::I32(heap_start)])?; + + // Deserialize data from pointer + match &values[..] { + [Value::I32(pointer)] => { + let output_len = { + let bytes = read::(&self.view()?, *pointer as u64, 4)?; + bytes.as_slice().read_u32::()? + }; + let output_ptr = { + let bytes = read::(&self.view()?, *pointer as u64 + 4, 4)?; + bytes.as_slice().read_u32::()? + }; + let output_bytes = read::(&self.view()?, output_ptr as u64, output_len as u64)?; + let output_buffer = Cursor::new(output_bytes); + Ok(rmp_serde::from_read(output_buffer)?) + } + _ => bail!("Expected pointer to serialized data, got {values:#?}"), + } + } +} + +// #[cfg(test)] +// mod tests; diff --git a/crates/space-wasm/src/tests.rs b/crates/space-wasm/src/tests.rs new file mode 100644 index 00000000..70c03a4e --- /dev/null +++ b/crates/space-wasm/src/tests.rs @@ -0,0 +1,125 @@ +use crate::*; +use pretty_assertions::assert_eq; +use serde_json::{json, Value as Json}; + +pub fn module(name: &str) -> Result> { + let base = env!("CARGO_MANIFEST_DIR"); + let path = format!("{base}/tests/{name}/target/wasm32-wasi/release/{name}.wasm"); + Ok(std::fs::read(path)?) +} + +#[test] +fn manual() -> Result<()> { + let wasm = Wasm::new(&module("manual")?, <_>::default())?; + let input = json! {{ + "value": 100, + "name": "Space Operator", + }}; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! {{ + "value": 200, + "name": "rotarepO ecapS", + }} + ); + Ok(()) +} + +#[test] +fn automatic() -> Result<()> { + let wasm = Wasm::new(&module("automatic")?, <_>::default())?; + let input = json! {{ + "value": 100, + "name": "Space Operator", + }}; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! {{ + "value": 200, + "name": "rotarepO ecapS", + }} + ); + Ok(()) +} + +#[test] +fn simple() -> Result<()> { + let wasm = Wasm::new(&module("simple")?, <_>::default())?; + let input = json! { + "This is my string".repeat(10000) + }; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! { + "gnirts ym si sihT".repeat(10000) + } + ); + Ok(()) +} + +#[test] +fn env() -> Result<()> { + let wasm = Wasm::new( + &module("env")?, + [("RUST_LOG".to_owned(), "info".to_owned())].into(), + )?; + let input = json!("RUST_LOG"); + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!(output, json!("info")); + Ok(()) +} + +#[test] +fn number() -> Result<()> { + let wasm = Wasm::new(&module("number")?, <_>::default())?; + let input = json! { + 100 + }; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! { + 200 + } + ); + Ok(()) +} + +#[test] +fn float() -> Result<()> { + let wasm = Wasm::new(&module("float")?, <_>::default())?; + let input = json! { + 5.4321 + }; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! { + 17.065445453565115 + } + ); + Ok(()) +} + +#[test] +fn http() -> Result<()> { + let wasm = Wasm::new(&module("http")?, <_>::default())?; + let input = json! {{ + "url": "https://dummyjson.com/products/1", + }}; + let output = wasm.run::<_, Json>("main", &input)?; + assert_eq!( + output, + json! {{ + "Ok": { + "id": 1, + "title": "iPhone 9", + "description": "An apple mobile which is nothing like apple", + }, + }} + ); + Ok(()) +} diff --git a/crates/space-wasm/tests/automatic/.cargo/config.toml b/crates/space-wasm/tests/automatic/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/automatic/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/automatic/Cargo.lock b/crates/space-wasm/tests/automatic/Cargo.lock new file mode 100644 index 00000000..5c708e34 --- /dev/null +++ b/crates/space-wasm/tests/automatic/Cargo.lock @@ -0,0 +1,137 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "automatic" +version = "0.1.0" +dependencies = [ + "serde", + "space-lib", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" diff --git a/crates/space-wasm/tests/automatic/Cargo.toml b/crates/space-wasm/tests/automatic/Cargo.toml new file mode 100644 index 00000000..07054d62 --- /dev/null +++ b/crates/space-wasm/tests/automatic/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "automatic" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +space-lib = "0.5" +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/space-wasm/tests/automatic/src/lib.rs b/crates/space-wasm/tests/automatic/src/lib.rs new file mode 100644 index 00000000..b922123f --- /dev/null +++ b/crates/space-wasm/tests/automatic/src/lib.rs @@ -0,0 +1,22 @@ +use space_lib::space; +use serde::{Serialize, Deserialize}; + +#[derive(Deserialize)] +struct Input { + value: usize, + name: String, +} + +#[derive(Serialize)] +struct Output { + value: usize, + name: String, +} + +#[space] +fn main(input: Input) -> Output { + Output { + value: input.value * 2, + name: input.name.chars().rev().collect(), + } +} diff --git a/crates/space-wasm/tests/env/.cargo/config.toml b/crates/space-wasm/tests/env/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/env/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/env/Cargo.lock b/crates/space-wasm/tests/env/Cargo.lock new file mode 100644 index 00000000..fc1d1d4a --- /dev/null +++ b/crates/space-wasm/tests/env/Cargo.lock @@ -0,0 +1,136 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "env" +version = "0.1.0" +dependencies = [ + "space-lib", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/crates/space-wasm/tests/env/Cargo.toml b/crates/space-wasm/tests/env/Cargo.toml new file mode 100644 index 00000000..25f837e7 --- /dev/null +++ b/crates/space-wasm/tests/env/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "env" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +space-lib = "0.5" diff --git a/crates/space-wasm/tests/env/src/lib.rs b/crates/space-wasm/tests/env/src/lib.rs new file mode 100644 index 00000000..047cfb05 --- /dev/null +++ b/crates/space-wasm/tests/env/src/lib.rs @@ -0,0 +1,6 @@ +use space_lib::space; + +#[space] +fn main(input: String) -> String { + std::env::var(input).unwrap() +} diff --git a/crates/space-wasm/tests/float/.cargo/config.toml b/crates/space-wasm/tests/float/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/float/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/float/Cargo.lock b/crates/space-wasm/tests/float/Cargo.lock new file mode 100644 index 00000000..9dd47677 --- /dev/null +++ b/crates/space-wasm/tests/float/Cargo.lock @@ -0,0 +1,136 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "float" +version = "0.1.0" +dependencies = [ + "space-lib", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/crates/space-wasm/tests/float/Cargo.toml b/crates/space-wasm/tests/float/Cargo.toml new file mode 100644 index 00000000..6c7d0294 --- /dev/null +++ b/crates/space-wasm/tests/float/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "float" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +space-lib = "0.5" diff --git a/crates/space-wasm/tests/float/src/lib.rs b/crates/space-wasm/tests/float/src/lib.rs new file mode 100644 index 00000000..b13fc517 --- /dev/null +++ b/crates/space-wasm/tests/float/src/lib.rs @@ -0,0 +1,7 @@ +use space_lib::space; +use std::f64::consts::PI; + +#[space] +fn main(input: f64) -> f64 { + input * PI +} diff --git a/crates/space-wasm/tests/http/.cargo/config.toml b/crates/space-wasm/tests/http/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/http/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/http/Cargo.lock b/crates/space-wasm/tests/http/Cargo.lock new file mode 100644 index 00000000..11929190 --- /dev/null +++ b/crates/space-wasm/tests/http/Cargo.lock @@ -0,0 +1,161 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "http" +version = "0.1.0" +dependencies = [ + "serde", + "space-lib", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "serde_json", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/crates/space-wasm/tests/http/Cargo.toml b/crates/space-wasm/tests/http/Cargo.toml new file mode 100644 index 00000000..30eff5b2 --- /dev/null +++ b/crates/space-wasm/tests/http/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "http" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +space-lib = { version = "0.5", features = ["json"] } diff --git a/crates/space-wasm/tests/http/src/lib.rs b/crates/space-wasm/tests/http/src/lib.rs new file mode 100644 index 00000000..9fccc4bc --- /dev/null +++ b/crates/space-wasm/tests/http/src/lib.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use space_lib::{space, Request, Result}; + +#[derive(Deserialize)] +struct Input { + url: String, +} + +#[derive(Serialize, Deserialize)] +struct Output { + id: usize, + title: String, + description: String, +} + +#[space] +fn main(input: Input) -> Result { + let output = Request::get(input.url).call()?.into_json::()?; + Ok(output) +} diff --git a/crates/space-wasm/tests/manual/.cargo/config.toml b/crates/space-wasm/tests/manual/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/manual/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/manual/Cargo.lock b/crates/space-wasm/tests/manual/Cargo.lock new file mode 100644 index 00000000..d597e66f --- /dev/null +++ b/crates/space-wasm/tests/manual/Cargo.lock @@ -0,0 +1,115 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "manual" +version = "0.1.0" +dependencies = [ + "rmp-serde", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775c11906edafc97bc378816b94585fbd9a054eabaf86fdd0ced94af449efab7" diff --git a/crates/space-wasm/tests/manual/Cargo.toml b/crates/space-wasm/tests/manual/Cargo.toml new file mode 100644 index 00000000..a3d79f17 --- /dev/null +++ b/crates/space-wasm/tests/manual/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "manual" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +rmp-serde = "1.1" +serde = { version = "1.0", features = ["derive"] } diff --git a/crates/space-wasm/tests/manual/src/lib.rs b/crates/space-wasm/tests/manual/src/lib.rs new file mode 100644 index 00000000..4d5dcc61 --- /dev/null +++ b/crates/space-wasm/tests/manual/src/lib.rs @@ -0,0 +1,46 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Deserialize)] +struct Input { + value: usize, + name: String, +} + +#[derive(Serialize)] +struct Output { + value: usize, + name: String, +} + +#[repr(C)] +struct SpaceSlice { + len: usize, + ptr: *mut u8, +} + +#[no_mangle] +fn main(ptr: usize) -> Box { + // Deserialize input + let bytes = unsafe { + let len = *(ptr as *const usize); + let data = (ptr + 4) as *mut u8; + std::slice::from_raw_parts(data, len) + }; + let input = rmp_serde::from_slice::(bytes).unwrap(); + + // Actual code + fn main_stub(input: Input) -> Output { + Output { + value: input.value * 2, + name: input.name.chars().rev().collect(), + } + } + let output = main_stub(input); + + // Serialize output + let bytes = rmp_serde::to_vec_named(&output).unwrap().leak(); + Box::new(SpaceSlice { + len: bytes.len(), + ptr: bytes.as_mut_ptr(), + }) +} diff --git a/crates/space-wasm/tests/number/.cargo/config.toml b/crates/space-wasm/tests/number/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/number/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/number/Cargo.lock b/crates/space-wasm/tests/number/Cargo.lock new file mode 100644 index 00000000..a8778546 --- /dev/null +++ b/crates/space-wasm/tests/number/Cargo.lock @@ -0,0 +1,136 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number" +version = "0.1.0" +dependencies = [ + "space-lib", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/crates/space-wasm/tests/number/Cargo.toml b/crates/space-wasm/tests/number/Cargo.toml new file mode 100644 index 00000000..e1ba49eb --- /dev/null +++ b/crates/space-wasm/tests/number/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "number" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +space-lib = "0.5" diff --git a/crates/space-wasm/tests/number/src/lib.rs b/crates/space-wasm/tests/number/src/lib.rs new file mode 100644 index 00000000..5b3de8ca --- /dev/null +++ b/crates/space-wasm/tests/number/src/lib.rs @@ -0,0 +1,6 @@ +use space_lib::space; + +#[space] +fn main(input: u64) -> u64 { + input * 2 +} diff --git a/crates/space-wasm/tests/simple/.cargo/config.toml b/crates/space-wasm/tests/simple/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/crates/space-wasm/tests/simple/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/crates/space-wasm/tests/simple/Cargo.lock b/crates/space-wasm/tests/simple/Cargo.lock new file mode 100644 index 00000000..8331c3fe --- /dev/null +++ b/crates/space-wasm/tests/simple/Cargo.lock @@ -0,0 +1,136 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "simple" +version = "0.1.0" +dependencies = [ + "space-lib", +] + +[[package]] +name = "space-lib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24940838c84c1aa482356a3c5df1cd85d9aaccae57f5869e71f0313fa1efb3ec" +dependencies = [ + "rmp-serde", + "serde", + "space-macro", +] + +[[package]] +name = "space-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6fb5ab85bef3998d2644cd548609d0cbd5fba7959fca8b958b8bec529ef5a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/crates/space-wasm/tests/simple/Cargo.toml b/crates/space-wasm/tests/simple/Cargo.toml new file mode 100644 index 00000000..82fd844b --- /dev/null +++ b/crates/space-wasm/tests/simple/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "simple" +version = "0.1.0" +edition = "2021" + +[profile.release] +lto = true +strip = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +space-lib = "0.5" diff --git a/crates/space-wasm/tests/simple/src/lib.rs b/crates/space-wasm/tests/simple/src/lib.rs new file mode 100644 index 00000000..526924e0 --- /dev/null +++ b/crates/space-wasm/tests/simple/src/lib.rs @@ -0,0 +1,6 @@ +use space_lib::space; + +#[space] +fn main(input: String) -> String { + input.chars().rev().collect() +} diff --git a/crates/srpc/Cargo.toml b/crates/srpc/Cargo.toml new file mode 100644 index 00000000..6961f61f --- /dev/null +++ b/crates/srpc/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "srpc" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix = "0.13.5" +actix-web = { version = "4.9.0", default-features = false } +actix-web-actors = "4.3.0" +futures-channel = "0.3.30" +futures-util = "0.3.30" +hashbrown = "0.14" +serde.workspace = true +serde_json.workspace = true +smallvec = { version = "1.13.2", features = ["const_generics"] } +thiserror = "1.0.58" +tower = { version = "0.4.13", features = ["filter", "util"] } +tracing = "0.1.40" +url = { version = "2.5.0", features = ["serde"] } + +[dev-dependencies] +criterion = "0.5.0" +reqwest = { version = "0.12", default-features = false, features = ["blocking"] } +tungstenite = "0.24.0" +tokio = { version = "1", features = ["sync"] } + +[[bench]] +name = "srpc_bench" +harness = false diff --git a/crates/srpc/benches/srpc_bench.rs b/crates/srpc/benches/srpc_bench.rs new file mode 100644 index 00000000..51de000c --- /dev/null +++ b/crates/srpc/benches/srpc_bench.rs @@ -0,0 +1,72 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use srpc::{GetBaseUrl, RegisterJsonService}; +use std::convert::Infallible; +use tungstenite::Message; + +fn make_ws_url(url: &str) -> String { + let url = url + .strip_prefix("http") + .unwrap() + .strip_suffix("call") + .unwrap(); + format!("ws{}ws", url) +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let (url_tx, url_rx) = tokio::sync::oneshot::channel(); + std::thread::spawn(|| { + actix::run(async move { + let addr = srpc::Server::start_http_server().unwrap(); + addr.send(RegisterJsonService::new( + "add".to_owned(), + "".to_owned(), + tower::service_fn(|(a, b): (i64, i64)| async move { Ok::<_, Infallible>(a + b) }), + )) + .await + .unwrap(); + let url = addr + .send(GetBaseUrl) + .await + .unwrap() + .unwrap() + .join("/call") + .unwrap() + .to_string(); + url_tx.send(url).unwrap(); + std::future::pending::<()>().await; + }) + .unwrap(); + }); + + let url = url_rx.blocking_recv().unwrap(); + + c.bench_function("srpc_http1", |b| { + let client = reqwest::blocking::ClientBuilder::new().build().unwrap(); + let body = r#"{"envelope":"","svc_name":"add","svc_id":"","input":[1, 2]}"#; + let req = client + .post(&url) + .header("content-type", "application/json") + .body(body); + b.iter(|| { + let body = req.try_clone().unwrap().send().unwrap().text().unwrap(); + assert_eq!(body, r#"{"envelope":"","success":true,"data":3}"#,); + }); + }); + + let ws_url = make_ws_url(&url); + + c.bench_function("srpc_ws", |b| { + let body = r#"{"envelope":"","svc_name":"add","svc_id":"","input":[1, 2]}"#; + let (mut conn, _) = tungstenite::connect(&ws_url).unwrap(); + b.iter(|| { + conn.send(Message::Text(body.to_owned())).unwrap(); + let Ok(Message::Text(body)) = conn.read() else { + panic!(); + }; + assert_eq!(body, r#"{"envelope":"","success":true,"data":3}"#); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/srpc/src/lib.rs b/crates/srpc/src/lib.rs new file mode 100644 index 00000000..17ada875 --- /dev/null +++ b/crates/srpc/src/lib.rs @@ -0,0 +1,571 @@ +//! RPC for Tower services. +//! +//! Each service is uniquely identified by Name and ID, allowing multiple services of the same class to exists. + +use actix::{Actor, ActorFutureExt, AsyncContext, Context, ResponseFuture, WrapFuture}; +use actix_web::{ + dev::ServerHandle, error::InternalError, http::StatusCode, web, App, HttpRequest, HttpResponse, + HttpServer, +}; +use actix_web_actors::ws::{self, WebsocketContext}; +use futures_channel::oneshot; +use futures_util::TryFutureExt; +use hashbrown::HashMap; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use smallvec::SmallVec; +use std::{collections::VecDeque, fmt::Display, marker::PhantomData}; +use thiserror::Error as ThisError; +use tower::{util::BoxService, BoxError, Service as _, ServiceBuilder, ServiceExt}; +use url::Url; + +pub type JsonService = BoxService; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Request { + pub envelope: String, + pub svc_name: String, + pub svc_id: String, + pub input: T, +} + +impl actix::Message for Request { + type Result = Result; +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Response { + pub envelope: String, + pub success: bool, + pub data: T, +} + +pub struct RegisterJsonService { + pub name: String, + pub id: String, + pub service: S, + _phantom: PhantomData, +} + +impl RegisterJsonService { + pub fn new(name: String, id: String, service: S) -> Self { + Self { + name, + id, + service, + _phantom: PhantomData, + } + } +} + +pub struct RegisterServiceResult { + pub old_service: Option, + pub name: String, + pub id: String, +} + +impl actix::Message for RegisterJsonService { + type Result = RegisterServiceResult; +} + +pub struct RemoveService { + pub name: String, + pub id: String, +} + +impl actix::Message for RemoveService { + type Result = bool; +} + +#[derive(Default)] +struct Service { + svc: Option, + queue: VecDeque<(Request, oneshot::Sender)>, +} + +enum Transport { + Http { handle: ServerHandle, port: u16 }, +} + +impl Transport { + fn start_http(addr: actix::Addr) -> Result { + let server = HttpServer::new(move || { + App::new().configure(|s| configure_server(s, addr.downgrade())) + }) + .workers(1) + .bind("127.0.0.1:0") + .map_err(Error::Bind)?; + let port = server.addrs()[0].port(); + let server = server.run(); + let handle = server.handle(); + actix::spawn(server); + Ok(Self::Http { handle, port }) + } +} + +pub struct Server { + /// svc_name => (svc_id => S) + services: HashMap>, + dead_services: HashMap>, + transport: SmallVec<[Transport; 1]>, +} + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("service not found: {}", .0)] + NotFound(String), + #[error("service dropped without sending a response")] + Dropped, + #[error("bind error: {}", .0)] + Bind(std::io::Error), +} + +impl Actor for Server { + type Context = Context; + + fn stopped(&mut self, _: &mut Self::Context) { + for t in &self.transport { + match t { + Transport::Http { handle, .. } => { + actix::spawn(handle.stop(true)); + } + } + } + } +} + +pub struct GetBaseUrl; + +impl actix::Message for GetBaseUrl { + type Result = Option; +} + +impl actix::Handler for Server { + type Result = actix::Response<::Result>; + fn handle(&mut self, _: GetBaseUrl, _: &mut Self::Context) -> Self::Result { + actix::Response::reply(self.base_url()) + } +} + +impl Server { + pub fn start_http_server() -> Result, Error> { + let ctx = Context::::new(); + let addr = ctx.address(); + Ok(ctx.run(Self { + services: <_>::default(), + dead_services: <_>::default(), + transport: [Transport::start_http(addr.clone())?].into(), + })) + } + + pub fn base_url(&self) -> Option { + self.transport.iter().find_map(|t| match t { + Transport::Http { port, .. } => { + Some(Url::parse(&format!("http://127.0.0.1:{}", port)).unwrap()) + } + }) + } + + fn after_ready( + &mut self, + result: Result, + req: Request, + responder: oneshot::Sender, + ctx: &mut actix::Context, + ) { + match result { + Ok(mut svc) => { + let future = svc.call(req.input); + actix::spawn(async move { + let (success, data) = match future.await { + Ok(x) => (true, x), + Err(x) => (false, x), + }; + responder + .send(Response { + envelope: req.envelope, + success, + data, + }) + .ok(); + }); + let s = self + .services + .get_mut(&req.svc_name) + .and_then(|map| map.get_mut(&req.svc_id)); + match s { + Some(s) => { + if let Some((req, tx)) = s.queue.pop_front() { + self.process_request_with_svc(ctx, req, tx, svc); + } else { + s.svc.replace(svc); + } + } + None => { + let queue = self + .dead_services + .get_mut(&req.svc_name) + .and_then(|map| map.remove(&req.svc_id)) + .map(|s| s.queue); + if let Some(queue) = queue { + actix::spawn(finish_requests(svc, queue)); + } + } + } + } + Err(error) => { + let _ = responder.send(Response { + envelope: req.envelope, + success: false, + data: error, + }); + } + } + } + + fn process_request_with_svc( + &mut self, + ctx: &mut actix::Context, + req: Request, + tx: oneshot::Sender, + svc: JsonService, + ) { + let task = svc + .ready_oneshot() + .into_actor(&*self) + .map(move |result, actor, ctx| actor.after_ready(result, req, tx, ctx)); + ctx.spawn(task); + } + + fn process_request( + &mut self, + ctx: &mut actix::Context, + req: Request, + tx: oneshot::Sender, + ) -> Result<(), Error> { + let s = self + .services + .get_mut(&req.svc_name) + .ok_or_else(|| Error::NotFound(format!("svc_name: {}", req.svc_name)))? + .get_mut(&req.svc_id) + .ok_or_else(|| Error::NotFound(format!("svc_id: {}", req.svc_id)))?; + + match s.svc.take() { + Some(svc) => self.process_request_with_svc(ctx, req, tx, svc), + None => { + s.queue.push_back((req, tx)); + } + } + + Ok(()) + } + + pub fn register_json_service( + &mut self, + name: String, + id: String, + s: S, + ) -> Option + where + S: tower::Service + Send + 'static, + T: DeserializeOwned, + S::Error: std::error::Error + Send + Sync + 'static, + S::Response: Serialize, + S::Future: Send + 'static, + { + tracing::info!("inserting {}::{}", name, id); + let svc = ServiceBuilder::new() + .filter(|r: JsonValue| { + tracing::debug!("request: {}", r); + serde_json::from_value::(r) + }) + .map_result( + |r: Result| -> Result { + match r { + Ok(t) => serde_json::to_value(&t).map_err(|e| e.into()), + Err(e) => Err(e.into()), + } + }, + ) + .check_service::() + .service(s) + .map_err(|error| JsonValue::String(error.to_string())) + .map_result(|r| { + match &r { + Ok(x) => tracing::debug!("success: {}", x), + Err(x) => tracing::debug!("error: {}", x), + } + r + }) + .boxed(); + self.services + .entry(name) + .or_default() + .entry(id) + .or_default() + .svc + .replace(svc) + } +} + +impl actix::Handler for Server { + type Result = ResponseFuture<::Result>; + fn handle(&mut self, msg: Request, ctx: &mut Self::Context) -> Self::Result { + let (tx, rx) = oneshot::channel(); + let result = self.process_request(ctx, msg, tx); + match result { + Ok(()) => Box::pin(rx.map_err(|_| Error::Dropped)), + Err(error) => { + tracing::debug!("error: {}", error); + Box::pin(std::future::ready(Err(error))) + } + } + } +} + +impl actix::Handler> for Server +where + S: tower::Service + Send + 'static, + T: DeserializeOwned, + S::Error: std::error::Error + Send + Sync + 'static, + S::Response: Serialize, + S::Future: Send + 'static, +{ + type Result = actix::Response< as actix::Message>::Result>; + fn handle(&mut self, msg: RegisterJsonService, _: &mut Self::Context) -> Self::Result { + let old_service = self.register_json_service(msg.name.clone(), msg.id.clone(), msg.service); + actix::Response::reply(RegisterServiceResult { + old_service, + name: msg.name, + id: msg.id, + }) + } +} + +async fn finish_requests( + mut svc: JsonService, + mut queue: VecDeque<(Request, oneshot::Sender)>, +) { + while let Some((req, tx)) = queue.pop_front() { + let envelope = req.envelope.clone(); + let response = match svc.ready().await { + Ok(svc) => match svc.call(req.input).await { + Ok(data) => Response { + envelope, + success: true, + data, + }, + Err(error) => Response { + envelope, + success: false, + data: error, + }, + }, + Err(error) => Response { + envelope, + success: false, + data: error, + }, + }; + tx.send(response).ok(); + } +} + +impl actix::Handler for Server { + type Result = actix::Response<::Result>; + fn handle(&mut self, msg: RemoveService, _: &mut Self::Context) -> Self::Result { + tracing::info!("removing {}::{}", msg.name, msg.id); + actix::Response::reply( + self.services + .get_mut(&msg.name) + .and_then(|map| map.remove(&msg.id)) + .map(|Service { svc, queue }| { + if let Some(svc) = svc { + actix::spawn(finish_requests(svc, queue)); + } else { + self.dead_services + .entry(msg.name) + .or_default() + .insert(msg.id, Service { svc: None, queue }); + } + true + }) + .unwrap_or(false), + ) + } +} +struct WsActor { + server: actix::Addr, +} + +impl actix::Actor for WsActor { + type Context = WebsocketContext; +} + +impl Response { + fn error(envelope: String) -> impl FnOnce(E) -> Self { + |error: E| Self { + envelope, + success: false, + data: JsonValue::String(error.to_string()), + } + } + fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|error| { + serde_json::to_string(&Self::error(self.envelope.clone())(error)).unwrap() + }) + } +} + +impl actix::StreamHandler> for WsActor { + fn handle(&mut self, item: Result, ctx: &mut Self::Context) { + match item { + Ok(ws::Message::Text(text)) => match serde_json::from_str::(&text) { + Ok(req) => { + let envelope = req.envelope.clone(); + let resp = self.server.send(req).into_actor(&*self); + let resp = resp.map(move |result, _, ctx| { + let response = match result { + Ok(Ok(resp)) => resp, + Err(error) => Response::error(envelope.clone())(error), + Ok(Err(error)) => Response::error(envelope.clone())(error), + }; + ctx.text(response.to_json()); + }); + ctx.spawn(resp); + } + Err(error) => ctx.text(Response::error(String::new())(error).to_json()), + }, + Ok(ws::Message::Ping(msg)) => { + ctx.pong(&msg); + } + _ => {} + } + } +} + +async fn call( + body: web::Json, + addr: web::Data>, +) -> web::Json { + let req = body.into_inner(); + let envelope = req.envelope.clone(); + + let addr = match addr.upgrade() { + Some(addr) => addr, + None => { + return web::Json(Response { + envelope, + success: false, + data: "Server stopped".into(), + }) + } + }; + + let result = addr.send(req).await; + + web::Json(match result { + Ok(result) => match result { + Ok(resp) => resp, + Err(error) => Response { + envelope, + success: false, + data: JsonValue::String(error.to_string()), + }, + }, + Err(error) => Response { + envelope, + success: false, + data: JsonValue::String(error.to_string()), + }, + }) +} + +async fn handle_ws( + req: HttpRequest, + stream: web::Payload, + addr: web::Data>, +) -> Result { + let server = addr + .upgrade() + .ok_or_else(|| InternalError::new("server stopped", StatusCode::INTERNAL_SERVER_ERROR))?; + actix_web_actors::ws::start(WsActor { server }, &req, stream) +} + +pub fn configure_server(s: &mut web::ServiceConfig, addr: actix::WeakAddr) { + s.app_data(web::Data::new(addr)) + .route("/call", web::post().to(call)) + .route("/ws", web::get().to(handle_ws)); +} + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + use tungstenite::Message; + + use super::*; + + fn spawn_service() -> String { + let (url_tx, url_rx) = tokio::sync::oneshot::channel(); + std::thread::spawn(|| { + actix::run(async move { + let addr = Server::start_http_server().unwrap(); + addr.send(RegisterJsonService::new( + "add".to_owned(), + "".to_owned(), + tower::service_fn( + |(a, b): (i64, i64)| async move { Ok::<_, Infallible>(a + b) }, + ), + )) + .await + .unwrap(); + let url = addr + .send(GetBaseUrl) + .await + .unwrap() + .unwrap() + .join("/call") + .unwrap() + .to_string(); + url_tx.send(url).unwrap(); + std::future::pending::<()>().await; + }) + .unwrap(); + }); + url_rx.blocking_recv().unwrap() + } + + #[test] + fn test_http() { + let url = spawn_service(); + let client = reqwest::blocking::Client::new(); + let body = r#"{"envelope":"","svc_name":"add","svc_id":"","input":[1, 2]}"#; + let body = client + .post(&url) + .header("content-type", "application/json") + .body(body) + .send() + .unwrap() + .text() + .unwrap(); + assert_eq!(body, r#"{"envelope":"","success":true,"data":3}"#); + } + + #[test] + fn test_ws() { + let url = spawn_service(); + let url = url + .strip_prefix("http") + .unwrap() + .strip_suffix("call") + .unwrap(); + let url = format!("ws{}ws", url); + let body = r#"{"envelope":"","svc_name":"add","svc_id":"","input":[1, 2]}"#; + let (mut conn, _) = tungstenite::connect(&url).unwrap(); + conn.send(Message::Text(body.to_owned())).unwrap(); + let Ok(Message::Text(body)) = conn.read() else { + panic!(); + }; + assert_eq!(body, r#"{"envelope":"","success":true,"data":3}"#); + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 00000000..2a0e0c97 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "utils" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tower = { version = "0.4", features = ["buffer", "util"] } +actix = "0.13" +hashbrown = "0.14" +futures-util = { version = "0.3", features = ["sink"] } +serde = "1" +bs58 = "0.4" +thiserror = "1" +base64 = "0.21" +bytes = "1" diff --git a/crates/utils/src/actix_service.rs b/crates/utils/src/actix_service.rs new file mode 100644 index 00000000..4403ac3f --- /dev/null +++ b/crates/utils/src/actix_service.rs @@ -0,0 +1,57 @@ +use actix::MailboxError; +use futures_util::{future::Map, FutureExt}; + +#[derive(Clone)] +pub struct ActixService +where + T: actix::Message + Send, + T::Result: Send, +{ + inner: actix::Recipient, +} + +impl From> for ActixService +where + T: actix::Message + Send, + T::Result: Send, +{ + fn from(inner: actix::Recipient) -> Self { + Self { inner } + } +} + +fn convert_error(result: Result, MailboxError>) -> Result +where + E: From, +{ + match result { + Ok(Ok(ok)) => Ok(ok), + Ok(Err(err)) => Err(err), + Err(err) => Err(E::from(err)), + } +} + +impl tower::Service for ActixService +where + T: actix::Message> + Send, + U: Send, + E: From + Send, +{ + type Response = U; + type Error = E; + type Future = Map< + actix::dev::RecipientRequest, + fn(Result, MailboxError>) -> Result, + >; + + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + fn call(&mut self, req: T) -> Self::Future { + self.inner.send(req).map(convert_error::) + } +} diff --git a/crates/utils/src/address_book.rs b/crates/utils/src/address_book.rs new file mode 100644 index 00000000..214cfc2b --- /dev/null +++ b/crates/utils/src/address_book.rs @@ -0,0 +1,242 @@ +use actix::{Actor, ArbiterHandle, WeakAddr}; +use hashbrown::HashMap; +use std::{ + any::Any, + borrow::Borrow, + hash::{Hash, Hasher}, +}; + +#[derive(Default)] +pub struct AddressBook { + addrs: HashMap>, +} + +#[derive(Debug)] +pub struct AlreadyStarted; + +impl AddressBook { + pub fn new() -> Self { + Self { + addrs: HashMap::new(), + } + } + + pub fn get_or_start(&mut self, id: A::ID, start: F) -> actix::Addr + where + A: Actor>, + A: ManagableActor, + F: FnOnce() -> actix::Addr, + { + self.get::(id.clone()) + .and_then(|weak| weak.upgrade()) + .unwrap_or_else(move || { + let addr = start(); + self.addrs + .insert(AnyID::new::(id), Box::new(addr.downgrade())); + addr + }) + } + + pub fn start(&mut self, actor: A) -> actix::Addr + where + A: Actor>, + A: ManagableActor, + { + let id = actor.id(); + let addr = actor.start(); + let weak = addr.downgrade(); + self.addrs.insert(AnyID::new::(id), Box::new(weak)); + addr + } + + pub fn try_start_in_rt(&mut self, actor: A, rt: ArbiterHandle) -> Result, A> + where + A: Actor>, + A: ManagableActor + Send, + { + let id = actor.id(); + if let hashbrown::hash_map::Entry::Vacant(slot) = self.addrs.entry(AnyID::new::(id)) { + let addr = A::start_in_arbiter(&rt, move |_| actor); + slot.insert(Box::new(addr.downgrade())); + Ok(addr) + } else { + Err(actor) + } + } + + pub fn try_start_with_context( + &mut self, + id: A::ID, + make_actor: F, + rt: ArbiterHandle, + ) -> Result, AlreadyStarted> + where + A: Actor>, + A: ManagableActor + Send, + F: FnOnce(&mut actix::Context) -> A + Send + 'static, + { + if let hashbrown::hash_map::Entry::Vacant(slot) = self.addrs.entry(AnyID::new::(id)) { + let addr = A::start_in_arbiter(&rt, make_actor); + slot.insert(Box::new(addr.downgrade())); + Ok(addr) + } else { + Err(AlreadyStarted) + } + } + + pub fn get(&self, id: A::ID) -> Option> + where + A: ManagableActor, + { + self.addrs + .get(&ID:: { id } as &dyn HashKey) + .map(|boxed| boxed.downcast_ref::>().unwrap().clone()) + } + + #[must_use] + pub fn insert(&mut self, id: A::ID, addr: WeakAddr) -> bool + where + A: ManagableActor, + { + if let hashbrown::hash_map::Entry::Vacant(slot) = self.addrs.entry(AnyID::new::(id)) { + slot.insert(Box::new(addr)); + true + } else { + false + } + } + + pub fn try_insert( + &mut self, + id: A::ID, + addr: WeakAddr, + ) -> Result<(), (A::ID, WeakAddr)> + where + A: ManagableActor, + { + if let hashbrown::hash_map::Entry::Vacant(slot) = + self.addrs.entry(AnyID::new::(id.clone())) + { + slot.insert(Box::new(addr)); + Ok(()) + } else { + Err((id, addr)) + } + } + + pub fn iter(&self) -> impl Iterator)> + '_ { + self.addrs.iter().filter_map(|(k, v)| { + v.downcast_ref::>() + .and_then(|weak| weak.upgrade()) + .and_then(|addr| { + k.id.as_any() + .downcast_ref::>() + .map(|id| (id.id.clone(), addr)) + }) + }) + } +} + +impl Borrow for AnyID { + fn borrow(&self) -> &dyn HashKey { + self.id.as_ref() + } +} + +pub trait ManagableActor: Any + Actor { + type ID: Hash + Eq + Clone; + + fn id(&self) -> Self::ID; +} + +struct ID +where + A: ManagableActor, +{ + id: A::ID, +} + +impl Clone for ID +where + A: ManagableActor, +{ + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + } + } +} + +trait HashKey: Any { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, other: &dyn HashKey) -> bool; + fn clone_box(&self) -> Box; + fn dyn_hash(&self, state: &mut dyn Hasher); +} + +impl Hash for dyn HashKey { + fn hash(&self, state: &mut H) { + self.dyn_hash(state as &mut dyn Hasher); + } +} + +impl PartialEq for dyn HashKey { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other) + } +} + +impl Eq for dyn HashKey {} + +impl HashKey for ID { + fn as_any(&self) -> &dyn Any { + self + } + fn dyn_eq(&self, other: &dyn HashKey) -> bool { + match other.as_any().downcast_ref::>() { + Some(other) => other.id == self.id, + None => false, + } + } + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + fn dyn_hash(&self, mut state: &mut dyn Hasher) { + self.type_id().hash(&mut state); + self.id.hash(&mut state); + } +} + +struct AnyID { + id: Box, +} + +impl Clone for AnyID { + fn clone(&self) -> Self { + Self { + id: self.id.clone_box(), + } + } +} + +impl Hash for AnyID { + fn hash(&self, state: &mut H) { + self.id.dyn_hash(state); + } +} + +impl Eq for AnyID {} + +impl PartialEq for AnyID { + fn eq(&self, other: &Self) -> bool { + self.id.dyn_eq(other.id.as_ref()) + } +} + +impl AnyID { + fn new(id: A::ID) -> Self { + AnyID { + id: Box::new(ID:: { id }), + } + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 00000000..a08bfed0 --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,64 @@ +use thiserror::Error as ThisError; + +pub mod actix_service; +pub mod address_book; +pub mod serde_base64; +pub mod serde_bs58; + +pub struct B58(pub [u8; N]); + +#[derive(ThisError, Debug)] +pub enum Bs58Error { + #[error(transparent)] + Decode(#[from] bs58::decode::Error), + #[error("invalid length, expected: {}, got: {}", expected, got)] + Size { expected: usize, got: usize }, +} + +impl std::str::FromStr for B58 { + type Err = Bs58Error; + + fn from_str(s: &str) -> Result { + let mut buf = [0u8; N]; + let size = bs58::decode(s).into(&mut buf)?; + if size != N { + return Err(Bs58Error::Size { + expected: N, + got: size, + }); + } + Ok(Self(buf)) + } +} + +pub fn bs58_decode(s: &str) -> Result<[u8; N], Bs58Error> { + Ok(s.parse::>()?.0) +} + +pub struct OptionVisitor(pub(crate) V); + +impl<'de, V> serde::de::Visitor<'de> for OptionVisitor +where + V: serde::de::Visitor<'de>, +{ + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("optional ")?; + self.0.expecting(formatter) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + fn visit_some(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self.0).map(Some) + } +} diff --git a/crates/utils/src/serde_base64.rs b/crates/utils/src/serde_base64.rs new file mode 100644 index 00000000..c80a691e --- /dev/null +++ b/crates/utils/src/serde_base64.rs @@ -0,0 +1,54 @@ +use base64::prelude::*; + +pub fn serialize(t: &bytes::Bytes, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&BASE64_STANDARD.encode(t)) +} + +struct Visitor; + +impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = bytes::Bytes; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("base64") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(BASE64_STANDARD + .decode(v) + .map_err(|_| serde::de::Error::custom("invalid base64"))? + .into()) + } +} + +pub fn deserialize<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + d.deserialize_str(Visitor) +} + +pub mod opt { + pub fn serialize(sig: &Option, s: S) -> Result + where + S: serde::Serializer, + { + match sig { + Some(sig) => super::serialize(sig, s), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + d.deserialize_option(crate::OptionVisitor(super::Visitor)) + } +} diff --git a/crates/utils/src/serde_bs58.rs b/crates/utils/src/serde_bs58.rs new file mode 100644 index 00000000..7fe3adac --- /dev/null +++ b/crates/utils/src/serde_bs58.rs @@ -0,0 +1,66 @@ +pub fn serialize(t: &[u8; N], s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&bs58::encode(t).into_string()) +} + +struct Visitor; + +impl<'de, const N: usize> serde::de::Visitor<'de> for Visitor { + type Value = [u8; N]; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("base58") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut pk = [0u8; N]; + let size = bs58::decode(v) + .into(&mut pk) + .map_err(|_| serde::de::Error::custom("invalid base58"))?; + if size != N { + return Err(serde::de::Error::custom("invalid base58")); + } + Ok(pk) + } +} + +pub fn deserialize<'de, const S: usize, D>(d: D) -> Result<[u8; S], D::Error> +where + D: serde::Deserializer<'de>, +{ + d.deserialize_str(Visitor::) +} + +pub mod opt { + struct Bs58<'a>(&'a [u8]); + impl<'a> serde::Serialize for Bs58<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_str(&bs58::encode(self.0).into_string()) + } + } + + pub fn serialize(sig: &Option<[u8; N]>, s: S) -> Result + where + S: serde::Serializer, + { + match sig { + Some(sig) => s.serialize_some(&Bs58(sig.as_slice())), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, const N: usize, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + d.deserialize_option(crate::OptionVisitor(super::Visitor)) + } +} diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 00000000..f3408117 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,6 @@ +# Supabase +.branches +.temp +.env +.config.toml +.local-config.toml diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..65a5f0d0 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,59 @@ +# Generate secrets and configurations + +Our Docker Compose setup needs 2 configuration files, both are located in `flow-backend/docker` folder: + +- `.env`: dotenv file containing environment variables +- `.config.toml`: configuration file used by flow-server. + +Template for these files are in `env.example` and `flow-server-config.toml`. + +Generate secrets and config files for your server: +```bash +./gen-secrets.ts +``` + +Generated secrets are saved in `.env` and `.config.toml` files. + +The script use `env.example` and `flow-server-config.toml` as templates, +you can edit them before running the script to customize values. + +# Running + +Start and wait for containers to be ready: + +```bash +docker compose up -d --wait +``` + +Port binding: +- Supabase: port 8000 +- Flow server: port 8080 +- PostgreSQL: port 5432 + +To see Supabase Dashboard: + +Open `.env` file to see `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD` values: + +```bash +cat .env | grep DASHBOARD +``` + +Visit http://localhost:8000/ . + +# Export your data and use them in self-hosted server + +Follow steps from [here](https://docs.spaceoperator.com/self-hosting/export-data-to-your-instance). + +# Stop and clean up + +To stop services: + +```bash +docker compose down +``` + +Stop and clean up all data: + +```bash +docker compose down -v +``` diff --git a/docker/deno.json b/docker/deno.json new file mode 100644 index 00000000..86c22283 --- /dev/null +++ b/docker/deno.json @@ -0,0 +1,11 @@ +{ + "imports": { + "@solana/web3.js": "npm:@solana/web3.js@^1.91.8", + "@std/crypto": "jsr:@std/crypto@^0.224.0", + "@std/dotenv": "jsr:@std/dotenv@^0.224.0", + "@std/encoding": "jsr:@std/encoding@^0.224.2", + "@std/fs": "jsr:@std/fs@^0.229.1", + "@std/toml": "jsr:@std/toml@^0.224.0", + "tweetnacl": "npm:tweetnacl@^1.0.3" + } +} diff --git a/docker/deno.lock b/docker/deno.lock new file mode 100644 index 00000000..6849b782 --- /dev/null +++ b/docker/deno.lock @@ -0,0 +1,335 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@~0.225.2": "0.225.3", + "jsr:@std/collections@0.224": "0.224.2", + "jsr:@std/crypto@0.224": "0.224.0", + "jsr:@std/dotenv@0.224": "0.224.0", + "jsr:@std/encoding@~0.224.2": "0.224.2", + "jsr:@std/fs@~0.229.1": "0.229.1", + "jsr:@std/path@~0.225.1": "0.225.1", + "jsr:@std/toml@0.224": "0.224.0", + "npm:@solana/web3.js@^1.91.8": "1.91.8", + "npm:@types/node@*": "18.16.19", + "npm:tweetnacl@^1.0.3": "1.0.3" + }, + "jsr": { + "@std/assert@0.225.3": { + "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f" + }, + "@std/collections@0.224.2": { + "integrity": "e77819455294e92d4e7ddad1dbfd46f94174c09318e541e6621fac4a4d0ab326" + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d" + }, + "@std/dotenv@0.224.0": { + "integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d" + }, + "@std/encoding@0.224.2": { + "integrity": "bd9ef45d6028296440005804db7fc8fcc86e72fc1a19bb77684fb39c4a613672" + }, + "@std/fs@0.229.1": { + "integrity": "38d3fb31f0ca0a8c1118e039939188f32e291a3f7f17dc0868fec22024bdfadd", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/path" + ] + }, + "@std/path@0.225.1": { + "integrity": "8c3220635a73730eb51fe43de9e10b79e2724a5bb8638b9355d35ae012fd9429", + "dependencies": [ + "jsr:@std/assert" + ] + }, + "@std/toml@0.224.0": { + "integrity": "15a60eb5eac12d8a81a48d99cb1aaed60a21c67dc94c2def696a7c39b815c74f", + "dependencies": [ + "jsr:@std/collections" + ] + } + }, + "npm": { + "@babel/runtime@7.24.6": { + "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "dependencies": [ + "regenerator-runtime" + ] + }, + "@noble/curves@1.4.0": { + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": [ + "@noble/hashes" + ] + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@solana/buffer-layout@4.0.1": { + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": [ + "buffer" + ] + }, + "@solana/web3.js@1.91.8": { + "integrity": "sha512-USa6OS1jbh8zOapRJ/CBZImZ8Xb7AJjROZl5adql9TpOoBN9BUzyyouS5oPuZHft7S7eB8uJPuXWYjMi6BHgOw==", + "dependencies": [ + "@babel/runtime", + "@noble/curves", + "@noble/hashes", + "@solana/buffer-layout", + "agentkeepalive", + "bigint-buffer", + "bn.js", + "borsh", + "bs58", + "buffer", + "fast-stable-stringify", + "jayson", + "node-fetch", + "rpc-websockets", + "superstruct" + ] + }, + "@types/connect@3.4.38": { + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": [ + "@types/node@18.16.19" + ] + }, + "@types/node@12.20.55": { + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==" + }, + "@types/ws@7.4.7": { + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": [ + "@types/node@18.16.19" + ] + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": [ + "jsonparse", + "through" + ] + }, + "agentkeepalive@4.5.0": { + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": [ + "humanize-ms" + ] + }, + "base-x@3.0.9": { + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": [ + "safe-buffer" + ] + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bigint-buffer@1.1.5": { + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "dependencies": [ + "bindings" + ] + }, + "bindings@1.5.0": { + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": [ + "file-uri-to-path" + ] + }, + "bn.js@5.2.1": { + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "borsh@0.7.0": { + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": [ + "bn.js", + "bs58", + "text-encoding-utf-8" + ] + }, + "bs58@4.0.1": { + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": [ + "base-x" + ] + }, + "buffer@6.0.3": { + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, + "bufferutil@4.0.8": { + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "dependencies": [ + "node-gyp-build@4.8.0" + ] + }, + "commander@2.20.3": { + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "delay@5.0.0": { + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==" + }, + "es6-promise@4.2.8": { + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify@5.0.0": { + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": [ + "es6-promise" + ] + }, + "eventemitter3@4.0.7": { + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "eyes@0.1.8": { + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" + }, + "fast-stable-stringify@1.0.0": { + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, + "file-uri-to-path@1.0.0": { + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "humanize-ms@1.2.1": { + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": [ + "ms" + ] + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "isomorphic-ws@4.0.1_ws@7.5.9": { + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dependencies": [ + "ws@7.5.9" + ] + }, + "jayson@4.1.0_ws@7.5.9": { + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": [ + "@types/connect", + "@types/node@12.20.55", + "@types/ws", + "JSONStream", + "commander", + "delay", + "es6-promisify", + "eyes", + "isomorphic-ws", + "json-stringify-safe", + "uuid", + "ws@7.5.9" + ] + }, + "json-stringify-safe@5.0.1": { + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node-fetch@2.7.0": { + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": [ + "whatwg-url" + ] + }, + "node-gyp-build@4.8.0": { + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==" + }, + "node-gyp-build@4.8.1": { + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==" + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "rpc-websockets@7.11.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-IkLYjayPv6Io8C/TdCL5gwgzd1hFz2vmBZrjMw/SPEXo51ETOhnzgS4Qy5GWi2JQN7HKHa66J3+2mv0fgNh/7w==", + "dependencies": [ + "bufferutil", + "eventemitter3", + "utf-8-validate", + "uuid", + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10" + ] + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "superstruct@0.14.2": { + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" + }, + "text-encoding-utf-8@1.0.2": { + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tweetnacl@1.0.3": { + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "utf-8-validate@5.0.10": { + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dependencies": [ + "node-gyp-build@4.8.0" + ] + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + }, + "ws@7.5.9": { + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==" + }, + "ws@8.16.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dependencies": [ + "bufferutil", + "utf-8-validate" + ] + }, + "ws@8.17.0_bufferutil@4.0.8_utf-8-validate@5.0.10": { + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "dependencies": [ + "bufferutil", + "utf-8-validate" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/crypto@0.224", + "jsr:@std/dotenv@0.224", + "jsr:@std/encoding@~0.224.2", + "jsr:@std/fs@~0.229.1", + "jsr:@std/toml@0.224", + "npm:@solana/web3.js@^1.91.8", + "npm:tweetnacl@^1.0.3" + ] + } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..68bf2a83 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,342 @@ +# Usage +# Pull images : docker compose pull +# Start: docker compose up -d --wait +# Stop: docker compose down +# Stop and clean: docker compose down -v + +name: flow + +secrets: + flow-server-config: + file: ./.config.toml + +volumes: + db-config: + db-data: + storage: + flow-server-data: + +services: + flow-server: + image: ${IMAGE:-public.ecr.aws/space-operator/flow-server:latest} + restart: unless-stopped + healthcheck: + test: deno eval "fetch('http://flow-server:8080/healthcheck').then(r => {if (r.status !== 200) throw 'error';})" + interval: 5s + timeout: 5s + retries: 10 + ports: + - "127.0.0.1:8080:8080" + depends_on: + db: + condition: service_healthy + kong: + condition: service_healthy + auth: + condition: service_healthy + environment: + MIGRATION_DB_URL: postgres://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + RUST_LOG: info,actix_web=debug,wasmer_compiler_cranelift=warn,db=debug + DENO_DIR: /data/deno + CONFIG_FILE: /run/secrets/flow-server-config + secrets: + - flow-server-config + volumes: + - ./supabase:/space-operator/supabase:ro + - flow-server-data:/data + + kong: + image: kong/kong-gateway:3.7 + restart: unless-stopped + # https://unix.stackexchange.com/a/294837 + entrypoint: bash -c 'eval "echo \"$$(cat /tmp/temp.yml)\"" > /etc/kong/kong.yml && /entrypoint.sh kong docker-start' + ports: + - ${KONG_HTTP_PORT}:8000/tcp + - ${KONG_HTTPS_PORT}:8443/tcp + # IMPORTANT: bind Kong Admin API to 127.0.0.1 to avoid exposing it to the internet + - "127.0.0.1:8001:8001/tcp" + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /etc/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,acme + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + KONG_ADMIN_LISTEN: "0.0.0.0:8001" + KONG_PROXY_ACCESS_LOG: /dev/stdout + KONG_ADMIN_ACCESS_LOG: /dev/stdout + KONG_PROXY_ERROR_LOG: /dev/stderr + KONG_ADMIN_ERROR_LOG: /dev/stderr + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + KONG_ACME_EMAIL: ${KONG_ACME_EMAIL} + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ./volumes/api/kong.yml:/tmp/temp.yml:ro + + db: + image: supabase/postgres:15.1.1.41 + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal # prevents Realtime polling queries from appearing in logs + restart: unless-stopped + ports: + # Pass down internal port because it's set dynamically by other services + - "127.0.0.1:${POSTGRES_PORT}:${POSTGRES_PORT}" + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + FLOW_RUNNER_PASSWORD: ${FLOW_RUNNER_PASSWORD} + volumes: + - ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro + # Must be superuser to create event trigger + - ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro + # Must be superuser to alter reserved role + - ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro + # Changes required for Analytics support + - ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro + # PGDATA directory is persisted between restarts + - db-data:/var/lib/postgresql/data + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + + studio: + image: supabase/studio:20240422-5cf8f30 + restart: unless-stopped + healthcheck: + test: node -e "fetch('http://studio:3000/api/profile').then(r => {if (r.status !== 200) throw 'error';})" + timeout: 5s + interval: 5s + retries: 10 + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + # Comment to use Big Query backend for analytics + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + # Uncomment to use Big Query backend for analytics + # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + + auth: + image: supabase/gotrue:v2.151.0 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + healthcheck: + test: wget --no-verbose --tries=1 --spider http://auth:9999/health + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true + # GOTRUE_SMTP_MAX_FREQUENCY: 1s + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + + # Uncomment to enable custom access token hook. You'll need to create a public.custom_access_token_hook function and grant necessary permissions. + # See: https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token for details + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED="true" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="pg-functions://postgres/public/custom_access_token_hook" + + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED="true" + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/mfa_verification_attempt" + + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED="true" + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/password_verification_attempt" + + rest: + image: postgrest/postgrest:v12.0.1 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: "postgrest" + + realtime: + # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain + image: supabase/realtime:v2.28.32 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + healthcheck: + test: > + curl -sSfL --head -o /dev/null -H "Authorization: Bearer $ANON_KEY" "http://realtime:4000/api/tenants/realtime-dev/health" + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + FLY_ALLOC_ID: fly123 + FLY_APP_NAME: realtime + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + ERL_AFLAGS: -proto_dist inet_tcp + ENABLE_TAILSCALE: "false" + DNS_NODES: "''" + ANON_KEY: ${ANON_KEY} + command: > + sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server" + + storage: + image: supabase/storage-api:v1.0.6 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + healthcheck: + test: wget --no-verbose --tries=1 --spider http://storage:5000/status + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - storage:/var/lib/storage + + imgproxy: + image: darthsim/imgproxy:v3.8.0 + healthcheck: + test: ["CMD", "imgproxy", "health"] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + volumes: + - storage:/var/lib/storage + + meta: + image: supabase/postgres-meta:v0.80.0 + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + functions: + image: supabase/edge-runtime:v1.45.2 + restart: unless-stopped + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + volumes: + - ./volumes/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main diff --git a/docker/env.example b/docker/env.example new file mode 100644 index 00000000..ff1f6da2 --- /dev/null +++ b/docker/env.example @@ -0,0 +1,108 @@ +############ +# Secrets +# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION +############ + +POSTGRES_PASSWORD=postgres +JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long +ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE +SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q +DASHBOARD_USERNAME=supabase +DASHBOARD_PASSWORD=supabase +FLOW_RUNNER_PASSWORD=flow_runner + +############ +# Database - You can change these to any PostgreSQL database that has logical replication enabled. +############ + +POSTGRES_HOST=db +POSTGRES_DB=postgres +POSTGRES_PORT=5432 +# default user is postgres + +############ +# API Proxy - Configuration for the Kong Reverse proxy. +############ + +KONG_ACME_EMAIL=email@example.com +KONG_HTTP_PORT=8000 +KONG_HTTPS_PORT=8443 + + + +############ +# API - Configuration for PostgREST. +############ + +PGRST_DB_SCHEMAS=public,storage,graphql_public + + +############ +# Auth - Configuration for the GoTrue authentication server. +############ + +## General +SITE_URL=http://localhost:3000 +ADDITIONAL_REDIRECT_URLS= +JWT_EXPIRY=3600 +DISABLE_SIGNUP=true +API_EXTERNAL_URL=http://localhost:8000 + +## Mailer Config +MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify" +MAILER_URLPATHS_INVITE="/auth/v1/verify" +MAILER_URLPATHS_RECOVERY="/auth/v1/verify" +MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify" + +## Email auth +ENABLE_EMAIL_SIGNUP=true +ENABLE_EMAIL_AUTOCONFIRM=true +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_HOST=supabase-mail +SMTP_PORT=2500 +SMTP_USER=fake_mail_user +SMTP_PASS=fake_mail_password +SMTP_SENDER_NAME=fake_sender +ENABLE_ANONYMOUS_USERS=false + +## Phone auth +ENABLE_PHONE_SIGNUP=false +ENABLE_PHONE_AUTOCONFIRM=true + + +############ +# Studio - Configuration for the Dashboard +############ + +STUDIO_DEFAULT_ORGANIZATION="Default Organization" +STUDIO_DEFAULT_PROJECT="Default Project" + +STUDIO_PORT=3000 +# replace if you intend to use Studio outside of localhost +SUPABASE_PUBLIC_URL=http://localhost:8000 + +# Enable webp support +IMGPROXY_ENABLE_WEBP_DETECTION=true + +############ +# Functions - Configuration for Functions +############ +# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet. +FUNCTIONS_VERIFY_JWT=false + +############ +# Logs - Configuration for Logflare +# Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction +############ + +LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key + +# Change vector.toml sinks to reflect this change +LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key + +# Docker socket location - this value will differ depending on your OS +DOCKER_SOCKET_LOCATION=/var/run/docker.sock + +# Google Cloud Project details +GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID +GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER diff --git a/docker/flow-server-config.toml b/docker/flow-server-config.toml new file mode 100644 index 00000000..6f8eab11 --- /dev/null +++ b/docker/flow-server-config.toml @@ -0,0 +1,27 @@ +host = "0.0.0.0" +port = 8080 +local_storage = "/data/local_storage" +# helius_api_key = "" +shutdown_timeout_secs = 60 + +cors_origins = [ + # allow all + "*", +] + +[supabase] +endpoint = "http://kong:8000" +jwt_key = "your-super-secret-jwt-token-with-at-least-32-characters-long" +service_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" +anon_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" +open_whitelists = true + +[db] +host = "db" +port = 5432 +user = "flow_runner" +password = "flow_runner" +dbname = "postgres" + +[db.ssl] +enabled = false diff --git a/docker/gen-secrets.ts b/docker/gen-secrets.ts new file mode 100755 index 00000000..1626e01d --- /dev/null +++ b/docker/gen-secrets.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write + +import {encodeBase64Url, encodeBase58, encodeBase64} from "@std/encoding"; +import {load} from "@std/dotenv"; +import {crypto} from "@std/crypto/crypto"; +import * as toml from "@std/toml"; +import * as fs from "@std/fs"; + +const ENV_PATH = ".env"; +const CONFIG_PATH = ".config.toml"; +const ENV_TEMPLATE = "env.example"; +const CONFIG_TEMPLATE = "flow-server-config.toml"; + +async function initHmac(secret: string): Promise { + return await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { + name: "HMAC", + hash: "SHA-256", + }, + false, + ["sign"] + ); +} + +async function generateKey(secret: CryptoKey, role: string): Promise { + const now = Math.floor(new Date().getTime() / 1000); + const payload = JSON.stringify({ + role, + iss: "supabase", + iat: now, + exp: now + 5 * 365 * 3600 * 24, // 5 years + }); + const headers = JSON.stringify({ + alg: "HS256", + typ: "JWT", + }); + const data = `${encodeBase64Url(headers)}.${encodeBase64Url(payload)}`; + const signature = await crypto.subtle.sign( + { + name: "HMAC", + hash: "SHA-256", + }, + secret, + new TextEncoder().encode(data) + ); + return `${data}.${encodeBase64Url(signature)}`; +} + +const encryptionKey = encodeBase64(crypto.getRandomValues(new Uint8Array(32))); + +const jwtSecret = encodeBase64(crypto.getRandomValues(new Uint8Array(64))); + +const hmacKey = await initHmac(jwtSecret); +const anonKey = await generateKey(hmacKey, "anon"); +const serviceRoleKey = await generateKey(hmacKey, "service_role"); + +const postgresPassword = encodeBase58( + crypto.getRandomValues(new Uint8Array(15)) +); +const flowRunnerPassword = encodeBase58( + crypto.getRandomValues(new Uint8Array(15)) +); +const dashboardPassword = encodeBase58( + crypto.getRandomValues(new Uint8Array(8)) +); + +const env = await load({envPath: ENV_TEMPLATE}); +env["POSTGRES_PASSWORD"] = postgresPassword; +env["JWT_SECRET"] = jwtSecret; +env["ANON_KEY"] = anonKey; +env["SERVICE_ROLE_KEY"] = serviceRoleKey; +env["DASHBOARD_PASSWORD"] = dashboardPassword; +env["FLOW_RUNNER_PASSWORD"] = flowRunnerPassword; +env["ENCRYPTION_KEY"] = encryptionKey; +const envContent = + Object.entries(env) + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join("\n") + "\n"; + +// deno-lint-ignore no-explicit-any +const config: any = toml.parse(await Deno.readTextFile(CONFIG_TEMPLATE)); +config.supabase.jwt_key = jwtSecret; +config.supabase.service_key = serviceRoleKey; +config.supabase.anon_key = anonKey; +config.db.password = flowRunnerPassword; +config.db.encryption_key = encryptionKey; +const configContent = toml.stringify(config) + "\n"; + +const fileExists: string[] = []; +if (await fs.exists(ENV_PATH)) fileExists.push(ENV_PATH); +if (await fs.exists(CONFIG_PATH)) fileExists.push(CONFIG_PATH); +if (fileExists.length > 0) { + console.log("Secret files already exist, please remove them before running:"); + for (const path of fileExists) { + console.log(`\t${path}`); + } + Deno.exit(1); +} + +console.log("Writing", ENV_PATH); +await Deno.writeTextFile(ENV_PATH, envContent); + +console.log("Writing", CONFIG_PATH); +await Deno.writeTextFile(CONFIG_PATH, configContent); diff --git a/docker/import-data.ts b/docker/import-data.ts new file mode 100755 index 00000000..6be5bcd1 --- /dev/null +++ b/docker/import-data.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net --allow-read + +import { load } from "@std/dotenv"; + +function getEnv(key: string): string { + const value = Deno.env.get(key); + if (value === undefined) + throw new Error(`environment variable ${key} not found`); + return value; +} + +await load({ export: true }); +const SERVICE_ROLE_KEY = getEnv("SERVICE_ROLE_KEY"); +const APIKEY = getEnv("APIKEY"); +console.log("Exporting data from https://dev-api.spaceoperator.com"); +const exportResp = await fetch( + "https://dev-api.spaceoperator.com/data/export", + { + method: "POST", + headers: { + "accept-encoding": "br, gzip", + "x-api-key": APIKEY, + }, + } +); +if (exportResp.status !== 200) { + console.error(await exportResp.text()); + Deno.exit(1); +} +const data = await exportResp.json(); +const SERVER = `http://127.0.0.1:8080`; +console.log(`Importing data to ${SERVER}`); +const importResp = await fetch( + `${SERVER}/data/import`, + { + headers: { + authorization: `Bearer ${SERVICE_ROLE_KEY}`, + "content-type": "application/json", + }, + body: JSON.stringify(data), + method: "POST", + } +); +if (importResp.status !== 200) { + console.error("Error:", importResp.status); + console.error(await importResp.text()); + Deno.exit(1); +} diff --git a/docker/supabase/migrations/20240514130738_init.sql b/docker/supabase/migrations/20240514130738_init.sql new file mode 100644 index 00000000..8dbb6dce --- /dev/null +++ b/docker/supabase/migrations/20240514130738_init.sql @@ -0,0 +1,651 @@ +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; + +CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium"; + +COMMENT ON SCHEMA "public" IS 'standard public schema'; + +CREATE EXTENSION IF NOT EXISTS "autoinc" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "http" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "moddatetime" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; + +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions"; + +CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; + + +CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" + LANGUAGE "plpgsql" SECURITY DEFINER + AS $$BEGIN + INSERT INTO public.users_public + (email, user_id, username, pub_key) + VALUES ( + new.email, + new.id, + new.raw_user_meta_data->>'pub_key', + new.raw_user_meta_data->>'pub_key' + ); + + INSERT INTO public.user_quotas (user_id) VALUES (new.id); + + INSERT INTO public.wallets (user_id, public_key, type, name, description) + VALUES (new.id, new.raw_user_meta_data->>'pub_key', 'ADAPTER', 'Main wallet', 'Wallet used to sign up'); + + RETURN new; +END;$$; + +CREATE OR REPLACE FUNCTION "public"."increase_credit"("user_id" "uuid", "amount" bigint) RETURNS bigint + LANGUAGE "sql" + AS $_$UPDATE user_quotas SET credit = credit + $2 WHERE user_id = $1 AND $2 >= 0 RETURNING credit;$_$; + +CREATE OR REPLACE FUNCTION "public"."increase_used_credit"("user_id" "uuid", "amount" bigint) RETURNS bigint + LANGUAGE "sql" + AS $_$UPDATE user_quotas SET used_credit = used_credit + $2 WHERE user_id = $1 AND $2 >= 0 AND used_credit + $2 <= credit RETURNING used_credit;$_$; + +CREATE OR REPLACE FUNCTION "public"."is_nft_admin"("user_id" "uuid") RETURNS boolean + LANGUAGE "sql" STABLE SECURITY DEFINER + AS $_$SELECT EXISTS (SELECT user_id FROM nft_admins WHERE user_id = $1);$_$; + +SET default_tablespace = ''; + +SET default_table_access_method = "heap"; + +CREATE TABLE IF NOT EXISTS "public"."apikeys" ( + "key_hash" "text" NOT NULL, + "user_id" "uuid" NOT NULL, + "name" "text" NOT NULL, + "trimmed_key" "text" NOT NULL, + "created_at" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."flow_run" ( + "user_id" "uuid" NOT NULL, + "id" "uuid" NOT NULL, + "flow_id" integer NOT NULL, + "start_time" timestamp without time zone, + "end_time" timestamp without time zone, + "not_run" "uuid"[], + "output" "jsonb", + "errors" "text"[], + "inputs" "jsonb" NOT NULL, + "environment" "jsonb" NOT NULL, + "instructions_bundling" "jsonb" NOT NULL, + "network" "jsonb" NOT NULL, + "call_depth" integer NOT NULL, + "origin" "jsonb" NOT NULL, + "nodes" "jsonb"[] NOT NULL, + "edges" "jsonb"[] NOT NULL, + "collect_instructions" boolean NOT NULL, + "partial_config" "jsonb", + "signers" "jsonb" NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."flow_run_logs" ( + "user_id" "uuid" NOT NULL, + "flow_run_id" "uuid" NOT NULL, + "log_index" integer NOT NULL, + "node_id" "uuid", + "times" integer, + "time" timestamp without time zone NOT NULL, + "log_level" character varying(5) NOT NULL, + "content" "text" NOT NULL, + "module" "text" +); + +CREATE TABLE IF NOT EXISTS "public"."flow_run_shared" ( + "flow_run_id" "uuid" NOT NULL, + "user_id" "uuid" NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."flows" ( + "id" integer NOT NULL, + "user_id" "uuid" DEFAULT "auth"."uid"() NOT NULL, + "name" "text" DEFAULT ''::"text" NOT NULL, + "isPublic" boolean DEFAULT false NOT NULL, + "description" "text" DEFAULT 'Flow Description'::"text" NOT NULL, + "tags" "text"[] DEFAULT '{}'::"text"[] NOT NULL, + "created_at" "date" DEFAULT "now"() NOT NULL, + "parent_flow" bigint, + "viewport" "jsonb" DEFAULT '{"x": 524, "y": 268, "zoom": 0.5}'::"jsonb" NOT NULL, + "uuid" "uuid" DEFAULT "extensions"."uuid_generate_v4"(), + "updated_at" timestamp without time zone, + "lastest_flow_run_id" "uuid", + "custom_networks" "jsonb"[] DEFAULT '{}'::"jsonb"[] NOT NULL, + "current_network" "jsonb" DEFAULT '{"id": "01000000-0000-8000-8000-000000000000", "url": "https://api.devnet.solana.com", "type": "default", "wallet": "Solana", "cluster": "devnet"}'::"jsonb" NOT NULL, + "instructions_bundling" "jsonb" DEFAULT '"Off"'::"jsonb" NOT NULL, + "guide" "jsonb", + "environment" "jsonb", + "nodes" "jsonb"[], + "edges" "jsonb"[], + "mosaic" "jsonb", + "start_shared" boolean DEFAULT false NOT NULL, + "start_unverified" boolean DEFAULT false NOT NULL +); + +COMMENT ON COLUMN "public"."flows"."isPublic" IS 'To know if this flow is public or not'; + +COMMENT ON COLUMN "public"."flows"."parent_flow" IS 'This means the flow was cloned'; + +COMMENT ON COLUMN "public"."flows"."viewport" IS 'flow viewport'; + +ALTER TABLE "public"."flows" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."flows_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + +CREATE TABLE IF NOT EXISTS "public"."kvstore" ( + "user_id" "uuid" DEFAULT "auth"."uid"() NOT NULL, + "store_name" "text" NOT NULL, + "key" "text" NOT NULL, + "value" "jsonb" NOT NULL, + "last_updated" timestamp without time zone DEFAULT "now"() +); + +CREATE TABLE IF NOT EXISTS "public"."kvstore_metadata" ( + "user_id" "uuid" DEFAULT "auth"."uid"() NOT NULL, + "store_name" "text" NOT NULL, + "stats_size" bigint DEFAULT 0 NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."node_run" ( + "user_id" "uuid" NOT NULL, + "flow_run_id" "uuid" NOT NULL, + "node_id" "uuid" NOT NULL, + "times" integer NOT NULL, + "start_time" timestamp without time zone, + "end_time" timestamp without time zone, + "output" "jsonb", + "errors" "text"[], + "input" "jsonb" DEFAULT '{"M": {}}'::"jsonb" NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."nodes" ( + "id" bigint NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"(), + "name" "text" DEFAULT ''::"text", + "user_id" "uuid" DEFAULT "auth"."uid"(), + "type" "text" DEFAULT 'mock'::"text", + "sources" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL, + "targets" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL, + "targets_form.json_schema" "jsonb", + "data" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL, + "targets_form.ui_schema" "jsonb" DEFAULT '{}'::"jsonb", + "targets_form.form_data" "jsonb" DEFAULT '{}'::"jsonb", + "status" "text" DEFAULT 'active'::"text", + "unique_node_id" "text", + "isPublic" boolean DEFAULT false, + "targets_form.extra" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL, + "storage_path" "text", + "licenses" "text"[] +); + +COMMENT ON TABLE "public"."nodes" IS 'Nodes Table'; + +COMMENT ON COLUMN "public"."nodes"."data" IS 'data'; + +COMMENT ON COLUMN "public"."nodes"."unique_node_id" IS 'Node id i.e http.0.1'; + +COMMENT ON COLUMN "public"."nodes"."storage_path" IS 'Path to where wasm file is stored'; + +ALTER TABLE "public"."nodes" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."nodes_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + +CREATE TABLE IF NOT EXISTS "public"."pubkey_whitelists" ( + "pubkey" "text" NOT NULL, + "info" "text" +); + +CREATE SEQUENCE IF NOT EXISTS "public"."seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE IF NOT EXISTS "public"."signature_requests" ( + "user_id" "uuid" NOT NULL, + "id" bigint NOT NULL, + "created_at" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "msg" "text" NOT NULL, + "pubkey" "text" NOT NULL, + "signature" "text", + "flow_run_id" "uuid", + "signatures" "jsonb"[], + "new_msg" "text" +); + +CREATE SEQUENCE IF NOT EXISTS "public"."signature_requests_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE "public"."signature_requests_id_seq" OWNED BY "public"."signature_requests"."id"; + +CREATE TABLE IF NOT EXISTS "public"."user_quotas" ( + "user_id" "uuid" NOT NULL, + "kvstore_count" bigint DEFAULT 0 NOT NULL, + "kvstore_count_limit" bigint DEFAULT 100 NOT NULL, + "kvstore_size" bigint DEFAULT 0 NOT NULL, + "kvstore_size_limit" bigint DEFAULT ((1024 * 1024) * 100) NOT NULL, + "credit" bigint DEFAULT '30'::bigint NOT NULL, + "used_credit" bigint DEFAULT 0 NOT NULL +); + +CREATE TABLE IF NOT EXISTS "public"."users_public" ( + "email" "text" NOT NULL, + "user_id" "uuid" NOT NULL, + "username" "text" DEFAULT ''::"text", + "description" "text" DEFAULT ''::"text", + "pub_key" "text" NOT NULL, + "status" "text" DEFAULT 'not_available'::"text" NOT NULL, + "updated_at" timestamp without time zone DEFAULT "now"(), + "avatar" "text" DEFAULT ''::"text", + "flow_skills" "jsonb" DEFAULT '[]'::"jsonb", + "node_skills" "jsonb" DEFAULT '[]'::"jsonb", + "tasks_skills" "jsonb" DEFAULT '[]'::"jsonb" +); + +COMMENT ON TABLE "public"."users_public" IS 'Profile data for each user.'; + +COMMENT ON COLUMN "public"."users_public"."pub_key" IS 'Public Key'; + +COMMENT ON COLUMN "public"."users_public"."status" IS 'I am available for work'; + +CREATE TABLE IF NOT EXISTS "public"."wallets" ( + "id" bigint NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"(), + "type" "text" DEFAULT 'ADAPTER'::"text", + "adapter" "text" DEFAULT ''::"text", + "public_key" "text", + "user_id" "uuid" NOT NULL, + "description" "text" DEFAULT 'Wallet used for payments'::"text" NOT NULL, + "name" "text" DEFAULT ''::"text" NOT NULL, + "icon" "text", + "keypair" "text" +); + +ALTER TABLE "public"."wallets" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."wallets_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + +ALTER TABLE ONLY "public"."signature_requests" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."signature_requests_id_seq"'::"regclass"); + +ALTER TABLE ONLY "public"."apikeys" + ADD CONSTRAINT "apikeys_pkey" PRIMARY KEY ("key_hash"); + +ALTER TABLE ONLY "public"."flow_run_logs" + ADD CONSTRAINT "flow_run_logs_pkey" PRIMARY KEY ("flow_run_id", "log_index"); + +ALTER TABLE ONLY "public"."flow_run" + ADD CONSTRAINT "flow_run_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "public"."flow_run_shared" + ADD CONSTRAINT "flow_run_shared_pkey" PRIMARY KEY ("flow_run_id", "user_id"); + +ALTER TABLE ONLY "public"."flows" + ADD CONSTRAINT "flows_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "public"."kvstore_metadata" + ADD CONSTRAINT "kvstore_metadata_pkey" PRIMARY KEY ("user_id", "store_name"); + +ALTER TABLE ONLY "public"."node_run" + ADD CONSTRAINT "node_run_pkey" PRIMARY KEY ("flow_run_id", "node_id", "times"); + +ALTER TABLE ONLY "public"."nodes" + ADD CONSTRAINT "nodes_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "public"."nodes" + ADD CONSTRAINT "nodes_unique_node_id_key" UNIQUE ("unique_node_id"); + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "pubkey_unique" UNIQUE ("pub_key"); + +ALTER TABLE ONLY "public"."pubkey_whitelists" + ADD CONSTRAINT "pubkey_whitelists_pkey" PRIMARY KEY ("pubkey"); + +ALTER TABLE ONLY "public"."signature_requests" + ADD CONSTRAINT "signature_requests_pkey" PRIMARY KEY ("user_id", "id"); + +ALTER TABLE ONLY "public"."apikeys" + ADD CONSTRAINT "uc-user_id-name" UNIQUE ("user_id", "name"); + +ALTER TABLE ONLY "public"."kvstore" + ADD CONSTRAINT "uq_user_id_store_name_key" PRIMARY KEY ("user_id", "store_name", "key"); + +ALTER TABLE ONLY "public"."user_quotas" + ADD CONSTRAINT "user_quotas_pkey" PRIMARY KEY ("user_id"); + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "users_public_email_key" UNIQUE ("email"); + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "users_public_pkey" PRIMARY KEY ("user_id"); + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "users_public_pub_key_key" UNIQUE ("pub_key"); + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "users_public_username_key" UNIQUE ("username"); + +ALTER TABLE ONLY "public"."wallets" + ADD CONSTRAINT "wallets_pkey" PRIMARY KEY ("id"); + +CREATE OR REPLACE TRIGGER "handle_updated_at" BEFORE UPDATE ON "public"."flows" FOR EACH ROW EXECUTE FUNCTION "extensions"."moddatetime"('updated_at'); + +CREATE OR REPLACE TRIGGER "handle_updated_at" BEFORE UPDATE ON "public"."kvstore" FOR EACH ROW EXECUTE FUNCTION "extensions"."moddatetime"('last_updated'); + +CREATE OR REPLACE TRIGGER "handle_updated_at" BEFORE UPDATE ON "public"."users_public" FOR EACH ROW EXECUTE FUNCTION "extensions"."moddatetime"('updated_at'); + +ALTER TABLE ONLY "public"."flow_run" + ADD CONSTRAINT "fk-flow_id" FOREIGN KEY ("flow_id") REFERENCES "public"."flows"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."node_run" + ADD CONSTRAINT "fk-flow_run_id" FOREIGN KEY ("flow_run_id") REFERENCES "public"."flow_run"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run_logs" + ADD CONSTRAINT "fk-flow_run_id" FOREIGN KEY ("flow_run_id") REFERENCES "public"."flow_run"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run_shared" + ADD CONSTRAINT "fk-flow_run_id" FOREIGN KEY ("flow_run_id") REFERENCES "public"."flow_run"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run_logs" + ADD CONSTRAINT "fk-node_run_id" FOREIGN KEY ("flow_run_id", "node_id", "times") REFERENCES "public"."node_run"("flow_run_id", "node_id", "times") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."node_run" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run_logs" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."signature_requests" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."apikeys" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flow_run_shared" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."flows" + ADD CONSTRAINT "flows_lastest_flow_run_id_fkey" FOREIGN KEY ("lastest_flow_run_id") REFERENCES "public"."flow_run"("id") ON DELETE SET NULL; + +ALTER TABLE ONLY "public"."flows" + ADD CONSTRAINT "flows_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."kvstore_metadata" + ADD CONSTRAINT "kvstore_metadata_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."nodes" + ADD CONSTRAINT "nodes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."signature_requests" + ADD CONSTRAINT "signature_requests_flow_run_id_fkey" FOREIGN KEY ("flow_run_id") REFERENCES "public"."flow_run"("id") ON DELETE SET NULL; + +ALTER TABLE ONLY "public"."user_quotas" + ADD CONSTRAINT "user_quotas_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."users_public" + ADD CONSTRAINT "users_public_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +ALTER TABLE ONLY "public"."wallets" + ADD CONSTRAINT "wallets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +CREATE POLICY "Enable delete for users based on user_id" ON "public"."wallets" FOR DELETE TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "Enable insert for authenticated users only" ON "public"."wallets" FOR INSERT TO "authenticated" WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "Enable read access for all users" ON "public"."users_public" FOR SELECT USING (true); + +CREATE POLICY "Enable read access for authenticated users" ON "public"."wallets" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "Enable update for users based on user_id" ON "public"."users_public" FOR UPDATE TO "authenticated" USING (("auth"."uid"() = "user_id")) WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "Enable update for users based on user_id" ON "public"."wallets" FOR UPDATE TO "authenticated" USING (("auth"."uid"() = "user_id")) WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "anon-select" ON "public"."flows" FOR SELECT TO "anon" USING (("isPublic" = true)); + +CREATE POLICY "anon-select" ON "public"."nodes" FOR SELECT TO "anon" USING (("isPublic" = true)); + +ALTER TABLE "public"."apikeys" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "authenticated-delete" ON "public"."flows" FOR DELETE TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-delete" ON "public"."nodes" FOR DELETE TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-insert" ON "public"."flows" FOR INSERT TO "authenticated" WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-insert" ON "public"."nodes" FOR INSERT TO "authenticated" WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select" ON "public"."apikeys" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select" ON "public"."flows" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "user_id") OR ("isPublic" = true))); + +CREATE POLICY "authenticated-select" ON "public"."nodes" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "user_id") OR ("isPublic" = true))); + +CREATE POLICY "authenticated-select" ON "public"."signature_requests" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select-flow_run-shared" ON "public"."flow_run" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "user_id") OR (EXISTS ( SELECT 1 + FROM "public"."flow_run_shared" "s" + WHERE (("s"."flow_run_id" = "flow_run"."id") AND ("s"."user_id" = "auth"."uid"())))))); + +CREATE POLICY "authenticated-select-flow_run_logs-shared" ON "public"."flow_run_logs" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "user_id") OR (EXISTS ( SELECT 1 + FROM "public"."flow_run_shared" "s" + WHERE (("s"."flow_run_id" = "flow_run_logs"."flow_run_id") AND ("s"."user_id" = "auth"."uid"())))))); + +CREATE POLICY "authenticated-select-flow_run_shared" ON "public"."flow_run_shared" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select-kvstore" ON "public"."kvstore" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select-kvstore_metadata" ON "public"."kvstore_metadata" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-select-node_run-shared" ON "public"."node_run" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "user_id") OR (EXISTS ( SELECT 1 + FROM "public"."flow_run_shared" "s" + WHERE (("s"."flow_run_id" = "node_run"."flow_run_id") AND ("s"."user_id" = "auth"."uid"())))))); + +CREATE POLICY "authenticated-select-user_quotas" ON "public"."user_quotas" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-update" ON "public"."flows" FOR UPDATE TO "authenticated" USING (("auth"."uid"() = "user_id")) WITH CHECK (("auth"."uid"() = "user_id")); + +CREATE POLICY "authenticated-update" ON "public"."nodes" FOR UPDATE TO "authenticated" USING ((("auth"."uid"() = "user_id") OR ("isPublic" = true))) WITH CHECK ((("type")::"text" <> 'native'::"text")); + +ALTER TABLE "public"."flow_run" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."flow_run_logs" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."flow_run_shared" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."flows" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."kvstore" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."kvstore_metadata" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "nft_admins-select" ON "public"."user_quotas" FOR SELECT TO "authenticated" USING ("public"."is_nft_admin"("auth"."uid"())); + +CREATE POLICY "nft_admins-update" ON "public"."user_quotas" FOR UPDATE TO "authenticated" USING ("public"."is_nft_admin"("auth"."uid"())); + +ALTER TABLE "public"."node_run" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."nodes" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."pubkey_whitelists" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."signature_requests" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "supabase_auth_admin-select-pubkey_whitelists" ON "public"."pubkey_whitelists" FOR SELECT TO "supabase_auth_admin" USING (true); + +ALTER TABLE "public"."user_quotas" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."users_public" ENABLE ROW LEVEL SECURITY; + +ALTER TABLE "public"."wallets" ENABLE ROW LEVEL SECURITY; + +ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."flow_run"; + +ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."flow_run_logs"; + +ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."node_run"; + +ALTER PUBLICATION "supabase_realtime" ADD TABLE ONLY "public"."signature_requests"; + +REVOKE USAGE ON SCHEMA "public" FROM PUBLIC; +GRANT USAGE ON SCHEMA "public" TO "flow_runner"; +GRANT USAGE ON SCHEMA "auth" TO "flow_runner"; +GRANT USAGE ON SCHEMA "storage" TO "flow_runner"; +GRANT ALL ON SCHEMA "public" TO PUBLIC; + +GRANT ALL ON TABLE "public"."apikeys" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."flow_run" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."flow_run_logs" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."flow_run_shared" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."flows" TO "flow_runner"; + +GRANT SELECT,USAGE ON SEQUENCE "public"."flows_id_seq" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."kvstore" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."kvstore_metadata" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."node_run" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."nodes" TO "flow_runner"; + +GRANT SELECT,USAGE ON SEQUENCE "public"."nodes_id_seq" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."pubkey_whitelists" TO "flow_runner"; + +GRANT SELECT,USAGE ON SEQUENCE "public"."seq" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."signature_requests" TO "flow_runner"; + +GRANT SELECT,USAGE ON SEQUENCE "public"."signature_requests_id_seq" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."user_quotas" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."users_public" TO "flow_runner"; + +GRANT ALL ON TABLE "public"."wallets" TO "flow_runner"; + +GRANT SELECT,USAGE ON SEQUENCE "public"."wallets_id_seq" TO "flow_runner"; + +GRANT SELECT ON TABLE "public"."pubkey_whitelists" TO "supabase_auth_admin"; + +-- +-- Dumped schema changes for auth and storage +-- + +CREATE OR REPLACE FUNCTION "auth"."validate_user"() RETURNS "trigger" + LANGUAGE "plpgsql" + AS $$ +declare +myrec record; +begin + select * into myrec from public.pubkey_whitelists + where pubkey = new.raw_user_meta_data->>'pub_key' and pubkey is not null; + if not found then + raise exception 'pubkey is not in whitelists, %', new.raw_user_meta_data->>'pub_key'; + end if; + + return new; +end; +$$; + +GRANT UPDATE ON TABLE "auth"."users" TO "flow_runner"; + +CREATE TABLE IF NOT EXISTS "auth"."passwords" ( + "user_id" "uuid" NOT NULL, + "password" "text" NOT NULL +); + +ALTER TABLE ONLY "auth"."passwords" + ADD CONSTRAINT "passwords_pkey" PRIMARY KEY ("user_id"); + +CREATE OR REPLACE TRIGGER "on_auth_check_whitelists" BEFORE INSERT ON "auth"."users" FOR EACH ROW EXECUTE FUNCTION "auth"."validate_user"(); + +ALTER TABLE ONLY "auth"."passwords" + ADD CONSTRAINT "fk-user_id" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +GRANT ALL ON TABLE "auth"."passwords" TO "flow_runner"; + +INSERT INTO "storage"."buckets" ("id", "name", "public") VALUES + ('user-storages', 'user-storages', FALSE), + ('user-public-storages', 'user-public-storages', TRUE); + +CREATE POLICY "user-pubic-storages xeg75m_0" ON "storage"."objects" FOR INSERT TO "authenticated" WITH CHECK ((("bucket_id" = 'user-public-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-pubic-storages xeg75m_1" ON "storage"."objects" FOR UPDATE TO "authenticated" USING ((("bucket_id" = 'user-public-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-pubic-storages xeg75m_2" ON "storage"."objects" FOR DELETE TO "authenticated" USING ((("bucket_id" = 'user-public-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-public-storages xeg75m_0" ON "storage"."objects" FOR SELECT TO "authenticated", "anon" USING (("bucket_id" = 'user-public-storages'::"text")); + +CREATE POLICY "user-storage w6lp96_0" ON "storage"."objects" FOR SELECT TO "authenticated" USING ((("bucket_id" = 'user-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-storage w6lp96_1" ON "storage"."objects" FOR INSERT TO "authenticated" WITH CHECK ((("bucket_id" = 'user-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-storage w6lp96_2" ON "storage"."objects" FOR UPDATE TO "authenticated" USING ((("bucket_id" = 'user-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE POLICY "user-storage w6lp96_3" ON "storage"."objects" FOR DELETE TO "authenticated" USING ((("bucket_id" = 'user-storages'::"text") AND ("path_tokens"[1] = ("auth"."uid"())::"text"))); + +CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" +LANGUAGE "plpgsql" +SECURITY DEFINER +AS $$ +BEGIN + INSERT INTO public.users_public + (email, user_id, username, pub_key) + VALUES ( + new.email, + new.id, + new.raw_user_meta_data->>'pub_key', + new.raw_user_meta_data->>'pub_key' + ); + + INSERT INTO public.user_quotas (user_id) VALUES (new.id); + + INSERT INTO public.wallets (user_id, public_key, type, name, description) + VALUES (new.id, new.raw_user_meta_data->>'pub_key', 'ADAPTER', 'Main wallet', 'Wallet used to sign up'); + + RETURN new; +END; +$$; + +CREATE OR REPLACE TRIGGER "on_auth_user_created" AFTER INSERT ON "auth"."users" FOR EACH ROW EXECUTE FUNCTION "public"."handle_new_user"(); + +RESET ALL; diff --git a/docker/supabase/migrations/20240517061121_grant.sql b/docker/supabase/migrations/20240517061121_grant.sql new file mode 100644 index 00000000..5346009c --- /dev/null +++ b/docker/supabase/migrations/20240517061121_grant.sql @@ -0,0 +1,6 @@ +GRANT USAGE ON SCHEMA auth TO flow_runner; +GRANT SELECT ON ALL TABLES IN SCHEMA auth TO flow_runner; +GRANT SELECT ON ALL SEQUENCES IN SCHEMA auth TO flow_runner; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA auth TO flow_runner; +GRANT ALL ON TABLE auth.users TO flow_runner; +GRANT ALL ON TABLE auth.identities TO flow_runner; diff --git a/docker/supabase/migrations/20240518143018_auth_trigger.sql b/docker/supabase/migrations/20240518143018_auth_trigger.sql new file mode 100644 index 00000000..04f4519f --- /dev/null +++ b/docker/supabase/migrations/20240518143018_auth_trigger.sql @@ -0,0 +1,17 @@ +CREATE OR REPLACE FUNCTION auth.disable_users_triggers() +RETURNS void +LANGUAGE SQL +AS $$ +ALTER TABLE auth.users DISABLE TRIGGER on_auth_user_created; +$$ SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION auth.disable_users_triggers() to flow_runner; + +CREATE OR REPLACE FUNCTION auth.enable_users_triggers() +RETURNS void +LANGUAGE SQL +AS $$ +ALTER TABLE auth.users ENABLE TRIGGER on_auth_user_created; +$$ SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION auth.enable_users_triggers() to flow_runner; diff --git a/docker/supabase/migrations/20240524150823_grant_sequence.sql b/docker/supabase/migrations/20240524150823_grant_sequence.sql new file mode 100644 index 00000000..5dde7ff4 --- /dev/null +++ b/docker/supabase/migrations/20240524150823_grant_sequence.sql @@ -0,0 +1 @@ +GRANT UPDATE ON ALL SEQUENCES IN SCHEMA public TO flow_runner; diff --git a/docker/supabase/migrations/20240525104546_kvstore_fk.sql b/docker/supabase/migrations/20240525104546_kvstore_fk.sql new file mode 100644 index 00000000..64c023ac --- /dev/null +++ b/docker/supabase/migrations/20240525104546_kvstore_fk.sql @@ -0,0 +1,14 @@ +ALTER TABLE kvstore +ADD CONSTRAINT kvstore_user_id_store_name_fkey +FOREIGN KEY (user_id, store_name) REFERENCES kvstore_metadata (user_id, store_name) +ON DELETE CASCADE; + +ALTER TABLE kvstore +ADD CONSTRAINT kvstore_user_id_fkey +FOREIGN KEY (user_id) REFERENCES auth.users (id) +ON DELETE CASCADE; + +ALTER TABLE kvstore_metadata +ADD CONSTRAINT kvstore_metadata_user_id_user_quotas_fkey +FOREIGN KEY (user_id) REFERENCES user_quotas (user_id) +ON DELETE CASCADE; diff --git a/docker/supabase/migrations/20240905183752_encrypt.sql b/docker/supabase/migrations/20240905183752_encrypt.sql new file mode 100644 index 00000000..922b2236 --- /dev/null +++ b/docker/supabase/migrations/20240905183752_encrypt.sql @@ -0,0 +1,2 @@ +ALTER TABLE wallets ADD COLUMN encrypted_keypair JSONB NULL; +UPDATE wallets SET encrypted_keypair['raw'] = to_json(keypair) WHERE keypair IS NOT NULL AND encrypted_keypair IS NULL; diff --git a/docker/supabase/migrations/20240906100833_grant_wallet_update.sql b/docker/supabase/migrations/20240906100833_grant_wallet_update.sql new file mode 100644 index 00000000..959cf6ba --- /dev/null +++ b/docker/supabase/migrations/20240906100833_grant_wallet_update.sql @@ -0,0 +1 @@ +GRANT INSERT, UPDATE ON wallets TO flow_runner; diff --git a/docker/supabase/migrations/20240907035451_update_wallets_table.sql b/docker/supabase/migrations/20240907035451_update_wallets_table.sql new file mode 100644 index 00000000..785febb6 --- /dev/null +++ b/docker/supabase/migrations/20240907035451_update_wallets_table.sql @@ -0,0 +1,3 @@ +ALTER TABLE wallets +ALTER COLUMN public_key TYPE text, +ALTER COLUMN public_key SET NOT NULL; diff --git a/docker/supabase/migrations/20240930062601_remove_keypair.sql b/docker/supabase/migrations/20240930062601_remove_keypair.sql new file mode 100644 index 00000000..1610804a --- /dev/null +++ b/docker/supabase/migrations/20240930062601_remove_keypair.sql @@ -0,0 +1 @@ +ALTER TABLE wallets DROP COLUMN keypair; diff --git a/docker/supabase/migrations/20241008071914_update_nodes.sql b/docker/supabase/migrations/20241008071914_update_nodes.sql new file mode 100644 index 00000000..72bd6124 --- /dev/null +++ b/docker/supabase/migrations/20241008071914_update_nodes.sql @@ -0,0 +1,7 @@ +ALTER TABLE nodes ALTER COLUMN data SET DATA TYPE jsonb USING data::jsonb; +ALTER TABLE nodes ALTER COLUMN sources SET DATA TYPE jsonb USING sources::jsonb; +ALTER TABLE nodes ALTER COLUMN targets SET DATA TYPE jsonb USING targets::jsonb; +ALTER TABLE nodes ALTER COLUMN "targets_form.json_schema" SET DATA TYPE jsonb USING "targets_form.json_schema"::jsonb; +ALTER TABLE nodes ALTER COLUMN "targets_form.ui_schema" SET DATA TYPE jsonb USING "targets_form.ui_schema"::jsonb; +ALTER TABLE nodes ALTER COLUMN "targets_form.form_data" SET DATA TYPE jsonb USING "targets_form.form_data"::jsonb; +ALTER TABLE nodes ALTER COLUMN "targets_form.extra" SET DATA TYPE jsonb USING "targets_form.extra"::jsonb; diff --git a/docker/supabase/migrations/20241008143527_nodes_update_policy.sql b/docker/supabase/migrations/20241008143527_nodes_update_policy.sql new file mode 100644 index 00000000..fc21accd --- /dev/null +++ b/docker/supabase/migrations/20241008143527_nodes_update_policy.sql @@ -0,0 +1 @@ +ALTER POLICY "authenticated-update" ON nodes TO authenticated USING (auth.uid() = user_id); diff --git a/docker/supabase/migrations/20241008144128_with_check.sql b/docker/supabase/migrations/20241008144128_with_check.sql new file mode 100644 index 00000000..fadc97bc --- /dev/null +++ b/docker/supabase/migrations/20241008144128_with_check.sql @@ -0,0 +1 @@ +ALTER POLICY "authenticated-update" ON nodes TO authenticated USING (auth.uid() = user_id) WITH CHECK (true); diff --git a/docker/supabase/migrations/20241030051429_check_native_nodes.sql b/docker/supabase/migrations/20241030051429_check_native_nodes.sql new file mode 100644 index 00000000..7fec617d --- /dev/null +++ b/docker/supabase/migrations/20241030051429_check_native_nodes.sql @@ -0,0 +1,3 @@ +ALTER TABLE nodes ADD CONSTRAINT native_check CHECK ( + type <> 'native' OR user_id IS NULL OR "isPublic" = FALSE +) NO INHERIT; diff --git a/docker/supabase/migrations/20241202120303_update_flows_table.sql b/docker/supabase/migrations/20241202120303_update_flows_table.sql new file mode 100644 index 00000000..aa6aa68b --- /dev/null +++ b/docker/supabase/migrations/20241202120303_update_flows_table.sql @@ -0,0 +1,8 @@ +UPDATE flows SET nodes = '{}'::jsonb[] WHERE nodes IS NULL; +UPDATE flows SET edges = '{}'::jsonb[] WHERE edges IS NULL; +UPDATE flows SET environment = '{}'::jsonb WHERE environment IS NULL; +ALTER TABLE flows +ALTER COLUMN nodes SET DEFAULT '{}'::jsonb[], ALTER COLUMN nodes SET NOT NULL, +ALTER COLUMN edges SET DEFAULT '{}'::jsonb[], ALTER COLUMN edges SET NOT NULL, +ALTER COLUMN parent_flow TYPE INTEGER, +ALTER COLUMN environment SET DEFAULT '{}'::jsonb, ALTER COLUMN environment SET NOT NULL; diff --git a/docker/supabase/migrations/20241214133549_flow_deployment.sql b/docker/supabase/migrations/20241214133549_flow_deployment.sql new file mode 100644 index 00000000..c13a9429 --- /dev/null +++ b/docker/supabase/migrations/20241214133549_flow_deployment.sql @@ -0,0 +1,77 @@ +CREATE TABLE flow_deployments ( + id UUID NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + user_id UUID NOT NULL, + entrypoint INTEGER NOT NULL, + start_permission JSONB NOT NULL, + output_instructions BOOL NOT NULL, + action_identity TEXT NULL, + action_config JSONB NULL, + fees JSONB [] NOT NULL, + PRIMARY KEY (id), + UNIQUE (id, entrypoint) +); + +-- Wallet used in a deployment +CREATE TABLE flow_deployments_wallets ( + user_id UUID NOT NULL, + deployment_id UUID NOT NULL, + wallet_id BIGINT NOT NULL, + PRIMARY KEY (deployment_id, wallet_id), + FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE, + FOREIGN KEY (deployment_id) REFERENCES flow_deployments (id) ON DELETE CASCADE +); + +-- Flow used in a deployment +CREATE TABLE flow_deployments_flows ( + deployment_id UUID NOT NULL, + flow_id INTEGER NOT NULL, + user_id UUID NOT NULL, + data JSONB NOT NULL, + PRIMARY KEY (deployment_id, flow_id), + FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE, + FOREIGN KEY (deployment_id) REFERENCES flow_deployments (id) ON DELETE CASCADE +); + +-- Tags to assign human-frienly references to flow deployments +CREATE TABLE flow_deployments_tags ( + entrypoint INTEGER NOT NULL, + tag TEXT NOT NULL, + deployment_id UUID NOT NULL, + user_id UUID NOT NULL, + PRIMARY KEY (entrypoint, tag), + FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE, + FOREIGN KEY (deployment_id) REFERENCES flow_deployments (id) ON DELETE CASCADE, + FOREIGN KEY (deployment_id, entrypoint) REFERENCES flow_deployments (id, entrypoint) +); + +create or replace function flow_deployments_insert() returns trigger as $flow_deployments_insert$ +begin + insert into + flow_deployments_tags(entrypoint, tag, deployment_id, user_id) + values(new.entrypoint, 'latest', new.id, new.user_id) + on conflict (entrypoint, tag) + do update set deployment_id = new.id; + return new; +end; +$flow_deployments_insert$ +language plpgsql +security definer; + +create trigger flow_deployments_insert after insert on flow_deployments +for each row execute function flow_deployments_insert(); + +GRANT SELECT, INSERT ON flow_deployments TO flow_runner; +GRANT SELECT, INSERT ON flow_deployments_wallets TO flow_runner; +GRANT SELECT, INSERT ON flow_deployments_flows TO flow_runner; +GRANT SELECT ON flow_deployments_tags TO flow_runner; + +ALTER TABLE flow_deployments ENABLE ROW LEVEL SECURITY; +ALTER TABLE flow_deployments_wallets ENABLE ROW LEVEL SECURITY; +ALTER TABLE flow_deployments_flows ENABLE ROW LEVEL SECURITY; +ALTER TABLE flow_deployments_tags ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "owner-select" ON flow_deployments FOR SELECT TO authenticated USING (auth.uid() = user_id); +CREATE POLICY "owner-select" ON flow_deployments_wallets FOR SELECT TO authenticated USING (auth.uid() = user_id); +CREATE POLICY "owner-select" ON flow_deployments_flows FOR SELECT TO authenticated USING (auth.uid() = user_id); +CREATE POLICY "owner-select" ON flow_deployments_tags FOR SELECT TO authenticated USING (auth.uid() = user_id); diff --git a/docker/supabase/migrations/20241230141331_wallet_purpose.sql b/docker/supabase/migrations/20241230141331_wallet_purpose.sql new file mode 100644 index 00000000..04ea2418 --- /dev/null +++ b/docker/supabase/migrations/20241230141331_wallet_purpose.sql @@ -0,0 +1 @@ +ALTER TABLE wallets ADD COLUMN IF NOT EXISTS purpose CHARACTER VARYING NULL; diff --git a/docker/supabase/migrations/20241230142807_flow_gg_marketplace.sql b/docker/supabase/migrations/20241230142807_flow_gg_marketplace.sql new file mode 100644 index 00000000..69c8fdc0 --- /dev/null +++ b/docker/supabase/migrations/20241230142807_flow_gg_marketplace.sql @@ -0,0 +1 @@ +ALTER TABLE flows ADD COLUMN IF NOT EXISTS gg_marketplace BOOLEAN NULL; diff --git a/docker/tests/login.ts b/docker/tests/login.ts new file mode 100644 index 00000000..5da364b7 --- /dev/null +++ b/docker/tests/login.ts @@ -0,0 +1,59 @@ +import * as sol from "@solana/web3.js"; +import { decodeBase58, encodeBase58 } from "@std/encoding"; +import { load } from "@std/dotenv"; +import { default as nacl } from "tweetnacl"; + +function getEnv(key: string): string { + const value = Deno.env.get(key); + if (value === undefined) + throw new Error(`environment variable ${key} not found`); + return value; +} + +await load({ export: true }); + +const SERVER = `http://localhost:${getEnv("KONG_HTTP_PORT")}/flow-server`; + +const keyB58 = Deno.env.get("KEYPAIR"); +let key; +if (keyB58 !== undefined) { + key = sol.Keypair.fromSecretKey(decodeBase58(keyB58)); +} else { + console.log("Generating random keypair"); + key = sol.Keypair.generate(); + console.log("key:", encodeBase58(key.secretKey)); +} + +const msg: string = await fetch(`${SERVER}/auth/init`, { + method: "POST", + body: JSON.stringify({ pubkey: key.publicKey.toBase58() }), + headers: { + "content-type": "application/json", + apikey: getEnv("ANON_KEY"), + }, +}) + .then((resp) => resp.json()) + .then((json) => String(json.msg)); + +console.log("message to sign:"); +console.log(msg); + +const signature = nacl.sign.detached( + new TextEncoder().encode(msg), + key.secretKey +); + +const signatureB58 = encodeBase58(signature); + +const token = `${msg}.${signatureB58}`; + +const authResult: string = await fetch(`${SERVER}/auth/confirm`, { + method: "POST", + body: JSON.stringify({ token }), + headers: { + "content-type": "application/json", + apikey: getEnv("ANON_KEY"), + }, +}).then((resp) => resp.json()); + +console.log(authResult); diff --git a/docker/volumes/api/kong.yml b/docker/volumes/api/kong.yml new file mode 100644 index 00000000..b9b35ca8 --- /dev/null +++ b/docker/volumes/api/kong.yml @@ -0,0 +1,258 @@ +_format_version: '2.1' +_transform: true + +plugins: + - name: acme + config: + account_email: $KONG_ACME_EMAIL + allow_any_domain: true + tos_accepted: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: $DASHBOARD_USERNAME + password: $DASHBOARD_PASSWORD + +### +### API Routes +### +services: + ## flow-server + - name: flow-server + _comment: 'Flow Server: /flow-server/* -> http://flow-server:8080/*' + url: http://flow-server:8080/ + routes: + - name: flow-server-all + strip_path: true + paths: + - /flow-server/ + + ## Open Auth routes + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' + url: http://functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + url: http://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/docker/volumes/db/init/data.sql b/docker/volumes/db/init/data.sql new file mode 100755 index 00000000..e69de29b diff --git a/docker/volumes/db/jwt.sql b/docker/volumes/db/jwt.sql new file mode 100644 index 00000000..cfd3b160 --- /dev/null +++ b/docker/volumes/db/jwt.sql @@ -0,0 +1,5 @@ +\set jwt_secret `echo "$JWT_SECRET"` +\set jwt_exp `echo "$JWT_EXP"` + +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; diff --git a/docker/volumes/db/logs.sql b/docker/volumes/db/logs.sql new file mode 100644 index 00000000..22fc2479 --- /dev/null +++ b/docker/volumes/db/logs.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; diff --git a/docker/volumes/db/realtime.sql b/docker/volumes/db/realtime.sql new file mode 100644 index 00000000..4d4b9ffb --- /dev/null +++ b/docker/volumes/db/realtime.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/docker/volumes/db/roles.sql b/docker/volumes/db/roles.sql new file mode 100644 index 00000000..63f186ee --- /dev/null +++ b/docker/volumes/db/roles.sql @@ -0,0 +1,11 @@ +-- NOTE: change to your own passwords for production environments +\set pgpass `echo "$POSTGRES_PASSWORD"` +\set flow_runner_password `echo "$FLOW_RUNNER_PASSWORD"` + +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; + +CREATE ROLE "flow_runner" WITH INHERIT NOCREATEROLE NOCREATEDB LOGIN REPLICATION BYPASSRLS PASSWORD :'flow_runner_password'; diff --git a/docker/volumes/db/webhooks.sql b/docker/volumes/db/webhooks.sql new file mode 100644 index 00000000..5837b861 --- /dev/null +++ b/docker/volumes/db/webhooks.sql @@ -0,0 +1,208 @@ +BEGIN; + -- Create pg_net extension + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + -- Create supabase_functions schema + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + -- supabase_functions.migrations definition + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + -- Initial supabase_functions migration + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + -- supabase_functions.hooks definition + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get( + url, + params, + headers, + timeout_ms + ); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, + 'record', NEW, + 'type', TG_OP, + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA + ); + + SELECT http_post INTO request_id FROM net.http_post( + url, + payload, + params, + headers, + timeout_ms + ); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + + INSERT INTO supabase_functions.hooks + (hook_table_id, hook_name, request_id) + VALUES + (TG_RELID, TG_NAME, request_id); + + RETURN NEW; + END + $function$; + -- Supabase super admin + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + -- Remove unused supabase_pg_net_admin role + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_pg_net_admin' + ) + THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + -- pg_net grants when extension is already enabled + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + -- Event trigger for pg_net + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_event_trigger + WHERE evtname = 'issue_pg_net_access' + ) THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; diff --git a/docker/volumes/functions/main/index.ts b/docker/volumes/functions/main/index.ts new file mode 100644 index 00000000..a094010b --- /dev/null +++ b/docker/volumes/functions/main/index.ts @@ -0,0 +1,94 @@ +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' + +console.log('main function started') + +const JWT_SECRET = Deno.env.get('JWT_SECRET') +const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' + +function getAuthToken(req: Request) { + const authHeader = req.headers.get('authorization') + if (!authHeader) { + throw new Error('Missing authorization header') + } + const [bearer, token] = authHeader.split(' ') + if (bearer !== 'Bearer') { + throw new Error(`Auth header is not 'Bearer {token}'`) + } + return token +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder() + const secretKey = encoder.encode(JWT_SECRET) + try { + await jose.jwtVerify(jwt, secretKey) + } catch (err) { + console.error(err) + return false + } + return true +} + +serve(async (req: Request) => { + if (req.method !== 'OPTIONS' && VERIFY_JWT) { + try { + const token = getAuthToken(req) + const isValidJWT = await verifyJWT(token) + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } catch (e) { + console.error(e) + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } + + const url = new URL(req.url) + const { pathname } = url + const path_parts = pathname.split('/') + const service_name = path_parts[1] + + if (!service_name || service_name === '') { + const error = { msg: 'missing function name in request' } + return new Response(JSON.stringify(error), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + const servicePath = `/home/deno/functions/${service_name}` + console.error(`serving the request with ${servicePath}`) + + const memoryLimitMb = 150 + const workerTimeoutMs = 1 * 60 * 1000 + const noModuleCache = false + const importMapPath = null + const envVarsObj = Deno.env.toObject() + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }) + return await worker.fetch(req) + } catch (e) { + const error = { msg: e.toString() } + return new Response(JSON.stringify(error), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } +}) diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml new file mode 100644 index 00000000..cce46df4 --- /dev/null +++ b/docker/volumes/logs/vector.yml @@ -0,0 +1,232 @@ +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + exclude_containers: + - supabase-vector + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-functions"' + db: '.appname == "supabase-db"' + # Ignores non nginx errors since they are related with kong booting up + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.request.method = req.method + .metadata.request.path = req.path + .metadata.request.protocol = req.protocol + .metadata.response.status_code = req.status + } + if err != null { + abort + } + # Ignores non nginx errors since they are related with kong booting up + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + # PostgREST logs are structured so we separate timestamp from message using regex + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P(self, data: A) -> Result + where + A: serde::de::EnumAccess<'de>, + { + use serde::de::VariantAccess; + let (ty, a) = data.variant::()?; + match ty { + Variant::Null => Ok(Value::Null), + Variant::String => Ok(Value::String(a.newtype_variant()?)), + Variant::Bool => Ok(Value::Bool(a.newtype_variant()?)), + Variant::U64 => Ok(Value::U64(a.newtype_variant()?)), + Variant::I64 => Ok(Value::I64(a.newtype_variant()?)), + Variant::F64 => Ok(Value::F64(a.newtype_variant()?)), + Variant::Decimal => Ok(Value::Decimal(Decimal::deserialize( + a.newtype_variant::>()?.0, + ))), + Variant::I128 => Ok(Value::I128(a.newtype_variant()?)), + Variant::U128 => Ok(Value::U128(a.newtype_variant()?)), + Variant::B32 => Ok(Value::B32(a.newtype_variant::>()?.0)), + Variant::B64 => Ok(Value::B64(a.newtype_variant::>()?.0)), + Variant::Bytes => Ok(Value::Bytes(a.newtype_variant()?)), + Variant::Array => Ok(Value::Array(a.newtype_variant()?)), + Variant::Map => Ok(Value::Map(a.newtype_variant()?)), + } + } +} + +impl<'de> serde::Deserialize<'de> for Value { + /// Turn any `Deserializer` into `Value`, intended to be used + /// with `Value as Deserializer`. + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + if d.is_human_readable() { + text_repr::TextRepr::deserialize(d).map(Into::into) + } else { + d.deserialize_enum(crate::TOKEN, crate::value_type::keys::ALL, ValueVisitor) + } + } +} + +impl<'de> serde::Deserializer<'de> for Value { + type Error = Error; + + fn is_human_readable(&self) -> bool { + false + } + + fn deserialize_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self { + Value::Null => visitor.visit_unit(), + Value::String(s) => visitor.visit_string(s), + Value::Bool(b) => visitor.visit_bool(b), + Value::I64(i) => visitor.visit_i64(i), + Value::U64(u) => visitor.visit_u64(u), + Value::F64(f) => visitor.visit_f64(f), + Value::Decimal(d) => visit_decimal(d, visitor), + Value::I128(i) => visitor.visit_i128(i), + Value::U128(u) => visitor.visit_u128(u), + Value::Array(array) => visit_array(array, visitor), + Value::Map(map) => visit_map(map, visitor), + Value::B32(x) => visit_bytes(&x, visitor), + Value::B64(x) => visit_bytes(&x, visitor), + Value::Bytes(x) => visit_bytes(&x, visitor), + } + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self { + Value::Null => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_newtype_struct( + self, + name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + match name { + crate::decimal::TOKEN => match self { + Value::Decimal(d) => visitor.visit_bytes(&d.serialize()), + Value::I64(i) => visitor.visit_i64(i), + Value::U64(u) => visitor.visit_u64(u), + Value::F64(f) => visitor.visit_f64(f), + Value::String(s) => visitor.visit_string(s), + _ => Err(serde::de::Error::invalid_type( + self.unexpected(), + &"decimal", + )), + }, + #[cfg(feature = "solana")] + crate::with::keypair::TOKEN | crate::with::signature::TOKEN => match self { + Value::B64(b) => visitor.visit_bytes(&b), + Value::Bytes(b) => visitor.visit_bytes(&b), + Value::String(s) => visitor.visit_str(&s), + Value::Array(a) => visit_array(a, visitor), + _ => Err(serde::de::Error::invalid_type( + self.unexpected(), + &"bytes or base58 string", + )), + }, + #[cfg(feature = "solana")] + crate::with::pubkey::TOKEN => match self { + Value::B32(b) => visitor.visit_bytes(&b), + Value::B64(b) => visitor.visit_bytes(&b), + Value::Bytes(b) => visitor.visit_bytes(&b), + Value::String(s) => visitor.visit_str(&s), + Value::Array(a) => visit_array(a, visitor), + Value::Map(m) => visit_map(m, visitor), + _ => Err(serde::de::Error::invalid_type( + self.unexpected(), + &"public key", + )), + }, + _ => visitor.visit_newtype_struct(self), + } + } + + fn deserialize_enum( + self, + name: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + if name == crate::TOKEN { + visitor.visit_enum(ValueEnumAccess(self)) + } else { + let (variant, value) = match self { + Value::Map(value) => { + let mut iter = value.into_iter(); + let (variant, value) = match iter.next() { + Some(v) => v, + None => { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Map, + &"map with a single key", + )); + } + }; + if iter.next().is_some() { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Map, + &"map with a single key", + )); + } + (variant, Some(value)) + } + Value::String(variant) => (variant, None), + other => { + return Err(serde::de::Error::invalid_type( + other.unexpected(), + &"string or map", + )); + } + }; + + visitor.visit_enum(EnumDeserializer { variant, value }) + } + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self { + Value::Decimal(v) => visitor.visit_bytes(&v.serialize()), + Value::B32(v) => visitor.visit_bytes(&v), + Value::B64(v) => visitor.visit_bytes(&v), + Value::Bytes(v) => visitor.visit_bytes(&v), + _ => self.deserialize_any(visitor), + } + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_bytes(visitor) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + unit unit_struct seq tuple + tuple_struct map struct identifier ignored_any + } +} + +fn visit_decimal<'de, V>(mut d: Decimal, visitor: V) -> Result +where + V: serde::de::Visitor<'de>, +{ + d.normalize_assign(); + if d.scale() == 0 { + if d.is_sign_negative() { + if let Some(i) = d.to_i64() { + return visitor.visit_i64(i); + } + } else if let Some(u) = d.to_u64() { + return visitor.visit_u64(u); + } + } + + // this is lossy + if let Some(f) = d.to_f64() { + return visitor.visit_f64(f); + } + + // I think to_f64 never fails, so this might be unreachable + visitor.visit_string(d.to_string()) +} + +impl Value { + pub(self) fn unexpected(&self) -> serde::de::Unexpected { + use serde::de::Unexpected; + match self { + Value::Null => Unexpected::Unit, + Value::String(s) => Unexpected::Str(s), + Value::Bool(b) => Unexpected::Bool(*b), + Value::I64(i) => Unexpected::Signed(*i), + Value::U64(u) => Unexpected::Unsigned(*u), + Value::F64(f) => Unexpected::Float(*f), + Value::Decimal(_) => Unexpected::Other("decimal"), + Value::I128(_) => Unexpected::Other("i128"), + Value::U128(_) => Unexpected::Other("u128"), + Value::Array(_) => Unexpected::Seq, + Value::Map(_) => Unexpected::Map, + Value::B32(_) => Unexpected::Other("[u8; 32]"), + Value::B64(_) => Unexpected::Other("[u8; 64]"), + Value::Bytes(_) => Unexpected::Other("bytes"), + } + } +} + +fn visit_array<'de, V>(array: Vec, visitor: V) -> Result +where + V: serde::de::Visitor<'de>, +{ + let mut deserializer = SeqDeserializer::<_, Error>::new(array.into_iter()); + let seq = visitor.visit_seq(&mut deserializer)?; + deserializer.end()?; + Ok(seq) +} + +fn visit_bytes<'de, V>(b: &[u8], visitor: V) -> Result +where + V: serde::de::Visitor<'de>, +{ + let mut deserializer = SeqDeserializer::<_, Error>::new(b.iter().cloned()); + let seq = visitor.visit_seq(&mut deserializer)?; + deserializer.end()?; + Ok(seq) +} + +impl<'de> serde::de::IntoDeserializer<'de, Error> for Value { + type Deserializer = Self; + + fn into_deserializer(self) -> Self::Deserializer { + self + } +} + +fn visit_map<'de, V>(object: Map, visitor: V) -> Result +where + V: serde::de::Visitor<'de>, +{ + let len = object.len(); + let mut deserializer = MapDeserializer::new(object); + let map = visitor.visit_map(&mut deserializer)?; + let remaining = deserializer.iter.len(); + if remaining == 0 { + Ok(map) + } else { + Err(serde::de::Error::invalid_length( + len, + &"fewer elements in map", + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + use serde::Deserialize; + use std::collections::{HashMap, HashSet}; + + #[test] + fn value_to_value() { + fn t(v: Value) { + assert_eq!(Value::deserialize(v.clone()).unwrap(), v) + } + t(Value::Null); + t(Value::I64(0i64)); + t(Value::String(String::new())); + t(Value::Bool(false)); + t(Value::U64(0)); + t(Value::I64(0)); + t(Value::F64(0.0)); + t(Value::Decimal(Decimal::MAX)); + t(Value::I128(0)); + t(Value::U128(0)); + t(Value::B32([0u8; 32])); + t(Value::B64([0u8; 64])); + t(Value::Bytes(bytes::Bytes::from_static( + "something".as_bytes(), + ))); + t(Value::Array(Vec::new())); + t(Value::Map(Map::new())); + } + + fn de(v: Value) -> T { + T::deserialize(v).unwrap() + } + + #[test] + fn test_primitive() { + assert_eq!(de::(Value::U64(0)), 0u8); + assert_eq!(de::(Value::U64(0)), 0i8); + assert_eq!(de::(Value::I64(1)), 1u8); + assert_eq!(de::(Value::I64(-1)), -1i8); + assert_eq!(de::(Value::F64(0.0)), 0f32); + assert!(!de::(Value::Bool(false))); + assert_eq!(de::(Value::String("abc".to_owned())), "abc"); + assert_eq!(de::(Value::I64(1)), 1f32); + } + + #[test] + fn test_option() { + assert_eq!(de::>(Value::U64(0)), Some(0)); + assert_eq!(de::>(Value::Null), None); + assert_eq!(de::>>(Value::U64(0)), Some(Some(0))); + } + + #[test] + fn test_array() { + assert_eq!( + de::>(Value::Array([Value::U64(0), Value::U64(1)].to_vec())), + vec![0, 1], + ); + + assert_eq!( + de::<(u32, f32, Option, (i32, i32), Vec)>(Value::Array( + [ + Value::U64(0), + Value::F64(0.1), + Value::Null, + Value::Array([Value::I64(1), Value::I64(2),].to_vec()), + Value::Array([Value::String("hello".to_owned())].to_vec()), + ] + .to_vec() + )), + (0u32, 0.1f32, None, (1, 2), ["hello".to_owned()].to_vec()), + ); + + assert_eq!( + de::>(Value::Array([Value::U64(0), Value::U64(1)].to_vec())), + HashSet::from([0, 1]), + ); + } + + #[test] + fn test_wrapper_struct() { + #[derive(Deserialize, Debug, PartialEq)] + struct Unit; + assert_eq!(de::(Value::Null), Unit); + + #[derive(Deserialize, Debug, PartialEq)] + struct Unit1(); + assert_eq!(de::(Value::Array(Vec::new())), Unit1()); + + #[derive(Deserialize, Debug, PartialEq)] + struct NewTypeStruct(i64); + assert_eq!(de::(Value::I64(0)), NewTypeStruct(0)); + + #[derive(Deserialize, Debug, PartialEq)] + struct NewTypeStructTuple((i32,)); + assert_eq!( + de::(Value::Array([Value::I64(0)].to_vec())), + NewTypeStructTuple((0,)) + ); + + #[derive(Deserialize, Debug, PartialEq)] + struct TupleStruct(i32, String, (i32, i32), (), ((),)); + assert_eq!( + de::(Value::Array( + [ + Value::I64(0), + Value::String("hello".to_owned()), + Value::Array([Value::I64(1), Value::I64(2)].to_vec()), + Value::Null, + Value::Array([Value::Null].to_vec()), + ] + .to_vec() + )), + TupleStruct(0, "hello".to_owned(), (1, 2), (), ((),)) + ); + } + + fn bool_true() -> bool { + true + } + + fn some_3() -> Option { + Some(3) + } + + #[test] + fn test_map() { + assert_eq!( + de::>(Value::Map(Map::from([ + ("1".to_owned(), Value::I64(2)), + ("3".to_owned(), Value::I64(4)) + ]))), + HashMap::::from([(1, 2), (3, 4)]) + ); + + #[derive(Deserialize, Debug, PartialEq)] + struct Struct { + x: i32, + #[serde(default = "bool_true")] + b0: bool, + #[serde(rename = "bb")] + b1: bool, + #[serde(flatten)] + flat: Flat, + } + #[derive(Deserialize, Debug, PartialEq)] + struct Flat { + k: String, + #[serde(default = "bool_true")] + b1: bool, + #[serde(default = "some_3")] + opt: Option, + } + assert_eq!( + de::(Value::Map(Map::from([ + ("x".to_owned(), Value::I64(1)), + ("bb".to_owned(), Value::Bool(false)), + ("k".to_owned(), Value::String("hello".to_owned())), + ]))), + Struct { + x: 1, + b0: true, + b1: false, + flat: Flat { + k: "hello".to_owned(), + b1: true, + opt: Some(3), + }, + } + ); + } + + #[test] + fn test_enum() { + #[derive(Deserialize, PartialEq, Debug)] + enum Enum { + Var1, + Var2, + #[serde(rename = "hello")] + Var3, + } + assert_eq!(de::(Value::String("Var1".to_owned())), Enum::Var1); + assert_eq!(de::(Value::String("Var2".to_owned())), Enum::Var2); + assert_eq!(de::(Value::String("hello".to_owned())), Enum::Var3); + + #[derive(Deserialize, PartialEq, Debug)] + #[serde(untagged)] + enum Enum1 { + A { a: u32 }, + BC { b: Option, c: Option }, + } + assert_eq!( + de::(Value::Map(Map::from([("a".to_owned(), Value::U64(0))]))), + Enum1::A { a: 0 } + ); + assert_eq!( + de::(Value::Map(Map::new())), + Enum1::BC { b: None, c: None } + ); + + #[derive(Deserialize, PartialEq, Debug)] + enum Enum2 { + A { a: u32 }, + BC { b: Option, c: Option }, + D, + E(f32), + } + assert_eq!( + de::(Value::Map(Map::from([( + "A".to_owned(), + Value::Map(Map::from([("a".to_owned(), Value::U64(0))])) + )]))), + Enum2::A { a: 0 } + ); + assert_eq!( + de::(Value::Map(Map::from([( + "BC".to_owned(), + Value::Map(Map::new()) + )]))), + Enum2::BC { b: None, c: None } + ); + assert_eq!( + de::(Value::Map(Map::from([("D".to_owned(), Value::Null)]))), + Enum2::D, + ); + assert_eq!( + de::(Value::Map(Map::from([("E".to_owned(), Value::F64(0.0),)]))), + Enum2::E(0.0), + ); + } + + #[test] + fn test_decimal() { + assert_eq!(de::(Value::Decimal(dec!(100.0))), 100); + assert_eq!(de::(Value::Decimal(dec!(100))), 100.0); + assert_eq!(de::(Value::Decimal(dec!(1999.1234))), 1999.1234); + assert_eq!( + de::(Value::Decimal(Decimal::MAX)), + 7.922816251426434e28 + ); + assert_eq!(de::(Value::Decimal(Decimal::from(u64::MAX))), u64::MAX); + } +} diff --git a/lib/flow-value/src/de/const_bytes.rs b/lib/flow-value/src/de/const_bytes.rs new file mode 100644 index 00000000..71b65b3d --- /dev/null +++ b/lib/flow-value/src/de/const_bytes.rs @@ -0,0 +1,68 @@ +pub struct ConstBytes(pub [u8; N]); + +impl serde::Serialize for ConstBytes { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de, const N: usize> serde::Deserialize<'de> for ConstBytes { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_bytes(ConstBytesVisitor::) + } +} + +struct ConstBytesVisitor; + +impl<'de, const N: usize> serde::de::Visitor<'de> for ConstBytesVisitor { + type Value = ConstBytes; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("bytes") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(ConstBytes(<[u8; N]>::try_from(v).map_err(|_| { + serde::de::Error::invalid_length(v.len(), &itoa::Buffer::new().format(N)) + })?)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let size = seq.size_hint(); + if let Some(size) = size { + if size != N { + return Err(serde::de::Error::invalid_length( + size, + &itoa::Buffer::new().format(N), + )); + } + } + + let mut buf = [0u8; N]; + for (i, b) in buf.iter_mut().enumerate() { + *b = seq.next_element::()?.ok_or_else(|| { + serde::de::Error::invalid_length(i, &itoa::Buffer::new().format(N)) + })?; + } + if seq.next_element::()?.is_some() { + return Err(serde::de::Error::invalid_length( + N + 1, + &itoa::Buffer::new().format(N), + )); + } + + Ok(ConstBytes(buf)) + } +} diff --git a/lib/flow-value/src/de/de_enum.rs b/lib/flow-value/src/de/de_enum.rs new file mode 100644 index 00000000..7ed4eb15 --- /dev/null +++ b/lib/flow-value/src/de/de_enum.rs @@ -0,0 +1,144 @@ +use crate::{Error, Value}; +use serde::de::{value::U32Deserializer, IntoDeserializer}; + +pub struct EnumDeserializer { + pub variant: String, + pub value: Option, +} + +impl<'de> serde::de::EnumAccess<'de> for EnumDeserializer { + type Error = Error; + type Variant = VariantDeserializer; + + fn variant_seed(self, seed: V) -> Result<(V::Value, VariantDeserializer), Error> + where + V: serde::de::DeserializeSeed<'de>, + { + let variant = self.variant.into_deserializer(); + let visitor = VariantDeserializer { value: self.value }; + seed.deserialize(variant).map(|v| (v, visitor)) + } +} + +pub struct VariantDeserializer { + pub value: Option, +} + +impl<'de> serde::de::VariantAccess<'de> for VariantDeserializer { + type Error = Error; + + fn unit_variant(self) -> Result<(), Error> { + match self.value { + Some(value) => serde::Deserialize::deserialize(value), + None => Ok(()), + } + } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + match self.value { + Some(value) => seed.deserialize(value), + None => Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"newtype variant", + )), + } + } + + fn tuple_variant(self, _len: usize, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self.value { + Some(Value::Array(v)) => { + if v.is_empty() { + visitor.visit_unit() + } else { + super::visit_array(v, visitor) + } + } + Some(other) => Err(serde::de::Error::invalid_type( + other.unexpected(), + &"tuple variant", + )), + None => Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"tuple variant", + )), + } + } + + fn struct_variant( + self, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + match self.value { + Some(Value::Map(v)) => super::visit_map(v, visitor), + Some(other) => Err(serde::de::Error::invalid_type( + other.unexpected(), + &"struct variant", + )), + None => Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"struct variant", + )), + } + } +} + +pub struct ValueEnumAccess(pub Value); + +impl<'de> serde::de::EnumAccess<'de> for ValueEnumAccess { + type Error = Error; + type Variant = Self; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: serde::de::DeserializeSeed<'de>, + { + let kind = U32Deserializer::::new(self.0.kind() as u32); + let variant = seed.deserialize(kind)?; + Ok((variant, self)) + } +} + +impl<'de> serde::de::VariantAccess<'de> for ValueEnumAccess { + type Error = Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + todo!() + } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + seed.deserialize(self.0) + } + + fn tuple_variant(self, _: usize, _: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::TupleVariant, + &"newtype variant", + )) + } + + fn struct_variant(self, _: &'static [&'static str], _: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::StructVariant, + &"newtype variant", + )) + } +} diff --git a/lib/flow-value/src/de/de_struct.rs b/lib/flow-value/src/de/de_struct.rs new file mode 100644 index 00000000..d6004388 --- /dev/null +++ b/lib/flow-value/src/de/de_struct.rs @@ -0,0 +1,232 @@ +use crate::{Error, Map, Value}; +use serde::de::IntoDeserializer; +use std::borrow::Cow; + +pub struct MapDeserializer { + pub iter: ::IntoIter, + pub value: Option, +} + +impl MapDeserializer { + pub fn new(map: Map) -> Self { + MapDeserializer { + iter: map.into_iter(), + value: None, + } + } +} + +impl<'de> serde::de::MapAccess<'de> for MapDeserializer { + type Error = Error; + + fn next_key_seed(&mut self, seed: T) -> Result, Error> + where + T: serde::de::DeserializeSeed<'de>, + { + match self.iter.next() { + Some((key, value)) => { + self.value = Some(value); + let key_de = MapKeyDeserializer { + key: Cow::Owned(key), + }; + seed.deserialize(key_de).map(Some) + } + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + match self.value.take() { + Some(value) => seed.deserialize(value), + None => Err(serde::de::Error::custom("value is missing")), + } + } + + fn size_hint(&self) -> Option { + match self.iter.size_hint() { + (lower, Some(upper)) if lower == upper => Some(upper), + _ => None, + } + } +} + +pub struct MapKeyDeserializer<'de> { + pub key: Cow<'de, str>, +} + +macro_rules! deserialize_integer_key { + ($method:ident => $visit:ident) => { + fn $method(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match (self.key.parse(), self.key) { + (Ok(integer), _) => visitor.$visit(integer), + (Err(_), Cow::Borrowed(s)) => visitor.visit_borrowed_str(s), + (Err(_), Cow::Owned(s)) => visitor.visit_string(s), + } + } + }; +} + +impl<'de> serde::Deserializer<'de> for MapKeyDeserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + BorrowedCowStrDeserializer::new(self.key).deserialize_any(visitor) + } + + deserialize_integer_key!(deserialize_i8 => visit_i8); + deserialize_integer_key!(deserialize_i16 => visit_i16); + deserialize_integer_key!(deserialize_i32 => visit_i32); + deserialize_integer_key!(deserialize_i64 => visit_i64); + deserialize_integer_key!(deserialize_u8 => visit_u8); + deserialize_integer_key!(deserialize_u16 => visit_u16); + deserialize_integer_key!(deserialize_u32 => visit_u32); + deserialize_integer_key!(deserialize_u64 => visit_u64); + + #[inline] + fn deserialize_option(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + // Map keys cannot be null. + visitor.visit_some(self) + } + + #[inline] + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_enum( + self, + name: &'static str, + variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + self.key + .into_deserializer() + .deserialize_enum(name, variants, visitor) + } + + serde::forward_to_deserialize_any! { + bool f32 f64 char str string bytes byte_buf unit unit_struct seq tuple + tuple_struct map struct identifier ignored_any + } +} + +pub struct BorrowedCowStrDeserializer<'de> { + pub value: Cow<'de, str>, +} + +impl<'de> BorrowedCowStrDeserializer<'de> { + pub fn new(value: Cow<'de, str>) -> Self { + BorrowedCowStrDeserializer { value } + } +} + +impl<'de> serde::de::Deserializer<'de> for BorrowedCowStrDeserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + match self.value { + Cow::Borrowed(string) => visitor.visit_borrowed_str(string), + Cow::Owned(string) => visitor.visit_string(string), + } + } + + fn deserialize_enum( + self, + _name: &str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_enum(self) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct identifier ignored_any + } +} + +impl<'de> serde::de::EnumAccess<'de> for BorrowedCowStrDeserializer<'de> { + type Error = Error; + type Variant = UnitOnly; + + fn variant_seed(self, seed: T) -> Result<(T::Value, Self::Variant), Error> + where + T: serde::de::DeserializeSeed<'de>, + { + let value = seed.deserialize(self)?; + Ok((value, UnitOnly)) + } +} + +pub struct UnitOnly; + +impl<'de> serde::de::VariantAccess<'de> for UnitOnly { + type Error = Error; + + fn unit_variant(self) -> Result<(), Error> { + Ok(()) + } + + fn newtype_variant_seed(self, _seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"newtype variant", + )) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"tuple variant", + )) + } + + fn struct_variant( + self, + _fields: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::UnitVariant, + &"struct variant", + )) + } +} diff --git a/lib/flow-value/src/de/text_repr.rs b/lib/flow-value/src/de/text_repr.rs new file mode 100644 index 00000000..e6623e20 --- /dev/null +++ b/lib/flow-value/src/de/text_repr.rs @@ -0,0 +1,171 @@ +use crate::{value_type::Variant, Value}; +use serde::de::VariantAccess; +use std::borrow::Cow; + +pub struct TextRepr(Value); + +impl From for Value { + fn from(v: TextRepr) -> Value { + v.0 + } +} + +struct EnumVisitor; + +impl<'de> serde::de::Visitor<'de> for EnumVisitor { + type Value = Value; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("any valid value") + } + + fn visit_enum(self, data: A) -> Result + where + A: serde::de::EnumAccess<'de>, + { + let (ty, a) = data.variant::()?; + match ty { + Variant::Null => { + let num = a.newtype_variant::()?; + if num != 0 { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(num), + &"0", + )); + } + Ok(Value::Null) + } + Variant::String => Ok(Value::String(a.newtype_variant()?)), + Variant::Bool => Ok(Value::Bool(a.newtype_variant()?)), + Variant::U64 => Ok(Value::U64(number_from_str(a)?)), + Variant::I64 => Ok(Value::I64(number_from_str(a)?)), + Variant::F64 => Ok(Value::F64(number_from_str(a)?)), + Variant::Decimal => Ok(Value::Decimal(number_from_str(a)?)), + Variant::I128 => Ok(Value::I128(number_from_str(a)?)), + Variant::U128 => Ok(Value::U128(number_from_str(a)?)), + Variant::B32 => Ok(Value::B32(b58_str(a)?)), + Variant::B64 => Ok(Value::B64(b58_str(a)?)), + Variant::Bytes => Ok(Value::Bytes(b64_str(a)?)), + Variant::Array => Ok(Value::Array(a.newtype_variant::()?.0)), + Variant::Map => Ok(Value::Map(a.newtype_variant::()?.0)), + } + } +} + +struct MapVisitor; + +impl<'de> serde::de::Visitor<'de> for MapVisitor { + type Value = crate::Map; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("map") + } + + fn visit_map(self, mut a: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut map = crate::Map::new(); + if let Some(len) = a.size_hint() { + map.reserve(len); + } + while let Some((k, v)) = a.next_entry::()? { + map.insert(k, v.into()); + } + Ok(map) + } +} + +struct Map(crate::Map); + +impl<'de> serde::Deserialize<'de> for Map { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Map(d.deserialize_map(MapVisitor)?)) + } +} + +struct ArrayVisitor; + +impl<'de> serde::de::Visitor<'de> for ArrayVisitor { + type Value = Vec; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("array") + } + + fn visit_seq(self, mut a: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = Vec::new(); + if let Some(len) = a.size_hint() { + vec.reserve(len); + } + while let Some(v) = a.next_element::()? { + vec.push(v.into()); + } + Ok(vec) + } +} + +struct Array(Vec); + +impl<'de> serde::Deserialize<'de> for Array { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Array(d.deserialize_seq(ArrayVisitor)?)) + } +} + +fn number_from_str<'de, A, T>(a: A) -> Result +where + A: VariantAccess<'de>, + T: std::str::FromStr, +{ + let s = a.newtype_variant::>()?; + s.parse::() + .map_err(|_| serde::de::Error::custom(format!("invalid number: {}", s))) +} + +fn b58_str<'de, A, const N: usize>(a: A) -> Result<[u8; N], A::Error> +where + A: VariantAccess<'de>, +{ + let mut buf = [0u8; N]; + let s = a.newtype_variant::>()?; + let size = bs58::decode(&*s) + .into(&mut buf) + .map_err(|_| serde::de::Error::custom("invalid base58"))?; + if size != N { + return Err(serde::de::Error::invalid_length( + size, + &itoa::Buffer::new().format(N), + )); + } + Ok(buf) +} + +fn b64_str<'de, A>(a: A) -> Result +where + A: VariantAccess<'de>, +{ + let s = a.newtype_variant::>()?; + base64::decode(&*s) + .map_err(|_| serde::de::Error::custom("invalid base64")) + .map(Into::into) +} + +impl<'de> serde::Deserialize<'de> for TextRepr { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = d.deserialize_enum(crate::TOKEN, crate::value_type::keys::ALL, EnumVisitor)?; + Ok(TextRepr(value)) + } +} diff --git a/lib/flow-value/src/decimal.rs b/lib/flow-value/src/decimal.rs new file mode 100644 index 00000000..3761b748 --- /dev/null +++ b/lib/flow-value/src/decimal.rs @@ -0,0 +1,132 @@ +use rust_decimal::Decimal; + +pub(crate) const TOKEN: &str = "$$d"; + +pub type Target = Decimal; + +pub mod opt { + pub fn serialize(sig: &Option, s: S) -> Result + where + S: serde::Serializer, + { + match sig { + Some(sig) => super::serialize(sig, s), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + d.deserialize_option(crate::OptionVisitor(super::Visitor)) + } +} + +pub fn serialize(d: &Decimal, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_newtype_struct(TOKEN, &crate::Bytes(&d.serialize())) +} + +struct Visitor; + +impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Decimal; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("decimal") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if v.len() != 16 { + return Err(serde::de::Error::invalid_length(v.len(), &"16")); + } + + let buf: [u8; 16] = v.try_into().unwrap(); + Ok(Decimal::deserialize(buf)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(Decimal::from(v)) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(Decimal::from(v)) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + // TODO: this is lossy + Decimal::try_from(v).map_err(serde::de::Error::custom) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let v = v.trim(); + if v.bytes().any(|c| c == b'e' || c == b'E') { + Decimal::from_scientific(v).map_err(serde::de::Error::custom) + } else { + v.parse().map_err(serde::de::Error::custom) + } + } + + fn visit_newtype_struct(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self) + } +} + +pub fn deserialize<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + d.deserialize_newtype_struct(TOKEN, Visitor) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Value; + use rust_decimal_macros::dec; + + fn de<'de, D: serde::Deserializer<'de>>(d: D) -> Decimal { + deserialize(d).unwrap() + } + + #[test] + fn test_deserialize_value() { + assert_eq!(de(Value::U64(100)), dec!(100)); + assert_eq!(de(Value::I64(-1)), dec!(-1)); + assert_eq!(de(Value::Decimal(Decimal::MAX)), Decimal::MAX); + assert_eq!(de(Value::F64(1231.2221)), dec!(1231.2221)); + assert_eq!(de(Value::String("1234.0".to_owned())), dec!(1234)); + assert_eq!(de(Value::String(" 1234.0".to_owned())), dec!(1234)); + assert_eq!(de(Value::String("1e5".to_owned())), dec!(100000)); + assert_eq!(de(Value::String(" 1e5".to_owned())), dec!(100000)); + } + + #[test] + fn test_serialize() { + assert_eq!( + serialize(&Decimal::MAX, crate::ser::Serializer).unwrap(), + Value::Decimal(Decimal::MAX) + ); + } +} diff --git a/lib/flow-value/src/json_repr/iter_ser.rs b/lib/flow-value/src/json_repr/iter_ser.rs new file mode 100644 index 00000000..8d775cb6 --- /dev/null +++ b/lib/flow-value/src/json_repr/iter_ser.rs @@ -0,0 +1,56 @@ +pub struct Array { + iter: I, +} + +impl Array { + pub fn new(iter: I) -> Self { + Self { iter } + } +} + +impl serde::Serialize for Array +where + I: Iterator + Clone, + I::Item: serde::Serialize, +{ + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = s.serialize_seq(None)?; + for item in self.iter.clone() { + seq.serialize_element(&item)?; + } + seq.end() + } +} + +pub struct Map { + iter: I, +} + +impl Map { + pub fn new(iter: I) -> Self { + Self { iter } + } +} + +impl<'a, I, K, V> serde::Serialize for Map +where + I: Iterator + Clone, + K: serde::Serialize + 'a, + V: serde::Serialize + 'a, +{ + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(None)?; + for (k, v) in self.iter.clone() { + map.serialize_entry(&k, &v)?; + } + map.end() + } +} diff --git a/lib/flow-value/src/keypair.rs b/lib/flow-value/src/keypair.rs new file mode 100644 index 00000000..c18103f1 --- /dev/null +++ b/lib/flow-value/src/keypair.rs @@ -0,0 +1,93 @@ +use crate::with::AsKeypair; +use solana_sdk::signer::keypair::Keypair; + +type Target = Keypair; + +type As = AsKeypair; + +pub mod opt { + use serde_with::{DeserializeAs, SerializeAs}; + + pub fn serialize(sig: &Option, s: S) -> Result + where + S: serde::Serializer, + { + Option::::serialize_as(sig, s) + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + Option::::deserialize_as(d) + } +} + +pub fn serialize(p: &Target, s: S) -> Result +where + S: serde::Serializer, +{ + As::serialize(p, s) +} + +pub fn deserialize<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + As::deserialize(d) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Value; + use solana_sdk::signer::keypair::Keypair; + + fn de<'de, D: serde::Deserializer<'de>>(d: D) -> Keypair { + deserialize(d).unwrap() + } + + #[test] + fn test_deserialize_value() { + let k = Keypair::new(); + assert_eq!(de(Value::B64(k.to_bytes())), k); + assert_eq!(de(Value::String(k.to_base58_string())), k); + } + + #[test] + fn test_serialize() { + let k = Keypair::new(); + assert_eq!( + serialize(&k, crate::ser::Serializer).unwrap(), + Value::B64(k.to_bytes()), + ) + } + + #[test] + fn test_enum() { + let key = Keypair::new(); + + #[derive(serde::Deserialize, PartialEq, Debug)] + #[serde(untagged)] + pub enum UntaggedEnum { + PrivateKey { + #[serde(with = "super")] + private_key: Keypair, + }, + Seed { + #[serde(default)] + seed: String, + #[serde(default)] + passphrase: String, + }, + } + assert_eq!( + crate::from_map::(crate::Map::from([( + "private_key".to_owned(), + Value::B64(key.to_bytes()) + )])) + .unwrap(), + UntaggedEnum::PrivateKey { private_key: key } + ); + } +} diff --git a/lib/flow-value/src/lib.rs b/lib/flow-value/src/lib.rs new file mode 100644 index 00000000..cc011554 --- /dev/null +++ b/lib/flow-value/src/lib.rs @@ -0,0 +1,897 @@ +//! This crate contains [`Value`], an enum representing all values that can be used as +//! node's input and output, and utilities for working with [`Value`]. +//! +//! Common operations: +//! - Converting [`Value`] to Rust types. +//! - Converting Rust types to [`Value`]. +//! - Receiving [`flow_value::Map`][Map] as node's input. +//! - Returning [`flow_value::Map`][Map] as node's output. +//! - Converting [`Value`] to/from JSON to use in HTTP APIs and database. +//! - Getting and updating nested values with JSON Pointer syntax. + +use rust_decimal::prelude::ToPrimitive; +use thiserror::Error as ThisError; + +pub use rust_decimal::Decimal; + +pub(crate) mod value_type; +pub use value_type::keys; + +pub(crate) const TOKEN: &str = "$V"; + +mod de; +pub use de::const_bytes::ConstBytes; + +mod ser; + +pub mod crud; +pub mod macros; + +pub mod with; + +// custom serialize and deserialize modules +pub mod decimal; +#[cfg(feature = "solana")] +#[deprecated] +pub mod keypair; +#[cfg(feature = "solana")] +pub mod pubkey; +#[cfg(feature = "solana")] +pub mod signature; + +/// Interpret a [`Value`] as an instance of type `T` +/// +/// # Example +/// +/// ``` +/// use solana_sdk::pubkey::Pubkey; +/// use solana_sdk::pubkey; +/// use flow_value::Value; +/// +/// #[derive(serde::Deserialize)] +/// pub struct User { +/// pubkey: Pubkey, +/// } +/// +/// let value = Value::Map(flow_value::map! { +/// "pubkey" => pubkey!("My11111111111111111111111111111111111111111"), +/// }); +/// flow_value::from_value::(value).unwrap(); +/// ``` +pub fn from_value(value: Value) -> Result +where + T: for<'de> serde::Deserialize<'de>, +{ + T::deserialize(value) +} + +/// Interpret a [`Map`] as an instance of type `T` +/// +/// # Example +/// +/// ``` +/// use solana_sdk::pubkey::Pubkey; +/// use solana_sdk::pubkey; +/// use flow_value::Value; +/// +/// #[derive(serde::Deserialize)] +/// pub struct User { +/// pubkey: Pubkey, +/// } +/// +/// let map = flow_value::map! { +/// "pubkey" => pubkey!("My11111111111111111111111111111111111111111"), +/// }; +/// flow_value::from_map::(map).unwrap(); +/// ``` +pub fn from_map(map: Map) -> Result +where + T: for<'de> serde::Deserialize<'de>, +{ + T::deserialize(Value::Map(map)) +} + +/// Convert a `T` into [`Value`]. +/// +/// # Example +/// +/// ``` +/// use solana_sdk::signature::Signature; +/// use flow_value::Value; +/// +/// let signature = Signature::new_unique(); +/// let value = flow_value::to_value(&flow_value::Bytes(signature.as_ref())).unwrap(); +/// assert_eq!(value, Value::B64(signature.into())); +/// ``` +pub fn to_value(t: &T) -> Result +where + T: serde::Serialize, +{ + t.serialize(ser::Serializer) +} + +/// Convert a `T` into [`Map`]. +/// +/// # Example +/// +/// ``` +/// use flow_value::Value; +/// +/// let map = flow_value::to_map(&serde_json::json!({"A": "B"})).unwrap(); +/// assert_eq!(map, flow_value::map! { "A" => "B" }); +/// ``` +pub fn to_map(t: &T) -> Result +where + T: serde::Serialize, +{ + to_value(t).and_then(|v| { + if let Value::Map(map) = v { + Ok(map) + } else { + Err(Error::ExpectedMap) + } + }) +} + +/// Allow for switching HashMap implementation +pub type HashMap = indexmap::IndexMap; + +/// Key type of [`Map`] +pub type Key = String; + +pub type Map = self::HashMap; + +/// [`Value`] represents all values that nodes can use as input and output. +/// +/// # Data Types +/// +/// - Scalar types: +/// - Null: [`Value::Null`]. +/// - Boolean: [`Value::Bool`]. +/// - Numbers: [`Value::U64`], [`Value::I64`], [`Value::U128`], [`Value::I128`], [`Value::Decimal`], [`Value::F64`]. +/// - String: [`Value::String`]. +/// - Binary: [`Value::B32`], [`Value::B64`], [`Value::Bytes`]. +/// - Array: [`Value::Array`] +/// - Map: [`Value::Map`] +/// +/// # Node Input +/// +/// Node receives a [`flow_value::Map`][Map] as its input. It is possible to use the map directly, but +/// it is often preferred to convert it to structs or enums of your choice. +/// +/// [`Value`] implements [`Deserializer`][serde::Deserializer], therefore it can be converted to +/// any types supported by Serde. We provide 2 helpers: +/// +/// - [`flow_value::from_value`][from_value] - [`Value`] to any `T: Deserialize`. +/// - [`flow_value::from_map`][from_map] - [`Map`] to any `T: Deserialize`. +/// +/// # Node Output +/// +/// Node returns a [`flow_value::Map`][Map] as its output. +/// +/// Building the output directly with [`flow_value::map!`][macro@map] and +/// [`flow_value::array!`][macro@array] macros: +/// ``` +/// let value = flow_value::map! { +/// "customer_name" => "John", +/// "items" => flow_value::array![1, 2, 3], +/// }; +/// ``` +/// +/// [`Value`] also implements [`Serializer`][serde::Serializer], you can use +/// [`flow_value::to_map`][to_map] to convert any type `T: Serialize` into [`value::Map`][Map]. +/// +/// ``` +/// #[derive(serde::Serialize)] +/// struct Order { +/// customer_name: String, +/// items: Vec, +/// } +/// +/// flow_value::to_map(&Order { +/// customer_name: "John".to_owned(), +/// items: [1, 2, 3].into(), +/// }) +/// .unwrap(); +/// ``` +/// +/// # JSON representation +/// +/// When using [`Value`] in database and HTTP APIs, it is converted to a JSON object: +/// +/// ```json +/// { +/// "": +/// } +/// ``` +/// +/// Identifiers of each enum variant: +/// - **N**: [`Value::Null`] +/// - **S**: [`Value::String`] +/// - **B**: [`Value::Bool`] +/// - **U**: [`Value::U64`] +/// - **I**: [`Value::I64`] +/// - **F**: [`Value::F64`] +/// - **D**: [`Value::Decimal`] +/// - **U1**: [`Value::U128`] +/// - **I1**: [`Value::I128`] +/// - **B3**: [`Value::B32`] +/// - **B6**: [`Value::B64`] +/// - **BY**: [`Value::Bytes`] +/// - **A**: [`Value::Array`] +/// - **M**: [`Value::Map`] +/// +/// See variant's documentation to see how data are encoded. +/// +/// Use [`serde_json`] to encode and decode [`Value`] as JSON: +/// ``` +/// use flow_value::Value; +/// +/// let value = Value::U64(10); +/// +/// // encode Value to JSON +/// let json = serde_json::to_string(&value).unwrap(); +/// assert_eq!(json, r#"{"U":"10"}"#); +/// +/// // decode JSON to Value +/// let value1 = serde_json::from_str::(&json).unwrap(); +/// assert_eq!(value1, value); +/// ``` +#[derive(Clone, PartialEq, Default)] +pub enum Value { + /// JSON representation: + /// ```json + /// { "N": 0 } + /// ``` + #[default] + Null, + /// UTF-8 string. + /// + /// JSON representation: + /// ```json + /// { "S": "hello" } + /// ``` + String(String), + /// JSON representation: + /// ```json + /// { "B": true } + /// ``` + Bool(bool), + /// JSON representation: + /// ```json + /// { "U": "100" } + /// ``` + /// + /// Numbers are encoded as JSON string to avoid losing precision when reading them in + /// Javascript/Typescript. + U64(u64), + /// JSON representation: + /// ```json + /// { "I": "-100" } + /// ``` + I64(i64), + /// JSON representation: + /// ```json + /// { "F": "0.0" } + /// ``` + /// Scientific notation is supported: + /// ```json + /// { "F": "1e9" } + /// ``` + F64(f64), + /// [`rust_decimal::Decimal`], suitable for financial calculations. + /// + /// JSON representation: + /// ```json + /// { "D": "3.1415926535897932384626433832" } + /// ``` + Decimal(Decimal), + /// JSON representation: + /// ```json + /// { "U1": "340282366920938463463374607431768211455" } + /// ``` + U128(u128), + /// JSON representation: + /// ```json + /// { "I1": "-170141183460469231731687303715884105728" } + /// ``` + I128(i128), + /// 32-bytes binary values, usually a Solana public key. + /// + /// JSON representation: encoded as a base-58 string + /// ```json + /// { "B3": "FMQUifdAHTytSxhiK4N7LmpvKRZaUmBnNnZmzFsdTPHB" } + /// ``` + B32([u8; 32]), + /// 64-bytes binary values, usually a Solana signature or keypair. + /// + /// JSON representation: encoded as a base-58 string + /// ```json + /// { "B6": "4onDpbfeT7nNN9MNMvTEZRn6pbtrQc1pdTBJB4a7HbfhAE6c5bkbuuFfYtkqs99hAqp5o6j7W1VyuKDxCn79k3Tk" } + /// ``` + B64([u8; 64]), + /// Binary values with length other than 32 and 64. + /// + /// JSON representation: encoded as a base-64 string + /// ```json + /// { "BY": "UmFpbnk=" } + /// ``` + Bytes(bytes::Bytes), + /// An array of [`Value`]. Array can contains other arrays, maps, ect. Array elements do not + /// have to be of the same type. + /// + /// JSON representation: + /// + /// Example array containing a number and a string: + /// ```json + /// { + /// "A": [ + /// { "U": 0 }, + /// { "S": "hello" } + /// ] + /// } + /// ``` + Array(Vec), + /// A key-value map, implemented with [`indexmap::IndexMap`], will preserve insertion order. + /// Keys are strings and values can be any [`Value`]. + /// + /// JSON representation: + /// ```json + /// { + /// "M": { + /// "first name": { "S": "John" }, + /// "age": { "U": "20" } + /// } + /// } + /// ``` + Map(Map), +} + +impl Value { + pub fn new_keypair_bs58(s: &str) -> Result { + // and Ed25519 keypair + const KEYPAIR_LENGTH: usize = 64; + let mut buf = [0u8; KEYPAIR_LENGTH]; + let size = bs58::decode(s).into(&mut buf)?; + if size != KEYPAIR_LENGTH { + return Err(Error::InvalidLenght { + need: KEYPAIR_LENGTH, + got: size, + }); + } + + Ok(Value::B64(buf)) + } + + pub fn normalize(self) -> Self { + match self { + Value::Null + | Value::String(_) + | Value::Bool(_) + | Value::U64(_) + | Value::I64(_) + | Value::F64(_) + | Value::B32(_) + | Value::B64(_) + | Value::Bytes(_) => self, + Value::Decimal(mut d) => { + d.normalize_assign(); + if d.scale() == 0 { + Value::I128(d.to_i128().expect("always fit into i128")).normalize() + } else { + Value::Decimal(d) + } + } + Value::I128(i) => if i < 0 { + i64::try_from(i).map(Value::I64).ok() + } else { + u64::try_from(i).map(Value::U64).ok() + } + .unwrap_or(self), + Value::U128(u) => u64::try_from(u).map(Value::U64).unwrap_or(self), + Value::Array(mut a) => { + for v in &mut a { + *v = std::mem::take(v).normalize(); + } + Value::Array(a) + } + Value::Map(mut m) => { + for v in m.values_mut() { + *v = std::mem::take(v).normalize(); + } + Value::Map(m) + } + } + } +} + +#[cfg(feature = "json")] +mod json { + use crate::Value; + use rust_decimal::Decimal; + + impl From for Value { + fn from(value: serde_json::Value) -> Self { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(b) => Value::Bool(b), + serde_json::Value::Number(n) => { + if let Some(u) = n.as_u64() { + Value::U64(u) + } else if let Some(i) = n.as_i64() { + if i < 0 { + Value::I64(i) + } else { + Value::U64(i as u64) + } + } else { + let s = n.to_string(); + if let Ok(u) = s.parse::() { + Value::U128(u) + } else if let Ok(i) = s.parse::() { + Value::I128(i) + } else if let Ok(d) = s.parse::() { + Value::Decimal(d) + } else if let Ok(d) = Decimal::from_scientific(&s) { + Value::Decimal(d) + } else if let Ok(f) = s.parse::() { + Value::F64(f) + } else { + // unlikely to happen + // if happen, probably a bug in serde_json + Value::String(s) + } + } + } + serde_json::Value::String(s) => Value::String(s), + serde_json::Value::Array(vec) => { + Value::Array(vec.into_iter().map(Value::from).collect()) + } + serde_json::Value::Object(map) => { + Value::Map(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect()) + } + } + } + } + + impl From for serde_json::Value { + fn from(value: Value) -> Self { + match value { + Value::Null => serde_json::Value::Null, + Value::String(x) => x.into(), + Value::Bool(x) => x.into(), + Value::U64(x) => x.into(), + Value::I64(x) => x.into(), + Value::F64(x) => x.into(), + Value::Array(x) => x.into(), + Value::Map(x) => x + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect::>() + .into(), + Value::U128(value) => value + .try_into() + .map(u64::into) + .unwrap_or_else(|_| (value as f64).into()), + Value::I128(value) => value + .try_into() + .map(i64::into) + .unwrap_or_else(|_| (value as f64).into()), + Value::Decimal(mut d) => { + d.normalize_assign(); + if d.scale() == 0 { + if let Ok(n) = u64::try_from(d) { + n.into() + } else if let Ok(n) = i64::try_from(d) { + n.into() + } else { + f64::try_from(d).map_or(serde_json::Value::Null, Into::into) + } + } else { + f64::try_from(d).map_or(serde_json::Value::Null, Into::into) + } + } + Value::B32(b) => (&b[..]).into(), + Value::B64(b) => (&b[..]).into(), + Value::Bytes(b) => (&b[..]).into(), + } + } + } +} + +impl From for Value { + fn from(x: String) -> Self { + Self::String(x) + } +} + +impl From<&str> for Value { + fn from(x: &str) -> Self { + Self::String(x.to_owned()) + } +} + +impl From for Value { + fn from(x: bool) -> Self { + Self::Bool(x) + } +} + +impl From for Value { + fn from(x: u8) -> Self { + Self::U64(x as u64) + } +} + +impl From for Value { + fn from(x: u16) -> Self { + Self::U64(x as u64) + } +} + +impl From for Value { + fn from(x: u32) -> Self { + Self::U64(x as u64) + } +} + +impl From for Value { + fn from(x: u64) -> Self { + Self::U64(x) + } +} + +impl From for Value { + fn from(x: u128) -> Self { + Self::U128(x) + } +} + +impl From for Value { + fn from(x: i8) -> Self { + Self::I64(x as i64) + } +} + +impl From for Value { + fn from(x: i16) -> Self { + Self::I64(x as i64) + } +} + +impl From for Value { + fn from(x: i32) -> Self { + Self::I64(x as i64) + } +} + +impl From for Value { + fn from(x: i64) -> Self { + Self::I64(x) + } +} + +impl From for Value { + fn from(x: i128) -> Self { + Self::I128(x) + } +} + +impl From for Value { + fn from(x: Decimal) -> Self { + Self::Decimal(x) + } +} + +impl From for Value { + fn from(x: f32) -> Self { + Self::F64(x as f64) + } +} + +impl From for Value { + fn from(x: f64) -> Self { + Self::F64(x) + } +} + +impl From<[u8; 32]> for Value { + fn from(x: [u8; 32]) -> Self { + Self::B32(x) + } +} + +impl From<[u8; 64]> for Value { + fn from(x: [u8; 64]) -> Self { + Self::B64(x) + } +} + +#[cfg(feature = "solana")] +impl From for Value { + fn from(x: solana_sdk::pubkey::Pubkey) -> Self { + Self::B32(x.to_bytes()) + } +} + +#[cfg(feature = "solana")] +impl From for Value { + fn from(x: solana_sdk::signer::keypair::Keypair) -> Self { + Self::B64(x.to_bytes()) + } +} + +#[cfg(feature = "solana")] +impl From for Value { + fn from(x: solana_sdk::signature::Signature) -> Self { + Self::B64(x.into()) + } +} + +impl From for Value { + fn from(x: bytes::Bytes) -> Self { + match x.len() { + 32 => Self::B32(<_>::try_from(&*x).unwrap()), + 64 => Self::B64(<_>::try_from(&*x).unwrap()), + _ => Self::Bytes(x), + } + } +} + +impl From<&[u8]> for Value { + fn from(x: &[u8]) -> Self { + match x.len() { + 32 => Self::B32(<_>::try_from(x).unwrap()), + 64 => Self::B64(<_>::try_from(x).unwrap()), + _ => Self::Bytes(bytes::Bytes::copy_from_slice(x)), + } + } +} + +impl From> for Value { + fn from(x: Vec) -> Self { + match x.len() { + 32 => Self::B32(<_>::try_from(&*x).unwrap()), + 64 => Self::B64(<_>::try_from(&*x).unwrap()), + _ => Self::Bytes(x.into()), + } + } +} + +impl From> for Value { + fn from(x: Vec) -> Self { + Self::Array(x) + } +} + +impl From for Value { + fn from(x: Map) -> Self { + Self::Map(x) + } +} + +impl std::fmt::Debug for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Null => f.debug_tuple("Null").finish(), + Value::String(x) => f.debug_tuple("String").field(x).finish(), + Value::Bool(x) => f.debug_tuple("Bool").field(x).finish(), + Value::I64(x) => f.debug_tuple("I64").field(x).finish(), + Value::U64(x) => f.debug_tuple("U64").field(x).finish(), + Value::F64(x) => f.debug_tuple("F64").field(x).finish(), + Value::Decimal(x) => f.debug_tuple("Decimal").field(x).finish(), + Value::I128(x) => f.debug_tuple("I128").field(x).finish(), + Value::U128(x) => f.debug_tuple("U128").field(x).finish(), + Value::Array(x) => f.debug_tuple("Array").field(x).finish(), + Value::Map(x) => f.debug_tuple("Map").field(x).finish(), + Value::Bytes(x) => f.debug_tuple("Bytes").field(&x.len()).finish(), + Value::B32(x) => f + .debug_tuple("B32") + .field(&bs58::encode(x).into_string()) + .finish(), + Value::B64(x) => f + .debug_tuple("B64") + .field(&bs58::encode(x).into_string()) + .finish(), + } + } +} + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("{0}")] + Custom(String), + #[error("key must be a string")] + KeyMustBeAString, + #[error("invalid base58: {0}")] + Bs58Decode(#[from] bs58::decode::Error), + #[error("need length {need}, got {got}")] + InvalidLenght { need: usize, got: usize }, + #[error("expected a map")] + ExpectedMap, + #[error("expected array")] + ExpectedArray, +} + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self + where + T: std::fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +// default implementation of [u8] doesn't call serialize_bytes +pub struct Bytes<'a>(pub &'a [u8]); + +impl<'a> serde::Serialize for Bytes<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_bytes(self.0) + } +} + +pub mod default { + pub const fn bool_true() -> bool { + true + } + + pub const fn bool_false() -> bool { + false + } +} + +pub(crate) struct OptionVisitor(pub(crate) V); + +impl<'de, V> serde::de::Visitor<'de> for OptionVisitor +where + V: serde::de::Visitor<'de>, +{ + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("optional ")?; + self.0.expecting(formatter) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + fn visit_some(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self.0).map(Some) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + #[test] + fn test_solana_instruction() { + use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::pubkey; + + let i = Instruction::new_with_bytes( + pubkey!("ESxeViFP4r7THzVx9hJDkhj4HrNGSjJSFRPbGaAb97hN"), + &[100; 1024], + vec![AccountMeta { + pubkey: pubkey!("ESxeViFP4r7THzVx9hJDkhj4HrNGSjJSFRPbGaAb97hN"), + is_signer: true, + is_writable: false, + }], + ); + + let v = to_value(&i).unwrap(); + dbg!(&v); + + let i1: Instruction = from_value(v).unwrap(); + + assert_eq!(i, i1); + } + + #[test] + fn test_json() { + fn t(v: Value, s: &str) { + assert_eq!(s, serde_json::to_string(&v).unwrap()); + assert_eq!(v, serde_json::from_str::(s).unwrap()); + } + t(Value::Null, r#"{"N":0}"#); + t(Value::String("hello".to_owned()), r#"{"S":"hello"}"#); + t(Value::U64(0), r#"{"U":"0"}"#); + t(Value::I64(-1), r#"{"I":"-1"}"#); + t( + Value::U128(u128::MAX), + r#"{"U1":"340282366920938463463374607431768211455"}"#, + ); + t( + Value::I128(i128::MIN), + r#"{"I1":"-170141183460469231731687303715884105728"}"#, + ); + t(Value::Bool(true), r#"{"B":true}"#); + t( + Value::Decimal(dec!(3.1415926535897932384626433833)), + r#"{"D":"3.1415926535897932384626433833"}"#, + ); + t( + crate::map! { + "foo" => 1i64, + } + .into(), + r#"{"M":{"foo":{"I":"1"}}}"#, + ); + t( + Value::Array(vec![1i64.into(), "hello".into()]), + r#"{"A":[{"I":"1"},{"S":"hello"}]}"#, + ); + t( + Value::B32( + bs58::decode("5sNRWMrT2P3KULzW3faaktCB3k2eqHow2GBJtcsCPcg7") + .into_vec() + .unwrap() + .try_into() + .unwrap(), + ), + r#"{"B3":"5sNRWMrT2P3KULzW3faaktCB3k2eqHow2GBJtcsCPcg7"}"#, + ); + t( + Value::B64( + bs58::decode("3PvNxykqBz1BzBaq2AMU4Sa3CPJGnSC9JXkyzXe33m6W7Sj4MMgsZet6YxUQdPx1fEFU79QWm6RpPRVJAyeqiNsR") + .into_vec() + .unwrap() + .try_into() + .unwrap(), + ), + r#"{"B6":"3PvNxykqBz1BzBaq2AMU4Sa3CPJGnSC9JXkyzXe33m6W7Sj4MMgsZet6YxUQdPx1fEFU79QWm6RpPRVJAyeqiNsR"}"#, + ); + t( + Value::Bytes(bytes::Bytes::from_static(&[ + 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, + ])), + r#"{"BY":"aGVsbG8gd29ybGQ="}"#, + ); + } + + #[test] + fn test_array_ser() { + #[derive(serde::Serialize)] + struct Output { + value: Value, + } + + let mut v = crate::to_map(&Output { + value: Vec::from([Value::U64(1)]).into(), + }) + .unwrap(); + assert_eq!( + v.swap_remove("value").unwrap(), + Value::Array([1u64.into()].into()) + ) + } + + #[cfg(feature = "json")] + #[test] + fn test_number_into_json() { + let json: serde_json::Value = Value::Decimal(dec!(15966.2)).into(); + assert_eq!(json.as_f64().unwrap(), 15966.2); + } +} diff --git a/lib/flow-value/src/macros.rs b/lib/flow-value/src/macros.rs new file mode 100644 index 00000000..88f20c04 --- /dev/null +++ b/lib/flow-value/src/macros.rs @@ -0,0 +1,35 @@ +#[macro_export] +macro_rules! map { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$($crate::map!(@single $rest)),*])); + + ($($key:expr => $value:expr,)+) => { $crate::map!($($key => $value),+) }; + ($($key:expr => $value:expr),*) => { + { + let _cap = $crate::map!(@count $($key),*); + let mut _map = $crate::Map::with_capacity(_cap); + $( + let _ = _map.insert($crate::Key::from($key), $crate::Value::from($value)); + )* + _map + } + }; +} + +#[macro_export] +macro_rules! array { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$($crate::array!(@single $rest)),*])); + + ($($value:expr,)+) => { $crate::array!($($value),+) }; + ($($value:expr),*) => { + { + let _cap = $crate::array!(@count $($value),*); + let mut _vec = ::std::vec::Vec::<$crate::Value>::with_capacity(_cap); + $( + _vec.push($crate::Value::from($value)); + )* + _vec + } + }; +} diff --git a/lib/flow-value/src/pubkey.rs b/lib/flow-value/src/pubkey.rs new file mode 100644 index 00000000..1d4bfdc3 --- /dev/null +++ b/lib/flow-value/src/pubkey.rs @@ -0,0 +1,71 @@ +use crate::with::AsPubkey; +use solana_sdk::pubkey::Pubkey; + +type Target = Pubkey; + +type As = AsPubkey; + +pub mod opt { + use serde_with::{DeserializeAs, SerializeAs}; + + pub fn serialize(sig: &Option, s: S) -> Result + where + S: serde::Serializer, + { + Option::::serialize_as(sig, s) + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + Option::::deserialize_as(d) + } +} + +pub fn serialize(p: &Target, s: S) -> Result +where + S: serde::Serializer, +{ + As::serialize(p, s) +} + +pub fn deserialize<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + As::deserialize(d) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Value; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::signature::Signer; + use solana_sdk::signer::keypair::Keypair; + + fn de<'de, D: serde::Deserializer<'de>>(d: D) -> Pubkey { + deserialize(d).unwrap() + } + + #[test] + fn test_deserialize_value() { + let id = solana_sdk::feature_set::add_set_compute_unit_price_ix::id(); + assert_eq!(de(Value::String(id.to_string())), id); + assert_eq!(de(Value::B32(id.to_bytes())), id); + + let k = Keypair::new(); + let pk = k.pubkey(); + assert_eq!(de(Value::B64(k.to_bytes())), pk); + } + + #[test] + fn test_serialize() { + let id = solana_sdk::feature_set::add_set_compute_unit_price_ix::id(); + assert_eq!( + serialize(&id, crate::ser::Serializer).unwrap(), + Value::B32(id.to_bytes()) + ); + } +} diff --git a/lib/flow-value/src/ser.rs b/lib/flow-value/src/ser.rs new file mode 100644 index 00000000..c42ca922 --- /dev/null +++ b/lib/flow-value/src/ser.rs @@ -0,0 +1,406 @@ +use crate::{Error, Map, Value}; + +mod iter_ser; +mod map_key; +mod maps; +mod seq; +mod tagged_bytes; +mod text_repr; + +use maps::{SerializeMap, SerializeStructVariant, SerializeTupleVariant}; +use seq::{SerializeSeq, SerializeSeqNoBytes}; +use tagged_bytes::TaggedBytes; + +impl serde::Serialize for Value { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use crate::TOKEN; + + let (i, k) = self.kind().variant(); + if s.is_human_readable() { + text_repr::TextRepr::new(self).serialize(s) + } else { + match self { + Value::Null => s.serialize_newtype_variant(TOKEN, i, k, &()), + Value::String(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::Bool(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::U64(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::I64(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::F64(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::Decimal(v) => { + s.serialize_newtype_variant(TOKEN, i, k, &crate::Bytes(&v.serialize())) + } + Value::I128(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::U128(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::B32(v) => s.serialize_newtype_variant(TOKEN, i, k, &crate::Bytes(v)), + Value::B64(v) => s.serialize_newtype_variant(TOKEN, i, k, &crate::Bytes(v)), + Value::Bytes(v) => s.serialize_newtype_variant(TOKEN, i, k, &crate::Bytes(v)), + Value::Array(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + Value::Map(v) => s.serialize_newtype_variant(TOKEN, i, k, &v), + } + } + } +} + +/// Turn any type that implements `Serialize` into `Value`. +pub struct Serializer; + +impl serde::Serializer for Serializer { + type Ok = Value; + type Error = Error; + + type SerializeSeq = SerializeSeqNoBytes; + type SerializeTuple = SerializeSeqNoBytes; + type SerializeTupleStruct = SerializeSeqNoBytes; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + fn is_human_readable(&self) -> bool { + false + } + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::from(v)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Value::from(v)) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Value::from(v)) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Value::from(v)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Value::from(v)) + } + + fn serialize_i128(self, v: i128) -> Result { + Ok(Value::from(v)) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Value::from(v)) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Value::from(v)) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Value::from(v)) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(Value::from(v)) + } + + fn serialize_u128(self, v: u128) -> Result { + Ok(Value::from(v)) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Value::from(v)) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Value::from(v)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Value::String(String::from(v))) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Value::from(v)) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Value::from(v)) + } + + fn serialize_none(self) -> Result { + Ok(Value::Null) + } + + fn serialize_some(self, v: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct(self, name: &'static str, v: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + match name { + crate::with::decimal::TOKEN => v.serialize(TaggedBytes::Decimal), + #[cfg(feature = "solana")] + crate::with::keypair::TOKEN + | crate::with::signature::TOKEN + | crate::with::pubkey::TOKEN => v.serialize(TaggedBytes::Bytes), + _ => v.serialize(self), + } + } + + fn serialize_seq(self, _: Option) -> Result { + Ok(SerializeSeqNoBytes::default()) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_map(self, len: Option) -> Result { + Ok(SerializeMap { + map: Map::with_capacity(len.unwrap_or(0)), + next_key: None, + }) + } + + fn serialize_struct( + self, + _: &'static str, + len: usize, + ) -> Result { + Ok(SerializeMap { + map: Map::with_capacity(len), + next_key: None, + }) + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeStructVariant { + name: variant, + map: Map::with_capacity(len), + }) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + serde::Serialize, + { + if name == crate::TOKEN { + match index { + // Decimal's index + 6 => value.serialize(TaggedBytes::Decimal), + // Other bytes + 9..=11 => value.serialize(TaggedBytes::Bytes), + // Array + 12 => value.serialize(SerializeSeqNoBytes::default()), + // Other variants can map directly to serde's data model + _ => value.serialize(Serializer), + } + } else { + let value = value.serialize(Serializer)?; + Ok(Value::Map(Map::from([(variant.to_owned(), value)]))) + } + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + _: usize, + ) -> Result { + Ok(SerializeTupleVariant { + name: variant, + seq: SerializeSeq::new(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal::Decimal; + use serde::Serialize; + use std::collections::HashMap; + + #[test] + fn test_value_to_value() { + fn t(v: Value) { + assert_eq!(v.serialize(Serializer).unwrap(), v); + } + t(Value::Null); + t(Value::I64(0i64)); + t(Value::String(String::new())); + t(Value::Bool(false)); + t(Value::U64(0)); + t(Value::I64(0)); + t(Value::F64(0.0)); + t(Value::Decimal(Decimal::MAX)); + t(Value::I128(0)); + t(Value::U128(0)); + t(Value::B32([0u8; 32])); + t(Value::B64([0u8; 64])); + t(Value::Bytes(bytes::Bytes::from_static( + "something".as_bytes(), + ))); + t(Value::Array(Vec::new())); + t(Value::Map(Map::new())); + } + + fn s(t: T) -> Value { + t.serialize(Serializer).unwrap() + } + + #[test] + fn test_serialize_primitive() { + assert_eq!(s(0u8), Value::U64(0)); + assert_eq!(s(0u16), Value::U64(0)); + assert_eq!(s(0u32), Value::U64(0)); + assert_eq!(s(0u64), Value::U64(0)); + assert_eq!(s(0i8), Value::I64(0)); + assert_eq!(s(0i16), Value::I64(0)); + assert_eq!(s(0i32), Value::I64(0)); + assert_eq!(s(0i64), Value::I64(0)); + assert_eq!(s(0f32), Value::F64(0.0)); + assert_eq!(s(0f64), Value::F64(0.0)); + assert_eq!(s(true), Value::Bool(true)); + assert_eq!(s(Option::<()>::None), Value::Null); + assert_eq!(s(()), Value::Null); + assert_eq!(s("end"), Value::String("end".to_owned())); + assert_eq!(s([1i32]), Value::Array(vec![Value::I64(1)])); + assert_eq!( + s((1i32, -2i32, "hello")), + Value::Array(vec![ + Value::I64(1), + Value::I64(-2), + Value::String("hello".to_owned()) + ]) + ); + assert_eq!(s([0u8; 0]), Value::Array(Vec::new())); + assert_eq!(s([()]), Value::Array([Value::Null].to_vec())); + assert_eq!( + s(HashMap::from([("a".to_owned(), -1i32)])), + Value::Map(Map::from([("a".to_owned(), Value::I64(-1))])) + ); + // assert_eq!(s([1u8; 32]), Value::B32([1; 32])); + assert_eq!(s(crate::Bytes(&[2u8; 64])), Value::B64([2; 64])); + } + + #[test] + fn test_derive() { + #[derive(Serialize)] + struct A { + a: Noop, + b: B, + c0: C, + c1: C, + #[serde(flatten)] + f: C, + #[serde(rename = "NULL")] + null: Option, + i: I32, + } + + #[derive(Serialize)] + struct Noop; + + #[derive(Serialize)] + struct B {} + + #[derive(Serialize)] + struct I32(i32); + + #[derive(Serialize)] + struct C { + #[serde(skip_serializing_if = "Option::is_none")] + y: Option, + } + + assert_eq!( + s(A { + a: Noop, + b: B {}, + c0: C { y: None }, + c1: C { y: Some(1) }, + f: C { y: Some(2) }, + null: None, + i: I32(323232), + }), + Value::Map( + [ + ("a".into(), Value::Null), + ("b".into(), Value::Map(Map::new())), + ("c0".into(), Value::Map(Map::new())), + ( + "c1".into(), + Value::Map([("y".into(), Value::I64(1))].into()) + ), + ("y".into(), Value::I64(2)), + ("NULL".into(), Value::Null), + ("i".into(), Value::I64(323232)), + ] + .into() + ) + ); + } + + #[test] + fn test_enum() { + #[derive(Serialize, Debug, PartialEq)] + enum Enum0 { + V0, + V1, + #[serde(rename = "var")] + V2, + V3(i32), + } + assert_eq!(s(Enum0::V0), Value::String("V0".to_owned())); + assert_eq!(s(Enum0::V1), Value::String("V1".to_owned())); + assert_eq!(s(Enum0::V2), Value::String("var".to_owned())); + assert_eq!( + s(Enum0::V3(-1)), + Value::Map(Map::from([("V3".to_owned(), Value::I64(-1))])) + ); + } +} diff --git a/lib/flow-value/src/ser/iter_ser.rs b/lib/flow-value/src/ser/iter_ser.rs new file mode 100644 index 00000000..8d775cb6 --- /dev/null +++ b/lib/flow-value/src/ser/iter_ser.rs @@ -0,0 +1,56 @@ +pub struct Array { + iter: I, +} + +impl Array { + pub fn new(iter: I) -> Self { + Self { iter } + } +} + +impl serde::Serialize for Array +where + I: Iterator + Clone, + I::Item: serde::Serialize, +{ + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = s.serialize_seq(None)?; + for item in self.iter.clone() { + seq.serialize_element(&item)?; + } + seq.end() + } +} + +pub struct Map { + iter: I, +} + +impl Map { + pub fn new(iter: I) -> Self { + Self { iter } + } +} + +impl<'a, I, K, V> serde::Serialize for Map +where + I: Iterator + Clone, + K: serde::Serialize + 'a, + V: serde::Serialize + 'a, +{ + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = s.serialize_map(None)?; + for (k, v) in self.iter.clone() { + map.serialize_entry(&k, &v)?; + } + map.end() + } +} diff --git a/lib/flow-value/src/ser/map_key.rs b/lib/flow-value/src/ser/map_key.rs new file mode 100644 index 00000000..5bce5bcf --- /dev/null +++ b/lib/flow-value/src/ser/map_key.rs @@ -0,0 +1,180 @@ +use crate::Error; +use serde::ser::Impossible; + +pub(crate) struct MapKeySerializer; + +const fn key_must_be_a_string() -> Error { + Error::KeyMustBeAString +} + +impl serde::Serializer for MapKeySerializer { + type Ok = String; + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(variant.to_owned()) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + value.serialize(self) + } + + fn serialize_bool(self, _value: bool) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_i8(self, value: i8) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_i16(self, value: i16) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_i32(self, value: i32) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_u8(self, value: u8) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_u16(self, value: u16) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_u32(self, value: u32) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_u64(self, value: u64) -> Result { + Ok(itoa::Buffer::new().format(value).to_owned()) + } + + fn serialize_f32(self, _value: f32) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_f64(self, _value: f64) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_char(self, value: char) -> Result { + Ok(String::from(value)) + } + + fn serialize_str(self, value: &str) -> Result { + Ok(value.to_owned()) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + serde::Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_none(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_some(self, _value: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + std::fmt::Display, + { + Ok(value.to_string()) + } +} diff --git a/lib/flow-value/src/ser/maps.rs b/lib/flow-value/src/ser/maps.rs new file mode 100644 index 00000000..6b3c466f --- /dev/null +++ b/lib/flow-value/src/ser/maps.rs @@ -0,0 +1,106 @@ +use super::map_key::MapKeySerializer; +use super::seq::SerializeSeq; +use crate::{Error, Map, Value}; + +pub struct SerializeTupleVariant { + pub name: &'static str, + pub seq: SerializeSeq, +} + +impl serde::ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + use serde::ser::SerializeTuple; + self.seq.serialize_element(value)?; + Ok(()) + } + + fn end(self) -> Result { + use serde::ser::SerializeTuple; + let key = crate::Key::from(self.name); + let value = self.seq.end()?; + Ok(Value::Map(Map::from([(key, value)]))) + } +} + +pub struct SerializeStructVariant { + pub name: &'static str, + pub map: Map, +} + +impl serde::ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + let value = value.serialize(super::Serializer)?; + self.map.insert(crate::Key::from(key), value); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Map(Map::from([( + crate::Key::from(self.name), + Value::Map(self.map), + )]))) + } +} + +pub struct SerializeMap { + pub map: Map, + pub next_key: Option, +} + +impl serde::ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + self.next_key = Some(key.serialize(MapKeySerializer)?); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + let key = self + .next_key + .take() + .expect("serialize_value called before serialize_key"); + let value = value.serialize(super::Serializer)?; + self.map.insert(key, value); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Map(self.map)) + } +} + +impl serde::ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + serde::ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } +} diff --git a/lib/flow-value/src/ser/seq.rs b/lib/flow-value/src/ser/seq.rs new file mode 100644 index 00000000..a183ee47 --- /dev/null +++ b/lib/flow-value/src/ser/seq.rs @@ -0,0 +1,346 @@ +use serde::ser::Impossible; + +use super::Serializer; +use crate::{Error, Value}; + +pub enum SerializeSeq { + Bytes(Vec), + Array(Vec), +} + +impl Default for SerializeSeq { + fn default() -> Self { + Self::new() + } +} + +impl SerializeSeq { + pub fn new() -> Self { + SerializeSeq::Bytes(Vec::new()) + } +} + +impl TryFrom for u8 { + type Error = Value; + + fn try_from(value: Value) -> Result { + match value { + Value::U64(x) => u8::try_from(x).map_err(|_| value), + Value::I64(x) => u8::try_from(x).map_err(|_| value), + Value::U128(x) => u8::try_from(x).map_err(|_| value), + value => Err(value), + } + } +} + +impl serde::ser::SerializeSeq for SerializeSeq { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + match self { + Self::Array(vec) => { + let value = value.serialize(Serializer)?; + vec.push(value); + } + Self::Bytes(vec) => { + let value = value.serialize(Serializer)?; + match u8::try_from(value) { + Ok(v) => vec.push(v), + Err(v) => { + let Self::Bytes(old) = std::mem::replace(self, Self::Array(Vec::new())) + else { + unreachable!() + }; + let Self::Array(new) = self else { + unreachable!() + }; + new.extend(old.into_iter().map(Value::from).chain(std::iter::once(v))); + } + } + } + } + Ok(()) + } + + fn end(self) -> Result { + Ok(match self { + Self::Bytes(vec) => { + if vec.is_empty() { + Value::Array(Vec::new()) + } else { + Value::from(vec) + } + } + Self::Array(vec) => Value::Array(vec), + }) + } +} + +impl serde::ser::SerializeTuple for SerializeSeq { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleStruct for SerializeSeq { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +#[derive(Default)] +pub struct SerializeSeqNoBytes { + array: Vec, +} + +impl serde::ser::SerializeSeq for SerializeSeqNoBytes { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + let value = value.serialize(Serializer)?; + self.array.push(value); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Array(self.array)) + } +} + +impl serde::Serializer for SerializeSeqNoBytes { + type Ok = Value; + + type Error = Error; + + type SerializeSeq = Self; + + type SerializeTuple = Impossible; + + type SerializeTupleStruct = Impossible; + + type SerializeTupleVariant = Impossible; + + type SerializeMap = Impossible; + + type SerializeStruct = Impossible; + + type SerializeStructVariant = Impossible; + + fn serialize_bool(self, _v: bool) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_i8(self, _v: i8) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_i16(self, _v: i16) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_i32(self, _v: i32) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_i64(self, _v: i64) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_u8(self, _v: u8) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_u16(self, _v: u16) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_u32(self, _v: u32) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_u64(self, _v: u64) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_f32(self, _v: f32) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_f64(self, _v: f64) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_char(self, _v: char) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_str(self, _v: &str) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_none(self) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_some(self, _value: &T) -> Result + where + T: serde::Serialize + ?Sized, + { + Err(Error::ExpectedArray) + } + + fn serialize_unit(self) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: serde::Serialize + ?Sized, + { + Err(Error::ExpectedArray) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: serde::Serialize + ?Sized, + { + Err(Error::ExpectedArray) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(Self { + array: len.map(Vec::with_capacity).unwrap_or_default(), + }) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::ExpectedArray) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::ExpectedArray) + } +} + +impl serde::ser::SerializeTuple for SerializeSeqNoBytes { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} + +impl serde::ser::SerializeTupleStruct for SerializeSeqNoBytes { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Error> + where + T: ?Sized + serde::Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } +} diff --git a/lib/flow-value/src/ser/tagged_bytes.rs b/lib/flow-value/src/ser/tagged_bytes.rs new file mode 100644 index 00000000..6846f639 --- /dev/null +++ b/lib/flow-value/src/ser/tagged_bytes.rs @@ -0,0 +1,185 @@ +use crate::{Error, Value}; +use rust_decimal::Decimal; +use serde::ser::Impossible; + +pub enum TaggedBytes { + Decimal, + Bytes, +} + +impl serde::Serializer for TaggedBytes { + type Ok = Value; + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + fn is_human_readable(&self) -> bool { + false + } + + fn serialize_bytes(self, value: &[u8]) -> Result { + match self { + Self::Decimal => Ok(Value::Decimal(Decimal::deserialize( + value.try_into().map_err(|_| Error::InvalidLenght { + need: 16, + got: value.len(), + })?, + ))), + Self::Bytes => Ok(Value::from(value)), + } + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unreachable!(); + } + + fn serialize_newtype_struct(self, _: &'static str, _: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + unreachable!(); + } + + fn serialize_bool(self, _: bool) -> Result { + unreachable!(); + } + + fn serialize_i8(self, _: i8) -> Result { + unreachable!(); + } + + fn serialize_i16(self, _: i16) -> Result { + unreachable!(); + } + + fn serialize_i32(self, _: i32) -> Result { + unreachable!(); + } + + fn serialize_i64(self, _: i64) -> Result { + unreachable!(); + } + + fn serialize_u8(self, _: u8) -> Result { + unreachable!(); + } + + fn serialize_u16(self, _: u16) -> Result { + unreachable!(); + } + + fn serialize_u32(self, _: u32) -> Result { + unreachable!(); + } + + fn serialize_u64(self, _: u64) -> Result { + unreachable!(); + } + + fn serialize_f32(self, _: f32) -> Result { + unreachable!(); + } + + fn serialize_f64(self, _: f64) -> Result { + unreachable!(); + } + + fn serialize_char(self, _: char) -> Result { + unreachable!(); + } + + fn serialize_str(self, _: &str) -> Result { + unreachable!(); + } + + fn serialize_unit(self) -> Result { + unreachable!(); + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unreachable!(); + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: &T, + ) -> Result + where + T: ?Sized + serde::Serialize, + { + unreachable!(); + } + + fn serialize_none(self) -> Result { + unreachable!(); + } + + fn serialize_some(self, _: &T) -> Result + where + T: ?Sized + serde::Serialize, + { + unreachable!(); + } + + fn serialize_seq(self, _: Option) -> Result { + unreachable!(); + } + + fn serialize_tuple(self, _: usize) -> Result { + unreachable!(); + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + unreachable!(); + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + unreachable!(); + } + + fn serialize_map(self, _: Option) -> Result { + unreachable!(); + } + + fn serialize_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + unreachable!(); + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + unreachable!(); + } +} diff --git a/lib/flow-value/src/ser/text_repr.rs b/lib/flow-value/src/ser/text_repr.rs new file mode 100644 index 00000000..cf35d633 --- /dev/null +++ b/lib/flow-value/src/ser/text_repr.rs @@ -0,0 +1,63 @@ +use crate::Value; + +#[derive(Debug)] +pub struct TextRepr<'a>(&'a Value); + +impl<'a> TextRepr<'a> { + pub fn new(value: &Value) -> TextRepr<'_> { + TextRepr(value) + } +} + +impl<'a> serde::Serialize for TextRepr<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + const NAME: &str = "TextRepr"; + + let value = self.0; + let (i, k) = value.kind().variant(); + match value { + Value::Null => s.serialize_newtype_variant(NAME, i, k, &0), + Value::String(v) => s.serialize_newtype_variant(NAME, i, k, v), + Value::Bool(v) => s.serialize_newtype_variant(NAME, i, k, v), + Value::U64(v) => { + s.serialize_newtype_variant(NAME, i, k, itoa::Buffer::new().format(*v)) + } + Value::I64(v) => { + s.serialize_newtype_variant(NAME, i, k, itoa::Buffer::new().format(*v)) + } + Value::F64(v) => s.serialize_newtype_variant(NAME, i, k, ryu::Buffer::new().format(*v)), + Value::Decimal(v) => { + // TODO: no alloc impl + s.serialize_newtype_variant(NAME, i, k, &v.to_string()) + } + Value::I128(v) => { + s.serialize_newtype_variant(NAME, i, k, itoa::Buffer::new().format(*v)) + } + Value::U128(v) => { + s.serialize_newtype_variant(NAME, i, k, itoa::Buffer::new().format(*v)) + } + Value::B32(v) => { + s.serialize_newtype_variant(NAME, i, k, &bs58::encode(v).into_string()) + } + Value::B64(v) => { + s.serialize_newtype_variant(NAME, i, k, &bs58::encode(v).into_string()) + } + Value::Bytes(v) => s.serialize_newtype_variant(NAME, i, k, &base64::encode(v)), + Value::Array(v) => s.serialize_newtype_variant( + NAME, + i, + k, + &super::iter_ser::Array::new(v.iter().map(Self::new)), + ), + Value::Map(v) => s.serialize_newtype_variant( + NAME, + i, + k, + &super::iter_ser::Map::new(v.iter().map(|(k, v)| (k, Self::new(v)))), + ), + } + } +} diff --git a/lib/flow-value/src/signature.rs b/lib/flow-value/src/signature.rs new file mode 100644 index 00000000..ac503697 --- /dev/null +++ b/lib/flow-value/src/signature.rs @@ -0,0 +1,64 @@ +use crate::with::AsSignature; +use solana_sdk::signature::Signature; + +type Target = Signature; + +type As = AsSignature; + +pub mod opt { + use serde_with::{DeserializeAs, SerializeAs}; + + pub fn serialize(sig: &Option, s: S) -> Result + where + S: serde::Serializer, + { + Option::::serialize_as(sig, s) + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + Option::::deserialize_as(d) + } +} + +pub fn serialize(p: &Target, s: S) -> Result +where + S: serde::Serializer, +{ + As::serialize(p, s) +} + +pub fn deserialize<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + As::deserialize(d) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Value; + + fn de<'de, D: serde::Deserializer<'de>>(d: D) -> Signature { + deserialize(d).unwrap() + } + + #[test] + fn test_deserialize_value() { + let s = Signature::new_unique(); + assert_eq!(de(Value::B64(s.into())), s); + assert_eq!(de(Value::String(s.to_string())), s); + } + + #[test] + fn test_serialize() { + let s = Signature::new_unique(); + assert_eq!( + serialize(&s, crate::ser::Serializer).unwrap(), + Value::B64(s.into()) + ); + } +} diff --git a/lib/flow-value/src/tests/ser.json b/lib/flow-value/src/tests/ser.json new file mode 100644 index 00000000..699bfb57 --- /dev/null +++ b/lib/flow-value/src/tests/ser.json @@ -0,0 +1 @@ +[-128,-32768,-2147483648,-9223372036854775808,255,65535,4294967295,18446744073709551615,null,"some\nstring",true,false,"1.121",1.121,1.121,"GQZRKDqVzM4DXGGMEUNdnBD3CC4TTywh3PwgjYPBm8W9","56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu","6pc4LiB8KHAPvbUbkozrTcPL5zXspYBdATv5raNDyVbhiKjrKokLb9o111kxTD5KkPVd7UBSCcFcnWFkrJ82Hu6"] diff --git a/lib/flow-value/src/value_type.rs b/lib/flow-value/src/value_type.rs new file mode 100644 index 00000000..5645887b --- /dev/null +++ b/lib/flow-value/src/value_type.rs @@ -0,0 +1,148 @@ +use crate::Value; + +#[derive(Clone, Copy, Debug)] +#[repr(u32)] +pub enum Variant { + Null = 0, + String = 1, + Bool = 2, + U64 = 3, + I64 = 4, + F64 = 5, + Decimal = 6, + I128 = 7, + U128 = 8, + B32 = 9, + B64 = 10, + Bytes = 11, + Array = 12, + Map = 13, +} + +impl Variant { + pub const fn variant(&self) -> (u32, &'static str) { + let idx = *self as u32; + (idx, keys::ALL[idx as usize]) + } +} + +pub mod keys { + pub const NULL: &str = "N"; + pub const STRING: &str = "S"; + pub const BOOL: &str = "B"; + pub const U64: &str = "U"; + pub const I64: &str = "I"; + pub const F64: &str = "F"; + pub const DECIMAL: &str = "D"; + pub const I128: &str = "I1"; + pub const U128: &str = "U1"; + pub const B32: &str = "B3"; + pub const B64: &str = "B6"; + pub const BYTES: &str = "BY"; + pub const ARRAY: &str = "A"; + pub const MAP: &str = "M"; + + pub const ALL: &[&str] = &[ + NULL, STRING, BOOL, U64, I64, F64, DECIMAL, I128, U128, B32, B64, BYTES, ARRAY, MAP, + ]; +} + +struct ValueTypeVisitor; + +impl<'de> serde::de::Visitor<'de> for ValueTypeVisitor { + type Value = Variant; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("ValueType") + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(match v { + 0 => Variant::Null, + 1 => Variant::String, + 2 => Variant::Bool, + 3 => Variant::U64, + 4 => Variant::I64, + 5 => Variant::F64, + 6 => Variant::Decimal, + 7 => Variant::I128, + 8 => Variant::U128, + 9 => Variant::B32, + 10 => Variant::B64, + 11 => Variant::Bytes, + 12 => Variant::Array, + 13 => Variant::Map, + _ => { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(v as u64), + &"value in [0, 13]", + )) + } + }) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(match v { + keys::NULL => Variant::Null, + keys::STRING => Variant::String, + keys::BOOL => Variant::Bool, + keys::U64 => Variant::U64, + keys::I64 => Variant::I64, + keys::F64 => Variant::F64, + keys::DECIMAL => Variant::Decimal, + keys::I128 => Variant::I128, + keys::U128 => Variant::U128, + keys::B32 => Variant::B32, + keys::B64 => Variant::B64, + keys::BYTES => Variant::Bytes, + keys::ARRAY => Variant::Array, + keys::MAP => Variant::Map, + _ => { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"one of valid keys", + )) + } + }) + } +} + +impl<'de> serde::Deserialize<'de> for Variant { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + if d.is_human_readable() { + d.deserialize_str(ValueTypeVisitor) + } else { + d.deserialize_u32(ValueTypeVisitor) + } + } +} + +impl Value { + pub fn kind(&self) -> Variant { + match self { + Value::Null => Variant::Null, + Value::String(_) => Variant::String, + Value::Bool(_) => Variant::Bool, + Value::U64(_) => Variant::U64, + Value::I64(_) => Variant::I64, + Value::F64(_) => Variant::F64, + Value::Decimal(_) => Variant::Decimal, + Value::I128(_) => Variant::I128, + Value::U128(_) => Variant::U128, + Value::B32(_) => Variant::B32, + Value::B64(_) => Variant::B64, + Value::Bytes(_) => Variant::Bytes, + Value::Array(_) => Variant::Array, + Value::Map(_) => Variant::Map, + } + } +} diff --git a/lib/flow-value/src/with.rs b/lib/flow-value/src/with.rs new file mode 100644 index 00000000..21e03f18 --- /dev/null +++ b/lib/flow-value/src/with.rs @@ -0,0 +1,663 @@ +//! [serde_with](https://docs.rs/serde_with/latest/serde_with/) helpers. + +use serde::{ + de::{self, MapAccess}, + Deserialize, Serialize, +}; +use serde_with::serde_conv; +use std::{borrow::Cow, convert::Infallible}; +use std::{mem::MaybeUninit, ops::ControlFlow}; + +pub use decimal::AsDecimal; +#[cfg(feature = "solana")] +pub use keypair::AsKeypair; +#[cfg(feature = "solana")] +pub use pubkey::AsPubkey; +#[cfg(feature = "solana")] +pub use signature::AsSignature; + +fn try_from_fn_erased( + buffer: &mut [MaybeUninit], + mut generator: impl FnMut(usize) -> Result, +) -> ControlFlow { + for (i, elem) in buffer.iter_mut().enumerate() { + let item = match generator(i) { + Ok(item) => item, + Err(error) => return ControlFlow::Break(error), + }; + elem.write(item); + } + + ControlFlow::Continue(()) +} + +fn try_from_fn(cb: F) -> Result<[T; N], E> +where + F: FnMut(usize) -> Result, +{ + let mut array = [const { MaybeUninit::uninit() }; N]; + match try_from_fn_erased(&mut array, cb) { + ControlFlow::Break(error) => Err(error), + ControlFlow::Continue(()) => Ok(array.map(|uninit| unsafe { uninit.assume_init() })), + } +} + +#[cfg(feature = "solana")] +pub(crate) mod pubkey { + use std::marker::PhantomData; + + use super::*; + use five8::BASE58_ENCODED_32_MAX_LEN; + use solana_sdk::pubkey::Pubkey; + + struct CustomPubkey<'a>(Cow<'a, Pubkey>); + + pub(crate) const TOKEN: &str = "$$p"; + + impl<'a> Serialize for CustomPubkey<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_newtype_struct(TOKEN, &crate::Bytes((*self.0).as_ref())) + } + } + + impl<'a, 'de> Deserialize<'de> for CustomPubkey<'a> { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_newtype_struct(TOKEN, Visitor { map: true }) + .map(|pk| CustomPubkey(Cow::Owned(pk))) + } + } + + struct Visitor { + map: bool, + } + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Pubkey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.map { + formatter.write_str("pubkey, keypair, base58 string, or adapter wallet") + } else { + formatter.write_str("pubkey, keypair, or base58 string") + } + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + match v.len() { + 32 => Ok(Pubkey::new_from_array(v.try_into().unwrap())), + // see ed25519-dalek's Keypair + 64 => Ok(Pubkey::new_from_array(v[32..].try_into().unwrap())), + l => Err(serde::de::Error::invalid_length(l, &"32 or 64")), + } + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() > BASE58_ENCODED_32_MAX_LEN { + let mut buf = [0u8; 64]; + five8::decode_64(v, &mut buf).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"pubkey or keypair encoded in bs58", + ) + })?; + Ok(Pubkey::new_from_array(buf[32..].try_into().unwrap())) + } else { + let mut buf = [0u8; 32]; + five8::decode_32(v, &mut buf).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"pubkey or keypair encoded in bs58", + ) + })?; + Ok(Pubkey::new_from_array(buf)) + } + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let hint = seq.size_hint(); + match hint { + Some(n) => { + if n == 32 { + let buffer: [u8; 32] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &"32"))?) + })?; + Ok(Pubkey::new_from_array(buffer)) + } else if n == 64 { + for _ in 0..32 { + seq.next_element::()?; + } + let buffer: [u8; 32] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i + 32, &"64"))?) + })?; + Ok(Pubkey::new_from_array(buffer)) + } else { + Err(de::Error::invalid_length(n, &"32 or 64")) + } + } + None => { + let buffer: [u8; 32] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &"32"))?) + })?; + let next = seq.next_element::()?; + if let Some(x) = next { + let mut result = [0u8; 32]; + result[0] = x; + let buffer: [u8; 31] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &"64"))?) + })?; + result[1..].copy_from_slice(&buffer); + Ok(Pubkey::new_from_array(result)) + } else { + Ok(Pubkey::new_from_array(buffer)) + } + } + } + } + + fn visit_newtype_struct(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + if self.map { + map.next_key::>()?; + let value = map.next_value::()?; + Ok(value.0) + } else { + Err(de::Error::invalid_type(de::Unexpected::Map, &self)) + } + } + } + + struct CustomPubkeyNoMap(Pubkey); + + impl<'de> Deserialize<'de> for CustomPubkeyNoMap { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_any(Visitor { map: false }) + .map(CustomPubkeyNoMap) + } + } + + #[allow(non_camel_case_types)] + struct public_key; + + impl Key for public_key { + const KEY: &'static str = "public_key"; + fn new() -> Self { + Self + } + } + + trait Key { + const KEY: &'static str; + fn new() -> Self; + } + + struct Const(K); + + impl<'de, K> Deserialize<'de> for Const + where + K: Key, + { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_str(StrVisitor::(PhantomData)) + } + } + + struct StrVisitor(PhantomData K>); + + impl<'de, K: Key> de::Visitor<'de> for StrVisitor { + type Value = Const; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(K::KEY) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v == K::KEY { + Ok(Const(K::new())) + } else { + Err(de::Error::invalid_value(de::Unexpected::Str(v), &K::KEY)) + } + } + } + + fn to_custom_pubkey<'a>(pk: &'a Pubkey) -> CustomPubkey<'a> { + CustomPubkey(Cow::Borrowed(pk)) + } + fn from_custom_pubkey(pk: CustomPubkey<'static>) -> Result { + Ok(pk.0.into_owned()) + } + serde_conv!(pub AsPubkey, Pubkey, to_custom_pubkey, from_custom_pubkey); + + #[cfg(test)] + mod tests { + use super::*; + use crate::Value; + use serde_with::{DeserializeAs, SerializeAs}; + use solana_sdk::{signature::Keypair, signer::Signer}; + + #[test] + fn test_pubkey() { + let key = Pubkey::new_unique(); + let value = AsPubkey::serialize_as(&key, crate::ser::Serializer).unwrap(); + assert!(matches!(value, Value::B32(_))); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::Map(crate::map! { "public_key" => key }); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::String(key.to_string()); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::Array(key.to_bytes().map(Value::from).to_vec()); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let keypair = Keypair::new(); + let key = keypair.pubkey(); + let value = Value::B64(keypair.to_bytes()); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::String(keypair.to_base58_string()); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::Array(keypair.to_bytes().map(Value::from).to_vec()); + let de_key = AsPubkey::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + } + } +} + +#[cfg(feature = "solana")] +pub(crate) mod signature { + use super::*; + use solana_sdk::signature::Signature; + + struct CustomSignature<'a>(Cow<'a, Signature>); + + pub(crate) const TOKEN: &str = "$$s"; + + impl<'a> Serialize for CustomSignature<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_newtype_struct(TOKEN, &crate::Bytes((*self.0).as_ref())) + } + } + + impl<'a, 'de> Deserialize<'de> for CustomSignature<'a> { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_newtype_struct(TOKEN, Visitor) + .map(|pk| CustomSignature(Cow::Owned(pk))) + } + } + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Signature; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("signature or bs58 string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let buffer: [u8; 64] = v + .try_into() + .map_err(|_| de::Error::invalid_length(v.len(), &"64"))?; + Ok(Signature::from(buffer)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut buffer = [0u8; 64]; + five8::decode_64(v, &mut buffer).map_err(de::Error::custom)?; + Ok(Signature::from(buffer)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let buffer: [u8; 64] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &"64"))?) + })?; + + Ok(Signature::from(buffer)) + } + + fn visit_newtype_struct(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self) + } + } + + fn to_custom_signature<'a>(pk: &'a Signature) -> CustomSignature<'a> { + CustomSignature(Cow::Borrowed(pk)) + } + fn from_custom_signature(pk: CustomSignature<'static>) -> Result { + Ok(pk.0.into_owned()) + } + serde_conv!(pub AsSignature, Signature, to_custom_signature, from_custom_signature); + + #[cfg(test)] + mod tests { + use super::*; + use crate::Value; + use serde_with::{DeserializeAs, SerializeAs}; + use solana_sdk::signature::Signature; + + #[test] + fn test_signature() { + let sig = Signature::new_unique(); + let value = AsSignature::serialize_as(&sig, crate::ser::Serializer).unwrap(); + assert!(matches!(value, Value::B64(_))); + let de_sig = AsSignature::deserialize_as(value).unwrap(); + assert_eq!(sig, de_sig); + + let value = Value::String(sig.to_string()); + let de_sig = AsSignature::deserialize_as(value).unwrap(); + assert_eq!(sig, de_sig); + + let value = Value::Array( + sig.as_ref() + .iter() + .map(|i| Value::from(*i)) + .collect::>(), + ); + let de_sig = AsSignature::deserialize_as(value).unwrap(); + assert_eq!(sig, de_sig); + } + } +} + +#[cfg(feature = "solana")] +pub(crate) mod keypair { + use super::*; + use solana_sdk::signer::keypair::Keypair; + + struct CustomKeypair([u8; 64]); + + pub(crate) const TOKEN: &str = "$$k"; + + impl Serialize for CustomKeypair { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_newtype_struct(TOKEN, &crate::Bytes(&self.0)) + } + } + + impl<'de> Deserialize<'de> for CustomKeypair { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_newtype_struct(TOKEN, Visitor) + } + } + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = CustomKeypair; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("keypair or bs58 string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let buffer: [u8; 64] = v + .try_into() + .map_err(|_| de::Error::invalid_length(v.len(), &"64"))?; + Ok(CustomKeypair(buffer)) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut buffer = [0u8; 64]; + five8::decode_64(v, &mut buffer).map_err(de::Error::custom)?; + Ok(CustomKeypair(buffer)) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let buffer: [u8; 64] = try_from_fn(|i| { + Ok(seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(i, &"64"))?) + })?; + + Ok(CustomKeypair(buffer)) + } + + fn visit_newtype_struct(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self) + } + } + + fn to_custom_keypair(k: &'_ Keypair) -> CustomKeypair { + CustomKeypair(k.to_bytes()) + } + fn from_custom_keypair(k: CustomKeypair) -> Result { + Keypair::from_bytes(&k.0).map_err(|error| error.to_string()) + } + serde_conv!(pub AsKeypair, Keypair, to_custom_keypair, from_custom_keypair); + + #[cfg(test)] + mod tests { + use super::*; + use crate::Value; + use serde_with::{DeserializeAs, SerializeAs}; + + #[test] + fn test_keypair() { + let key = Keypair::new(); + let value = AsKeypair::serialize_as(&key, crate::ser::Serializer).unwrap(); + assert!(matches!(value, Value::B64(_))); + let de_key = AsKeypair::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::String(key.to_base58_string()); + let de_key = AsKeypair::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + + let value = Value::Array(key.to_bytes().map(Value::from).to_vec()); + let de_key = AsKeypair::deserialize_as(value).unwrap(); + assert_eq!(key, de_key); + } + } +} + +pub(crate) mod decimal { + use super::*; + use rust_decimal::Decimal; + + struct CustomDecimal<'a>(Cow<'a, Decimal>); + + pub(crate) const TOKEN: &str = "$$d"; + + impl<'a> Serialize for CustomDecimal<'a> { + fn serialize(&self, s: S) -> Result + where + S: serde::Serializer, + { + s.serialize_newtype_struct(TOKEN, &crate::Bytes(&(*self.0).serialize())) + } + } + + impl<'a, 'de> Deserialize<'de> for CustomDecimal<'a> { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_newtype_struct(TOKEN, Visitor) + .map(|d| CustomDecimal(Cow::Owned(d))) + } + } + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Decimal; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("decimal") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let buf: [u8; 16] = v + .try_into() + .map_err(|_| de::Error::invalid_length(v.len(), &"16"))?; + Ok(Decimal::deserialize(buf)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(Decimal::from(v)) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(Decimal::from(v)) + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + // TODO: this is lossy + Decimal::try_from(v).map_err(serde::de::Error::custom) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let v = v.trim(); + if v.bytes().any(|c| c == b'e' || c == b'E') { + Decimal::from_scientific(v).map_err(serde::de::Error::custom) + } else { + v.parse().map_err(serde::de::Error::custom) + } + } + + fn visit_newtype_struct(self, d: D) -> Result + where + D: serde::Deserializer<'de>, + { + d.deserialize_any(self) + } + } + + fn to_custom_decimal<'a>(d: &'a Decimal) -> CustomDecimal<'a> { + CustomDecimal(Cow::Borrowed(d)) + } + fn from_custom_decimal(d: CustomDecimal<'static>) -> Result { + Ok(d.0.into_owned()) + } + serde_conv!(pub AsDecimal, Decimal, to_custom_decimal, from_custom_decimal); + + #[cfg(test)] + mod tests { + use super::*; + use crate::Value; + use rust_decimal_macros::dec; + use serde_with::{DeserializeAs, SerializeAs}; + + fn de<'de, D: serde::Deserializer<'de>>(d: D) -> Decimal { + AsDecimal::deserialize_as(d).unwrap() + } + + #[test] + fn test_decimal() { + assert_eq!( + AsDecimal::serialize_as(&Decimal::MAX, crate::ser::Serializer).unwrap(), + Value::Decimal(Decimal::MAX) + ); + assert_eq!(de(Value::U64(100)), dec!(100)); + assert_eq!(de(Value::I64(-1)), dec!(-1)); + assert_eq!(de(Value::Decimal(Decimal::MAX)), Decimal::MAX); + assert_eq!(de(Value::F64(1231.2221)), dec!(1231.2221)); + assert_eq!(de(Value::String("1234.0".to_owned())), dec!(1234)); + assert_eq!(de(Value::String(" 1234.0".to_owned())), dec!(1234)); + assert_eq!(de(Value::String("1e5".to_owned())), dec!(100000)); + assert_eq!(de(Value::String(" 1e5".to_owned())), dec!(100000)); + } + } +} diff --git a/lib/space-lib/Cargo.toml b/lib/space-lib/Cargo.toml new file mode 100644 index 00000000..b9251704 --- /dev/null +++ b/lib/space-lib/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "space-lib" +version = "0.5.1" +edition = "2021" +license = "Apache-2.0" +authors = ["Knarkzel "] +documentation = "https://docs.rs/space-lib" +description = "WebAssembly host functions for Space Operator" + +[features] +json = ["serde_json"] + +[dependencies] +rmp-serde = "1.1" +space-macro = { workspace = true } +serde_json = { version = "1.0", optional = true } +serde = { version = "1.0", features = ["derive"] } diff --git a/lib/space-lib/README.md b/lib/space-lib/README.md new file mode 100644 index 00000000..dec5d4dd --- /dev/null +++ b/lib/space-lib/README.md @@ -0,0 +1,46 @@ +# space-lib + +``` +cargo add space-lib +``` + +This crate provides WebAssembly host functions and other utilities +for Space Operator. + +## Example + +```rust +use space_lib::{space, Result}; +use serde::{Serialize, Deserialize}; + +#[derive(Deserialize)] +struct Input { + value: usize, + name: String, +} + +#[derive(Serialize)] +struct Output { + value: usize, + name: String, +} + +#[space] +fn main(input: Input) -> Result { + let output = Output { + value: input.value * 2, + name: input.name.chars().rev().collect(), + }; + Ok(output) +} +``` + +## HTTP client + +```rust +use space_lib::Request; + +let body = Request::get("https://www.spaceoperator.com") + .call()? + .into_string()?; +``` diff --git a/lib/space-lib/src/common.rs b/lib/space-lib/src/common.rs new file mode 100644 index 00000000..1e96a00d --- /dev/null +++ b/lib/space-lib/src/common.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub enum Method { + GET, + POST, + DELETE, + HEAD, + PATCH, + PUT, +} + +#[derive(Serialize, Deserialize)] +pub struct RequestData { + pub url: String, + pub headers: Vec, + pub queries: Vec, + pub method: Method, +} diff --git a/lib/space-lib/src/error.rs b/lib/space-lib/src/error.rs new file mode 100644 index 00000000..458e07ba --- /dev/null +++ b/lib/space-lib/src/error.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +// Error type +#[derive(Debug, Serialize, Deserialize)] +pub struct Error { + description: String, +} + +impl Error { + pub fn new(message: T) -> Self { + Self { + description: message.to_string(), + } + } +} + +impl From for Error { + fn from(value: T) -> Self { + Self { + description: value.to_string(), + } + } +} + +// Result type +pub type Result = std::result::Result; diff --git a/lib/space-lib/src/ffi.rs b/lib/space-lib/src/ffi.rs new file mode 100644 index 00000000..29446fc4 --- /dev/null +++ b/lib/space-lib/src/ffi.rs @@ -0,0 +1,38 @@ +use crate::{ + common::{Method, RequestData}, + Result, +}; + +extern "C" { + fn http_call_request(bytes: u32, bytes_len: u32) -> u64; +} + +/// Calls http request, then returns body +pub fn call_request( + url: String, + headers: Vec, + queries: Vec, + method: Method, +) -> Result> { + // Call ffi function + let data = RequestData { + url, + headers, + queries, + method, + }; + let bytes = rmp_serde::to_vec_named(&data)?; + let offset = unsafe { http_call_request(bytes.as_ptr() as u32, bytes.len() as u32) }; + + // Extract [len, data] + if offset == 0 { + Err("Expected data, got null ptr")? + } else { + let slice = unsafe { + let len = *(offset as *const u32); + let data = (offset + 4) as *const u8; + std::slice::from_raw_parts(data, len as usize) + }; + Ok(rmp_serde::from_slice(slice)?) + } +} diff --git a/lib/space-lib/src/http.rs b/lib/space-lib/src/http.rs new file mode 100644 index 00000000..7a458e4c --- /dev/null +++ b/lib/space-lib/src/http.rs @@ -0,0 +1,88 @@ +use crate::{common::Method, ffi, Result}; + +pub struct Request { + url: String, + method: Method, + headers: Vec, + queries: Vec, +} + +impl Request { + /// Create a new request with method. + pub fn new>(url: T, method: Method) -> Self { + Self { + url: url.into(), + method, + headers: Vec::new(), + queries: Vec::new(), + } + } + + /// Make a GET request. + pub fn get>(url: T) -> Self { + Self::new(url, Method::GET) + } + + /// Make a POST request. + pub fn post>(url: T) -> Self { + Self::new(url, Method::POST) + } + + /// Make a DELETE request. + pub fn delete>(url: T) -> Self { + Self::new(url, Method::DELETE) + } + + /// Make a HEAD request. + pub fn head>(url: T) -> Self { + Self::new(url, Method::HEAD) + } + + /// Make a PATCH request. + pub fn patch>(url: T) -> Self { + Self::new(url, Method::PATCH) + } + + /// Make a PUT request. + pub fn put>(url: T) -> Self { + Self::new(url, Method::PUT) + } + + /// Set a header field. + pub fn set(mut self, header: T, value: U) -> Self { + self.headers.extend([header.to_string(), value.to_string()]); + self + } + + /// Set a query parameter. + pub fn query(mut self, param: T, value: U) -> Self { + self.queries.extend([param.to_string(), value.to_string()]); + self + } + + /// Send the request. + pub fn call(self) -> Result { + let bytes = ffi::call_request(self.url, self.headers, self.queries, self.method)?; + Ok(Response { bytes }) + } +} + +pub struct Response { + bytes: Vec, +} + +impl Response { + pub fn into_vec(self) -> Vec { + self.bytes + } + + pub fn into_string(self) -> Result { + Ok(std::str::from_utf8(&self.bytes).map(|it| it.to_string())?) + } + + #[cfg(feature = "json")] + pub fn into_json(self) -> Result { + let json = std::str::from_utf8(&self.bytes)?; + Ok(serde_json::from_str(json)?) + } +} diff --git a/lib/space-lib/src/lib.rs b/lib/space-lib/src/lib.rs new file mode 100644 index 00000000..5e3ad0ba --- /dev/null +++ b/lib/space-lib/src/lib.rs @@ -0,0 +1,57 @@ +//! This crate provides WebAssembly host functions and other utilities for Space Operator. +//! +//! ## Example +//! +//! ```rust,ignore +//! use space_lib::{space, Result}; +//! use serde::{Serialize, Deserialize}; +//! +//! #[derive(Deserialize)] +//! struct Input { +//! value: usize, +//! name: String, +//! } +//! +//! #[derive(Serialize)] +//! struct Output { +//! value: usize, +//! name: String, +//! } +//! +//! #[space] +//! fn main(input: Input) -> Result { +//! let output = Output { +//! value: input.value * 2, +//! name: input.name.chars().rev().collect(), +//! }; +//! Ok(output) +//! } +//! ``` +//! +//! ## HTTP client +//! +//! ```rust,ignore +//! use space_lib::Request; +//! +//! let body = Request::get("https://www.spaceoperator.com") +//! .call()? +//! .into_string()?; +//! ``` + +// Modules +pub mod common; +mod error; +mod ffi; +mod http; + +// Exports +pub use error::{Error, Result}; +pub use http::{Request, Response}; +pub use rmp_serde; +pub use space_macro::space; + +#[repr(C)] +pub struct SpaceSlice { + pub len: usize, + pub ptr: *mut u8, +} diff --git a/lib/space-macro/Cargo.toml b/lib/space-macro/Cargo.toml new file mode 100644 index 00000000..bcdc69a0 --- /dev/null +++ b/lib/space-macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "space-macro" +version = "0.2.1" +edition = "2021" +license = "Apache-2.0" +authors = ["Knarkzel "] +documentation = "https://docs.rs/space-lib" +description = "Macro for space-lib" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "1.0", features = ["full"] } diff --git a/lib/space-macro/src/lib.rs b/lib/space-macro/src/lib.rs new file mode 100644 index 00000000..c51e30e6 --- /dev/null +++ b/lib/space-macro/src/lib.rs @@ -0,0 +1,56 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{Error, FnArg, ItemFn, Pat, PatIdent, PatType}; + +#[proc_macro_attribute] +pub fn space(_: TokenStream, input: TokenStream) -> TokenStream { + // Parse ast from token stream + let ast = syn::parse::(input).expect("Place this attribute above a function"); + + // Signature and output type + let name = ast.sig.ident; + let output = ast.sig.output; + let stub_name = format_ident!("{name}_stub"); + + // Stub + let body = ast.block.stmts; + let stub = match ast.sig.inputs.first() { + Some(FnArg::Typed(PatType { pat, ty, .. })) => match &**pat { + Pat::Ident(PatIdent { + mutability, ident, .. + }) => quote! { + let #ident = ::space_lib::rmp_serde::from_slice::<#ty>(__input_bytes).unwrap(); + + // Actual code + fn #stub_name(#mutability #ident: #ty) #output { + #(#body);* + } + let output = #stub_name(#ident); + + // Serialize output + let __output_bytes = ::space_lib::rmp_serde::to_vec_named(&output).unwrap().leak(); + ::std::boxed::Box::new(::space_lib::SpaceSlice { + len: __output_bytes.len(), + ptr: __output_bytes.as_mut_ptr(), + }) + }, + _ => Error::new(name.span(), "expected ident").to_compile_error(), + }, + _ => Error::new(name.span(), "expected one argument").to_compile_error(), + }; + + quote! { + #[no_mangle] + fn #name(ptr: usize) -> ::std::boxed::Box<::space_lib::SpaceSlice> { + // Deserialize input + let __input_bytes = unsafe { + let len = *(ptr as *const usize); + let data = (ptr + 4) as *mut u8; + ::std::slice::from_raw_parts(data, len) + }; + + #stub + } + } + .into() +} diff --git a/lib/space-operator-cli/CHANGELOG.md b/lib/space-operator-cli/CHANGELOG.md new file mode 100644 index 00000000..126a7031 --- /dev/null +++ b/lib/space-operator-cli/CHANGELOG.md @@ -0,0 +1,14 @@ +# Change log + +## 0.5.0 + +- Use `Wallet` instead of `Keypair`. + +## 0.4.1 + +- Update README.md + +## 0.4.0 + +- Use `serde_as` for node inputs, outputs. +- Use `pub` for node inputs, outputs. diff --git a/lib/space-operator-cli/Cargo.lock b/lib/space-operator-cli/Cargo.lock new file mode 100644 index 00000000..f7dc2a64 --- /dev/null +++ b/lib/space-operator-cli/Cargo.lock @@ -0,0 +1,2965 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap-markdown" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ebc67e6266e14f8b31541c2f204724fa2ac7ad5c17d6f5908fbb92a60f42cff" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_builder" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "clru" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version", +] + +[[package]] +name = "faster-hex" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gix" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048b8d1ae2104f045cb37e5c450fc49d5d8af22609386bfc739c11ba88995eb" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-actor" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665" +dependencies = [ + "bstr", + "gix-date", + "gix-utils", + "itoa", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-attributes" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebccbf25aa4a973dd352564a9000af69edca90623e8a16dad9cbc03713131311" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-chunk" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52" +dependencies = [ + "thiserror", +] + +[[package]] +name = "gix-command" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff2e692b36bbcf09286c70803006ca3fd56551a311de450be317a0ab8ea92e7" +dependencies = [ + "bstr", + "gix-path", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133b06f67f565836ec0c473e2116a60fb74f80b6435e21d88013ac0e3c60fc78" +dependencies = [ + "bstr", + "gix-chunk", + "gix-features", + "gix-hash", + "memmap2", + "thiserror", +] + +[[package]] +name = "gix-config" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "memchr", + "once_cell", + "smallvec", + "thiserror", + "unicode-bom", + "winnow", +] + +[[package]] +name = "gix-config-value" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c84b7af01e68daf7a6bb8bb909c1ff5edb3ce4326f1f43063a5a96d3c3c8a5" +dependencies = [ + "bstr", + "itoa", + "jiff", + "thiserror", +] + +[[package]] +name = "gix-diff" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c9afd80fff00f8b38b1c1928442feb4cd6d2232a6ed806b6b193151a3d336c" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "imara-diff", + "thiserror", +] + +[[package]] +name = "gix-dir" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed3a9076661359a1c5a27c12ad6c3ebe2dd96b8b3c0af6488ab7c128b7bdd98" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0577366b9567376bc26e815fd74451ebd0e6218814e242f8e5b7072c58d956d2" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-hash", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-features" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" +dependencies = [ + "crc32fast", + "flate2", + "gix-hash", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "prodash", + "sha1_smol", + "thiserror", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4121790ae140066e5b953becc72e7496278138d19239be2e63b5067b0843119e" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" +dependencies = [ + "fastrand", + "gix-features", + "gix-utils", +] + +[[package]] +name = "gix-glob" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" +dependencies = [ + "faster-hex", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" +dependencies = [ + "gix-hash", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e447cd96598460f5906a0f6c75e950a39f98c2705fc755ad2f2020c9e937fab7" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-index" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.14.5", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-object" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-odb" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3158068701c17df54f0ab2adda527f5a6aca38fd5fd80ceb7e3c0a2717ec747" +dependencies = [ + "arc-swap", + "gix-date", + "gix-features", + "gix-fs", + "gix-hash", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3223aa342eee21e1e0e403cad8ae9caf9edca55ef84c347738d10681676fd954" +dependencies = [ + "clru", + "gix-chunk", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-packetline-blocking" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9802304baa798dd6f5ff8008a2b6516d54b74a69ca2d3a2b9e2d6c3b5556b40" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfc4febd088abdcbc9f1246896e57e37b7a34f6909840045a1767c6dafac7af" +dependencies = [ + "bstr", + "gix-trace", + "home", + "once_cell", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d23bf239532b4414d0e63b8ab3a65481881f7237ed9647bb10c1e3cc54c5ceb" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + +[[package]] +name = "gix-quote" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff" +dependencies = [ + "bstr", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-ref" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", + "winnow", +] + +[[package]] +name = "gix-refspec" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb005f82341ba67615ffdd9f7742c87787544441c88090878393d0682869ca6" +dependencies = [ + "bstr", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4621b219ac0cdb9256883030c3d56a6c64a6deaa829a92da73b9a576825e1e" +dependencies = [ + "bstr", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", + "thiserror", +] + +[[package]] +name = "gix-revwalk" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41e72544b93084ee682ef3d5b31b1ba4d8fa27a017482900e5e044d5b1b3984" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "gix-status" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70d35ba639f0c16a6e4cca81aa374a05f07b23fa36ee8beb72c100d98b4ffea" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror", +] + +[[package]] +name = "gix-submodule" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529d0af78cc2f372b3218f15eb1e3d1635a21c8937c12e2dd0b6fc80c2ca874b" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "14.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "once_cell", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b" + +[[package]] +name = "gix-traverse" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "home", + "thiserror", + "url", +] + +[[package]] +name = "gix-utils" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" +dependencies = [ + "bstr", + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2badbb64e57b404593ee26b752c26991910fd0d81fe6f9a71c1a8309b6c86" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "gix-worktree" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c312ad76a3f2ba8e865b360d5cb3aa04660971d16dec6dd0ce717938d903149a" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "imara-diff" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" +dependencies = [ + "ahash", + "hashbrown 0.14.5", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jiff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prodash" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "space-operator-cli" +version = "0.6.0" +dependencies = [ + "bon", + "cargo_metadata", + "chrono", + "clap", + "clap-markdown", + "console", + "directories", + "error-stack", + "futures", + "gix", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "reqwest", + "semver", + "serde", + "serde_json", + "similar", + "spo-postgrest", + "strum", + "syn", + "thiserror", + "tokio", + "toml", + "url", + "uuid", + "xshell", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spo-postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2f6cfdcd676f2b74c661287ff37df9e947f14259f2eed9519fc43fd4faa8c" +dependencies = [ + "reqwest", + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "xshell" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db0ab86eae739efd1b054a8d3d16041914030ac4e01cd1dca0cf252fd8b6437" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/lib/space-operator-cli/Cargo.toml b/lib/space-operator-cli/Cargo.toml new file mode 100644 index 00000000..1f28f534 --- /dev/null +++ b/lib/space-operator-cli/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "space-operator-cli" +version = "0.6.0" +edition = "2021" +description = "CLI for Space Operator" +license = "AGPL-3.0-only" +repository = "https://github.com/space-operator/flow-backend" +homepage = "https://spaceoperator.com" +readme = "README.md" + +[workspace] + +[[bin]] +path = "src/main.rs" +name = "spo" + +[dependencies] +bon = "2.3.0" +cargo_metadata = "0.18.1" +chrono = { version = "0.4.38", features = ["serde"] } +clap = { version = "=4.5.18", features = ["derive"] } +console = "0.15.8" +directories = "5.0.1" +error-stack = "0.5.0" +futures = "0.3.30" +gix = { version = "0.66.0", default-features = false, features = ["status"] } +postgrest = { package = "spo-postgrest", version = "1.6.0" } +prettyplease = "0.2.22" +proc-macro2 = "1.0.87" +quote = { version = "1.0.37" } +regex = "1.11.0" +reqwest = { version = "0.12.8", features = ["json"] } +semver = "1.0.23" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = { version = "1.0.128", features = ["preserve_order"] } +similar = { version = "2.6.0", features = ["inline"] } +strum = { version = "0.26.3", features = ["derive"] } +syn = "2.0.79" +thiserror = "1.0.64" +tokio = { version = "1.40.0", features = ["macros", "fs"] } +toml = { version = "0.8.19", features = ["preserve_order"] } +url = { version = "2.5.2", features = ["serde"] } +uuid = { version = "1.10.0", features = ["serde"] } +xshell = "0.2.6" + +[dev-dependencies] +clap-markdown = "0.1.4" diff --git a/lib/space-operator-cli/README.md b/lib/space-operator-cli/README.md new file mode 100644 index 00000000..1f3abf63 --- /dev/null +++ b/lib/space-operator-cli/README.md @@ -0,0 +1,436 @@ +# Space Operator CLI + +[![Crates.io][crates-badge]][crates-url] +[![AGPLv3 licensed][AGPLv3-badge]][AGPLv3-url] + +[crates-badge]: https://img.shields.io/crates/v/space-operator-cli.svg +[crates-url]: https://crates.io/crates/space-operator-cli +[AGPLv3-badge]: https://img.shields.io/badge/license-AGPLv3-blue.svg +[AGPLv3-url]: ../../LICENSE + +CLI for [Space Operator](https://spaceoperator.com). + +Table of contents: +- [Install](#install) +- [Login](#login) +- [Run flow-server](#run-flow-server) +- [Generate a new native node](#generate-a-new-native-node) +- [Generate input and output struct](#generate-input-and-output-struct) +- [Upload node](#upload-node) +- [Command-Line Help for spo](#command-line-help-for-spo) + +## Install + +Install using `cargo install`: + +```shell +cargo install space-operator-cli --force +``` + +Binary name: `spo` + +``` +$ spo --help +Usage: spo [OPTIONS] [COMMAND] + +Commands: + login Login to Space Operator using API key + start Start flow-server [aliases: s] + node Manage your nodes [aliases: n] + generate Generate various things [aliases: g] + help Print this message or the help of the given subcommand(s) + +Options: + --url URL of flow-server to use (default: https://dev-api.spaceoperator.com) + -h, --help Print help +``` + +## Login + +Run `spo login`: + +```bash +$ spo login +Go to https://spaceoperator.com/dashboard/profile/apikey go generate a key +Please paste your API key below +``` + +Enter your API key to login. + +## Run flow-server + +Clone [flow-backend](https://github.com/space-operator/flow-backend) repository: + +```bash +git clone https://github.com/space-operator/flow-backend +``` + +`cd` into `flow-backend` and run `spo start`: + +```bash +cd flow-backend +spo start +``` + +This will create a configuration file for flow-server then compile and run it +(first compilation will take several minutes). + +Example output: +``` +generated config.toml +$ cargo build --bin flow-server + Finished `dev` profile [optimized] target(s) in 0.60s +$ target/debug/flow-server config.toml +2024-10-19T14:19:36.514531Z INFO flow_server: native commands: ["add_config_lines", "add_config_lines_core", "add_required_signatory", "add_signatory", "arweave_file_upload", "arweave_nft_upload", "associated_token_account", "attest_from_eth", "attest_token", "burn_cNFT", "burn_v1", "cancel_proposal", "cast_vote", "collect", "complete_native", "complete_proposal", "complete_transfer_wrapped", "const", "create_core_collection_v2", "create_core_v2", "create_governance", "create_master_edition", "create_metadata_account", "create_mint_account", "create_native_treasury", "create_proposal", "create_realm", "create_streamflow_timelock", "create_token_account", "create_token_owner_record", "create_tree", "create_v1", "create_wrapped", "create_wrapped_on_eth", "das_api", "delegate_v1", "deposit_governing_tokens", "execute_transaction", "fetch_assets", "fileexplorer", "finalize_vote", "find_pda", "flow_input", "flow_output", "flow_run_info", "foreach", "gen_metaplex_attrs", "gen_pdg_attrs", "generate_base", "generate_keypair", "get_balance", "get_effect_list", "get_foreign_asset_eth", "get_vaa", "governance_post_message", "http_request", "initialize_candy_guard", "initialize_candy_machine", "initialize_candy_machine_core", "initialize_core_candy_guards", "initialize_record_with_seed", "initialize_token_bridge", "insert_transaction", "interflow", "interflow_instructions", "json_extract", "json_get_field", "json_insert", "kv_create_store", "kv_delete_store", "kv_read_item", "kv_write_item", "kvexplorer", "memo", "mint", "mint_cNFT_to_collection", "mint_candy_machine_core", "mint_compressed_NFT", "mint_token", "mint_v1", "mpl_core_update_plugin", "nft_complete_native", "nft_complete_wrapped", "nft_complete_wrapped_meta", "nft_transfer_native", "nft_transfer_wrapped", "note", "parse_pdg_attrs", "parse_vaa", "pdg_render", "post_message", "post_vaa", "postgrest_builder_eq", "postgrest_builder_insert", "postgrest_builder_is", "postgrest_builder_limit", "postgrest_builder_match", "postgrest_builder_neq", "postgrest_builder_not", "postgrest_builder_order", "postgrest_builder_select", "postgrest_builder_update", "postgrest_builder_upsert", "postgrest_execute_query", "postgrest_new_query", "postgrest_new_rpc", "print", "push_effect_list", "pyth_price", "range", "read_record", "redeem_nft_on_eth", "redeem_on_eth", "refund_proposal_deposit", "relinquish_token_owner_record_locks", "relinquish_vote", "remove_required_signatory", "remove_transaction", "request_airdrop", "revoke_governing_tokens", "set_authority", "set_authority_2022", "set_governance_config", "set_governance_delegate", "set_realm_authority", "set_realm_config", "set_token_owner_record_locks", "sign_off_proposal", "storage_create_signed_url", "storage_delete", "storage_download", "storage_get_file_metadata", "storage_get_public_url", "storage_list", "storage_upload", "supabase", "to_bytes", "to_string", "to_vec", "transfer_cNFT", "transfer_from_eth", "transfer_native", "transfer_nft_from_eth", "transfer_sol", "transfer_token", "transfer_wrapped", "update_cNFT", "update_core_v1", "update_render_params", "update_v1", "verify_collection_v1", "verify_creator_v1", "verify_signatures", "wait", "wallet", "withdraw_governing_tokens", "withdraw_streamflow_timelock", "wrap", "wrap_core", "write_to_record"] +2024-10-19T14:19:36.514586Z INFO flow_server: allow CORS origins: ["*"] +2024-10-19T14:19:36.528967Z INFO db::local_storage: openning sled storage: _data/guest_local_storage +2024-10-19T14:19:37.728815Z WARN flow_server: missing credentials, some routes are not available: need database credentials +2024-10-19T14:19:37.728846Z INFO flow_server: listening on "0.0.0.0" port 8080 +2024-10-19T14:19:37.729177Z INFO actix_server::builder: starting 8 workers +2024-10-19T14:19:37.729198Z INFO actix_server::server: Actix runtime found; starting in Actix runtime +2024-10-19T14:19:37.729207Z INFO actix_server::server: starting service: "actix-web-service-0.0.0.0:8080", workers: 8, listening on: 0.0.0.0:8080 +2024-10-19T14:19:37.759200Z DEBUG flow_server::db_worker::token_worker: started TokenWorker c334e245-75b4-49fd-93c0-c4b25ab74f70 +2024-10-19T14:19:37.759314Z INFO flow_server::db_worker: started DBWorker +``` + +## Generate a new native node + +Make sure you are inside [flow-backend](https://github.com/space-operator/flow-backend) repository. + +Generate with `spo node new`: +``` +$ spo node new +could not determine which package to update +use `-p` option to specify a package +available packages: + client + flow-lib + spo-helius + flow-value + space-lib + cmds-deno + command-rpc + srpc + cmds-pdg + pdg-common + cmds-solana + cmds-std + db + flow + rhai-script + space-wasm + utils + flow-server +``` + +Because our workspace have several packages, you must specify one of them to use +(our CLI can automatically choose one if you are inside one of them). + +``` +$ spo node new -p cmds-solana +``` + +Fill the prompts for node definition, for example: + +``` +$ spo node new -p cmds-solana +using package: cmds-solana +enter ? for help +? module path: ? +enter valid Rust module path to save the node (empty to save at root) +? module path: +? node id: transfer +? display name: Transfer +description: + +adding node inputs (enter empty name to finish) +? name: fee_payer +? input type: keypair +? optional (true/false): false +? passthrough (true/false): true + +adding node inputs (enter empty name to finish) +? name: amount +? input type: decimal +? optional (true/false): false +? passthrough (true/false): false + +adding node inputs (enter empty name to finish) +? name: + +adding node outputs (enter empty name to finish) +? name: balance +? output type: decimal +? optional (true/false): true + +adding node outputs (enter empty name to finish) +? name: +will this node emit Solana instructions? (y/n): y +adding `signature` output +adding `submit` input +adding instruction info: { + "before": [ + "balance", + "fee_payer" + ], + "signature": "signature", + "after": [] +} +writing node definition to crates/cmds-solana/node-definitions/transfer.json +writing code to crates/cmds-solana/src/transfer.rs +updating module crates/cmds-solana/src/lib.rs +upload node (y/n): y +node: transfer +command is not in database +upload? (y/n): y +inserted new node, id=1256 +view your node: +https://spaceoperator.com/dashboard/nodes/c334e245-75b4-49fd-93c0-c4b25ab74f70.transfer.0.1 +``` + +Generated Rust code: +```rust +use flow_lib::command::prelude::*; +const NAME: &str = "transfer"; +flow_lib::submit!(CommandDescription::new(NAME, | _ | build())); +fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!("/transfer.json"); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) +} +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +pub struct Input { + pub fee_payer: Wallet, + #[serde_as(as = "AsDecimal")] + pub amount: Decimal, + #[serde(default = "value::default::bool_true")] + pub submit: bool, +} +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +pub struct Output { + #[serde_as(as = "AsDecimal")] + pub balance: Option, + #[serde_as(as = "Option")] + pub signature: Option, +} +async fn run(mut ctx: Context, input: Input) -> Result { + tracing::info!("input: {:?}", input); + let signature = ctx + .execute(Instructions::default(), value::map! {}) + .await? + .signature; + Err(CommandError::msg("unimplemented")) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_build() { + build().unwrap(); + } + #[tokio::test] + async fn test_run() { + let ctx = Context::default(); + build().unwrap().run(ctx, ValueSet::new()).await.unwrap_err(); + } +} +``` + +Then, you can use `spo start` to run a local flow-server and test your node in flow editor. + +## Generate input and output struct + +If you updated the node definition manually, you can use `spo generate input` and +`spo generate output` to generate new type definitions. + +For example + +```shell +spo generate input crates/cmds-solana/node-definitions/nft/v1/mint_v1.json +``` + +```rust +#[serde_as] +#[derive(Deserialize, Serialize, Debug)] +struct Input { + fee_payer: Wallet, + authority: Option, + #[serde_as(as = "AsPubkey")] + mint_account: Pubkey, + #[serde_as(as = "AsPubkey")] + token_owner: Pubkey, + amount: Option, + #[serde_as(as = "Option")] + delegate_record: Option, + #[serde_as(as = "Option")] + authorization_rules_program: Option, + #[serde_as(as = "Option")] + authorization_rules: Option, + authorization_data: Option, + #[serde(default = "value::default::bool_true")] + submit: bool, +} +``` + +## Upload node + +Use `spo node upload` to upload new node definition or update existing one. +We only support `native` node at the moment. + +# Command-Line Help for `spo` + +This document contains the help content for the `spo` command-line program. + +**Command Overview:** + +* [`spo`↴](#spo) +* [`spo login`↴](#spo-login) +* [`spo start`↴](#spo-start) +* [`spo node`↴](#spo-node) +* [`spo node new`↴](#spo-node-new) +* [`spo node upload`↴](#spo-node-upload) +* [`spo generate`↴](#spo-generate) +* [`spo generate input`↴](#spo-generate-input) +* [`spo generate output`↴](#spo-generate-output) +* [`spo generate config`↴](#spo-generate-config) + +## `spo` + +**Usage:** `spo [OPTIONS] [COMMAND]` + +###### **Subcommands:** + +* `login` — Login to Space Operator using API key +* `start` — Start flow-server +* `node` — Manage your nodes +* `generate` — Generate various things + +###### **Options:** + +* `--url ` — URL of flow-server to use (default: https://dev-api.spaceoperator.com) + + + +## `spo login` + +Login to Space Operator using API key + +**Usage:** `spo login` + + + +## `spo start` + +Start flow-server + +**Usage:** `spo start [CONFIG]` + +###### **Arguments:** + +* `` — Path to configuration file + + + +## `spo node` + +Manage your nodes + +**Usage:** `spo node ` + +###### **Subcommands:** + +* `new` — Generate a new node +* `upload` — Upload nodes + + + +## `spo node new` + +Generate a new node + +**Usage:** `spo node new [OPTIONS]` + +###### **Options:** + +* `--allow-dirty` — Allow dirty git repository +* `-p`, `--package ` — Specify which Rust package to add the new node to + + + +## `spo node upload` + +Upload nodes + +**Usage:** `spo node upload [OPTIONS] ` + +###### **Arguments:** + +* `` — Path to JSON node definition file + +###### **Options:** + +* `--dry-run` — Only print diff, don't do anything +* `--no-confirm` — Don't ask for confirmation + + + +## `spo generate` + +Generate various things + +**Usage:** `spo generate ` + +###### **Subcommands:** + +* `input` — Generate input struct +* `output` — Generate output struct +* `config` — Generate configuration file for flow-server + + + +## `spo generate input` + +Generate input struct + +**Usage:** `spo generate input ` + +###### **Arguments:** + +* `` — Path to node definition file + + + +## `spo generate output` + +Generate output struct + +**Usage:** `spo generate output ` + +###### **Arguments:** + +* `` — Path to node definition file + + + +## `spo generate config` + +Generate configuration file for flow-server + +**Usage:** `spo generate config [PATH]` + +###### **Arguments:** + +* `` — Path to save configuration file (default: config.toml) + + + +
+ + + This document was generated automatically by +
clap-markdown. + diff --git a/lib/space-operator-cli/src/main.rs b/lib/space-operator-cli/src/main.rs new file mode 100644 index 00000000..176d30ea --- /dev/null +++ b/lib/space-operator-cli/src/main.rs @@ -0,0 +1,1880 @@ +#![allow(clippy::print_stdout, clippy::print_stderr)] + +use cargo_metadata::{Metadata, Package, Target}; +use chrono::Utc; +use clap::{ColorChoice, CommandFactory, Parser, Subcommand, ValueEnum}; +use console::style; +use directories::ProjectDirs; +use error_stack::{Report, ResultExt}; +use futures::{io::AllowStdIo, AsyncBufReadExt, AsyncReadExt}; +use postgrest::Postgrest; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use regex::Regex; +use reqwest::{ + header::{HeaderName, HeaderValue, AUTHORIZATION}, + StatusCode, +}; +use schema::{CommandDefinition, CommandId, ValueType}; +use semver::Version; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{ + borrow::{Borrow, Cow}, + cmp::Ordering, + fmt::Display, + io::{BufReader, Stdin, Write}, + path::{Path, PathBuf}, + sync::LazyLock, +}; +use strum::IntoEnumIterator; +use thiserror::Error as ThisError; +use tokio::task::spawn_blocking; +use url::Url; +use uuid::Uuid; +use xshell::{cmd, Shell}; + +static CLIENT: LazyLock = LazyLock::new(|| reqwest::Client::new()); + +pub mod schema; + +pub mod claim_token { + use chrono::{DateTime, Utc}; + use uuid::Uuid; + + use super::*; + + #[derive(Deserialize, Serialize, Debug)] + pub struct Output { + pub user_id: Uuid, + pub access_token: String, + pub refresh_token: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub expires_at: DateTime, + } + + pub async fn claim_token( + http: &reqwest::Client, + flow_server: &Url, + apikey: &str, + ) -> Result> { + let apikey = HeaderValue::from_str(apikey).change_context(Error::InvalidApiKey)?; + let resp = http + .post( + flow_server + .join("/auth/claim_token") + .change_context(Error::Url)?, + ) + .header(HeaderName::from_static("x-api-key"), apikey) + .send() + .await + .change_context(Error::Http)?; + read_json_response::<_, FlowServerErrorBody>(resp).await + } +} + +pub mod get_info { + use url::Url; + + use super::*; + + #[derive(Deserialize, Serialize, Debug)] + pub struct Output { + pub supabase_url: Url, + pub anon_key: String, + } + + pub async fn get_info( + http: &reqwest::Client, + flow_server: &Url, + access_token: &str, + ) -> Result> { + let resp = http + .get(flow_server.join("/info").change_context(Error::Url)?) + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await + .change_context(Error::Http)?; + read_json_response::<_, FlowServerErrorBody>(resp).await + } +} + +async fn refresh( + http: &reqwest::Client, + info: &get_info::Output, + refresh_token: &str, + user_id: &Uuid, +) -> Result> { + #[derive(Serialize)] + struct Body<'a> { + refresh_token: &'a str, + } + + #[derive(Deserialize)] + struct Resp { + access_token: String, + expires_in: u32, + refresh_token: String, + } + + let resp = http + .post( + info.supabase_url + .join("/auth/v1/token?grant_type=refresh_token") + .change_context(Error::Url)?, + ) + .header(HeaderName::from_static("apikey"), &info.anon_key) + .json(&Body { refresh_token }) + .send() + .await + .change_context(Error::Http)?; + + let resp = read_json_response::(resp).await?; + + Ok(claim_token::Output { + user_id: *user_id, + access_token: resp.access_token, + refresh_token: resp.refresh_token, + expires_at: Utc::now() + chrono::Duration::seconds(resp.expires_in as i64), + }) +} + +fn get_color() -> ColorChoice { + std::env::var("COLOR") + .ok() + .and_then(|var| ColorChoice::from_str(&var, true).ok()) + .unwrap_or_default() +} + +#[derive(Parser, Debug)] +#[command(name = "spo")] +#[command(color = get_color())] +struct Args { + /// URL of flow-server to use (default: https://dev-api.spaceoperator.com) + #[arg(long)] + url: Option, + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Login to Space Operator using API key + Login {}, + /// Start flow-server + #[command(visible_alias = "s")] + Start { + /// Path to configuration file + config: Option, + /// Connect to local Docker instance + #[arg(long)] + docker: bool, + }, + /// Manage your nodes + #[command(visible_alias = "n")] + Node { + #[command(subcommand)] + command: NodeCommands, + }, + /// Generate various things + #[command(visible_alias = "g")] + Generate { + #[command(subcommand)] + command: GenerateCommands, + }, +} + +#[derive(Subcommand, Debug)] +enum GenerateCommands { + /// Generate input struct + #[command(visible_alias = "i")] + Input { + /// Path to node definition file + path: PathBuf, + }, + /// Generate output struct + #[command(visible_alias = "o")] + Output { + /// Path to node definition file + path: PathBuf, + }, + /// Generate configuration file for flow-server + #[command(visible_alias = "c")] + Config { + /// Path to save configuration file (default: config.toml) + path: Option, + }, +} + +#[derive(Subcommand, Debug)] +enum NodeCommands { + /// Generate a new node + #[command(visible_alias = "n")] + New { + /// Allow dirty git repository + #[arg(long)] + allow_dirty: bool, + /// Specify which Rust package to add the new node to + #[arg(long, short)] + package: Option, + }, + /// Upload nodes + #[command(visible_alias = "u")] + Upload { + /// Path to JSON node definition file + path: PathBuf, + /// Only print diff, don't do anything + #[arg(long)] + dry_run: bool, + /// Don't ask for confirmation + #[arg(long)] + no_confirm: bool, + }, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub flow_server: Url, + pub info: get_info::Output, + pub apikey: String, + pub jwt: claim_token::Output, +} + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("please start docker-compose first")] + DockerComposeNotRunning, + #[error("response from server: {}", .0)] + ErrorResponse(String), + #[error("{}: {}", .0, .1)] + UnknownResponse(StatusCode, String), + #[error("invalid API key")] + InvalidApiKey, + #[error("HTTP error")] + Http, + #[error("invalid semver string")] + Version, + #[error("no shell available")] + Shell, + #[error("an error occurred")] + Subprocess, + #[error("thread join error")] + Thread, + #[error("URL error")] + Url, + #[error("DB error")] + Postgrest, + #[error("could not find location to save application data")] + Dir, + #[error("failed to serialize application data")] + SerializeData, + #[error("failed to parse application data")] + ParseData, + #[error("failed to write application data {}", .0.display())] + WriteData(PathBuf), + #[error("failed to read application data {}", .0.display())] + ReadData(PathBuf), + #[error("failed to read file {}", .0.display())] + ReadFile(PathBuf), + #[error("failed to write file {}", .0.display())] + WriteFile(PathBuf), + #[error("failed to parse node definition")] + ParseNodeDefinition, + #[error("{}", .0)] + Unimplemented(&'static str), + #[error("you are not logged in")] + NotLogin, + #[error("JSON error")] + Json, + #[error("token refresh error")] + TokenRefresh, + #[error("git error: {}", .0)] + Gix(&'static str), + #[error("IO error: {}", .0)] + Io(&'static str), + #[error("failed to get cargo metadata")] + Metadata, + #[error("package not found: {}", .0)] + PackageNotFound(String), + #[error("package is not a library: {}", .0)] + NotLib(String), + #[error("invalid value")] + InvalidValue, +} + +#[derive(Deserialize, ThisError, Debug)] +#[error("{error}")] +pub struct FlowServerErrorBody { + pub error: String, +} + +#[derive(Deserialize, ThisError, Debug)] +#[error("{msg}")] +pub struct GoTrueErrorBody { + pub msg: String, +} + +#[derive(Deserialize, ThisError, Debug)] +#[serde(untagged)] +pub enum PostgrestErrorBody { + #[error("{error}")] + Error { error: String }, + #[error("{message}")] + Postgrest { + message: String, + details: Option, + hint: Option, + }, +} + +async fn read_json_response( + resp: reqwest::Response, +) -> Result> { + let code = resp.status(); + let bytes = resp.bytes().await.change_context(Error::Http)?; + if code.is_success() { + match serde_json::from_slice::(&bytes) { + Ok(body) => Ok(body), + Err(error) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + Err(Report::new(error).change_context(Error::UnknownResponse(code, text))) + } + } + } else { + match serde_json::from_slice::(&bytes) { + Ok(body) => Err(Error::ErrorResponse(body.to_string()).into()), + Err(_) => { + let text = String::from_utf8_lossy(&bytes).into_owned(); + Err(Error::UnknownResponse(code, text).into()) + } + } + } +} + +async fn read_file(path: impl AsRef) -> Result> { + let path = path.as_ref(); + if path == Path::new("-") { + let mut stdin = stdin(); + let mut result = String::new(); + stdin + .read_to_string(&mut result) + .await + .change_context(Error::Io("read stdin"))?; + Ok(result) + } else { + tokio::fs::read_to_string(path) + .await + .change_context_lazy(|| Error::ReadFile(path.to_owned())) + } +} + +async fn write_file(path: impl AsRef, data: impl AsRef<[u8]>) -> Result> { + let path = path.as_ref(); + if path == Path::new("-") { + let mut stdout = std::io::stdout(); + stdout + .write_all(data.as_ref()) + .change_context(Error::Io("write stdout"))?; + stdout.flush().change_context(Error::Io("write stdout"))?; + Ok(false) + } else { + tokio::fs::write(path, data) + .await + .change_context_lazy(|| Error::WriteFile(path.to_owned()))?; + Ok(true) + } +} + +pub struct ApiClient { + pg: postgrest::Postgrest, + config: Config, +} + +impl ApiClient { + pub fn from_config(config: Config) -> Result> { + let pg = Postgrest::new_with_client( + config + .info + .supabase_url + .join("/rest/v1") + .change_context(Error::Url)?, + CLIENT.clone(), + ) + .insert_header(HeaderName::from_static("apikey"), &config.info.anon_key); + Ok(Self { pg, config }) + } + + pub async fn load() -> Result> { + let path = Self::data_file_full_path()?; + let text = tokio::fs::read_to_string(&path) + .await + .change_context_lazy(|| Error::ReadData(path.clone()))?; + let config: Config = toml::from_str(&text).change_context(Error::ParseData)?; + Self::from_config(config) + } + + pub async fn new(flow_server: Url, apikey: String) -> Result> { + let token = claim_token::claim_token(&CLIENT, &flow_server, &apikey).await?; + let info = get_info::get_info(&CLIENT, &flow_server, &token.access_token).await?; + let config = Config { + flow_server, + info, + apikey, + jwt: token, + }; + Self::from_config(config) + } + + async fn get_access_token(&mut self) -> Result> { + let now = chrono::Utc::now(); + if now >= self.config.jwt.expires_at + chrono::Duration::minutes(1) { + self.config.jwt = refresh( + &CLIENT, + &self.config.info, + &self.config.jwt.refresh_token, + &self.config.jwt.user_id, + ) + .await + .change_context(Error::TokenRefresh)?; + } + Ok(self.config.jwt.access_token.clone()) + } + + pub async fn update_node( + &mut self, + id: CommandId, + def: &CommandDefinition, + ) -> Result<(CommandId, String), Report> { + #[derive(Serialize)] + struct UpdateNode<'a> { + #[serde(flatten)] + def: &'a CommandDefinition, + unique_node_id: &'a str, + name: &'a str, + } + + let unique_node_id = format!( + "{}.{}.{}", + self.config.jwt.user_id, def.data.node_id, def.data.version + ); + let body = serde_json::to_string_pretty(&UpdateNode { + def, + unique_node_id: &unique_node_id, + name: &def.data.node_id, + }) + .change_context(Error::Json)?; + + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .eq("id", id.to_string()) + .update(body) + .select("id") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Resp { + id: CommandId, + } + + let resp = read_json_response::(resp).await?; + + Ok((resp.id, unique_node_id)) + } + + pub async fn insert_node( + &mut self, + def: &CommandDefinition, + ) -> Result<(CommandId, String), Report> { + #[derive(Serialize)] + struct InsertNode<'a> { + #[serde(flatten)] + def: &'a CommandDefinition, + #[serde(rename = "isPublic")] + is_public: bool, + unique_node_id: &'a str, + name: &'a str, + } + + let unique_node_id = format!( + "{}.{}.{}", + self.config.jwt.user_id, def.data.node_id, def.data.version + ); + let body = serde_json::to_string(&InsertNode { + def, + is_public: false, + unique_node_id: &unique_node_id, + name: &def.data.node_id, + }) + .change_context(Error::Json)?; + + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .insert(body) + .select("id") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Resp { + id: CommandId, + } + + let resp = read_json_response::(resp).await?; + + Ok((resp.id, unique_node_id)) + } + + pub async fn get_my_native_node( + &mut self, + node_id: &str, + ) -> Result, Report> { + #[derive(Serialize)] + struct Query<'a> { + node_id: &'a str, + } + let resp = self + .pg + .from("nodes") + .auth(self.get_access_token().await?) + .eq("user_id", self.config.jwt.user_id.to_string()) + .eq("type", "native") + .cs( + "data", + serde_json::to_string(&Query { node_id }).change_context(Error::Json)?, + ) + .select("*") + .execute() + .await + .change_context(Error::Postgrest)?; + let mut nodes = + read_json_response::, PostgrestErrorBody>(resp).await?; + error_stack::ensure!( + nodes.len() <= 1, + Error::ErrorResponse("more than 1 native nodes".to_owned()) + ); + + match nodes.pop() { + Some(json) => { + #[derive(Deserialize)] + struct Row { + id: CommandId, + #[serde(flatten)] + def: CommandDefinition, + } + + let row = serde_json::from_value::(json).change_context(Error::Json)?; + Ok(Some((row.id, row.def))) + } + None => Ok(None), + } + } + + pub fn data_dir() -> Result> { + Ok(project_dirs()?.data_dir().to_owned()) + } + + pub const fn data_file_name() -> &'static str { + "data.toml" + } + + pub fn data_file_full_path() -> Result> { + Ok(Self::data_dir()?.join(Self::data_file_name()).to_owned()) + } + + pub async fn save_application_data(&self) -> Result<(), Report> { + let base = Self::data_dir()?; + + tokio::fs::create_dir_all(&base) + .await + .change_context_lazy(|| Error::WriteData(base.clone()))?; + + let path = base.join(Self::data_file_name()); + + let data = toml::to_string_pretty(&self.config).change_context(Error::SerializeData)?; + tokio::fs::write(&path, data) + .await + .change_context_lazy(|| Error::WriteData(path.clone()))?; + Ok(()) + } + + pub async fn get_username(&mut self) -> Result, Report> { + let resp = self + .pg + .from("users_public") + .auth(self.get_access_token().await?) + .eq("user_id", self.config.jwt.user_id.to_string()) + .select("username") + .single() + .execute() + .await + .change_context(Error::Postgrest)?; + + #[derive(Deserialize)] + struct Body { + username: Option, + } + + read_json_response::(resp) + .await + .map(|body| body.username) + } +} + +fn project_dirs() -> Result> { + Ok(ProjectDirs::from("com", "spaceoperator", "spo").ok_or(Error::Dir)?) +} + +async fn ask(q: &str) -> bool { + print!("{} (y/n): ", style(q).bold()); + std::io::stdout().flush().ok(); + + let mut stdin = AllowStdIo::new(BufReader::new(stdin())); + let mut answer = String::new(); + stdin.read_line(&mut answer).await.ok(); + + answer.trim().to_lowercase() == "y" +} + +struct Line(Option); + +impl std::fmt::Display for Line { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self.0 { + None => write!(f, " "), + Some(idx) => write!(f, "{:<4}", idx + 1), + } + } +} + +fn print_diff(local: &T, db: &T) -> bool { + use console::{style, Style}; + use similar::{ChangeTag, TextDiff}; + + let local_json = serde_json::to_string_pretty(local).unwrap(); + let db_json = serde_json::to_string_pretty(db).unwrap(); + + let diff = TextDiff::from_lines(db_json.as_str(), local_json.as_str()); + + if diff + .iter_all_changes() + .filter(|c| c.tag() != ChangeTag::Equal) + .count() + == 0 + { + println!("No differences"); + return false; + } + + for (idx, group) in diff.grouped_ops(3).iter().enumerate() { + if idx > 0 { + println!("{:-^1$}", "-", 80); + } + for op in group { + for change in diff.iter_inline_changes(op) { + let (sign, s) = match change.tag() { + ChangeTag::Delete => ("-", Style::new().red()), + ChangeTag::Insert => ("+", Style::new().green()), + ChangeTag::Equal => (" ", Style::new().dim()), + }; + print!( + "{}{} |{}", + style(Line(change.old_index())).dim(), + style(Line(change.new_index())).dim(), + s.apply_to(sign).bold(), + ); + for (emphasized, value) in change.iter_strings_lossy() { + if emphasized { + print!("{}", s.apply_to(value).underlined().on_black()); + } else { + print!("{}", s.apply_to(value)); + } + } + if change.missing_newline() { + println!(); + } + } + } + } + + true +} + +fn is_dirty() -> Result> { + let repo = + gix::ThreadSafeRepository::discover(".").change_context(Error::Gix("open repository"))?; + Ok(repo + .to_thread_local() + .is_dirty() + .change_context(Error::Gix("get status"))?) +} + +fn cargo_metadata() -> Result> { + cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .change_context(Error::Metadata) +} + +fn find_target_crate_by_name<'a>( + meta: &'a Metadata, + name: &str, +) -> Result<&'a Package, Report> { + let members = meta.workspace_packages(); + Ok(members + .into_iter() + .find(|p| p.name == name) + .ok_or_else(|| Error::PackageNotFound(name.to_owned()))?) +} + +fn find_target_crate<'a>(meta: &'a Metadata) -> Result, Report> { + let members = meta.workspace_packages(); + let pwd = std::env::current_dir().change_context(Error::Io("get current dir"))?; + let member = members.into_iter().find(|p| { + p.targets.iter().any(|t| t.is_lib()) + && p.manifest_path + .parent() + .map(|root| pwd.starts_with(root)) + .unwrap_or(false) + }); + Ok(member) +} + +async fn prompt_node_definition() -> Result> { + let mut stdin = stdin(); + + let name_regex = Regex::new(r#"^[[:alpha:]][[:word:]]*$"#).unwrap(); + let name_hint = "value can only contains characters [a-zA-Z0-9_] and must start with [a-zA-Z]"; + + let node_id = Prompt::builder() + .question("node id: ") + .check_regex(&name_regex) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + + let display_name = Prompt::builder() + .question("display name: ") + .check_regex(&Regex::new(r#"\S+"#).unwrap()) + .regex_hint("value cannot be empty") + .build() + .prompt(&mut stdin) + .await?; + + let description = Prompt::builder() + .question("description: ") + .build() + .prompt(&mut stdin) + .await?; + + let mut inputs = Vec::::new(); + loop { + println!("\nadding node inputs (enter empty name to finish)"); + + let name = Prompt::builder() + .question("name: ") + .check_regex(&name_regex) + .allow_empty(true) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + if name.is_empty() { + break; + } + + let types = schema::ValueType::iter() + .map(|t| t.into()) + .collect::>(); + let type_bound_str = Prompt::builder() + .question("input type: ") + .check_list(&types) + .build() + .prompt(&mut stdin) + .await?; + let type_bound: schema::ValueType = type_bound_str.parse().unwrap(); + + let optional = Prompt::builder() + .question("optional (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let optional: bool = optional.parse().unwrap(); + + let default_value = if optional && type_bound == ValueType::Bool { + let default = Prompt::builder() + .question("default value (empty/true/false): ") + .check_list(&["true", "false"]) + .allow_empty(true) + .build() + .prompt(&mut stdin) + .await?; + default + .parse::() + .ok() + .map(serde_json::Value::Bool) + .unwrap_or(serde_json::Value::Null) + } else { + serde_json::Value::Null + }; + + let passthrough = Prompt::builder() + .question("passthrough (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let passthrough: bool = passthrough.parse().unwrap(); + + inputs.push(schema::Target { + name, + type_bounds: [type_bound_str].into(), + required: !optional, + default_value, + passthrough, + tooltip: String::new(), + }); + } + + let mut outputs = Vec::::new(); + loop { + println!("\nadding node outputs (enter empty name to finish)"); + + let name = Prompt::builder() + .question("name: ") + .check_regex(&name_regex) + .allow_empty(true) + .regex_hint(name_hint) + .build() + .prompt(&mut stdin) + .await?; + if name.is_empty() { + break; + } + + let types = schema::ValueType::iter() + .map(|t| t.into()) + .collect::>(); + let type_bound_str = Prompt::builder() + .question("output type: ") + .check_list(&types) + .build() + .prompt(&mut stdin) + .await?; + let _: schema::ValueType = type_bound_str.parse().unwrap(); + + let optional = Prompt::builder() + .question("optional (true/false): ") + .check_list(&["true", "false"]) + .build() + .prompt(&mut stdin) + .await?; + let optional: bool = optional.parse().unwrap(); + + outputs.push(schema::Source { + name, + r#type: type_bound_str, + tooltip: String::new(), + optional, + default_value: serde_json::Value::Null, + }); + } + + let ins = ask("will this node emit Solana instructions?").await; + let info = if ins { + if !outputs.iter().any(|o| o.name == "signature") { + println!("adding `signature` output"); + outputs.push(schema::Source { + name: "signature".to_owned(), + r#type: "signature".to_owned(), + default_value: serde_json::Value::Null, + tooltip: String::new(), + optional: true, + }); + } + if !inputs.iter().any(|o| o.name == "submit") { + println!("adding `submit` input"); + inputs.push(schema::Target { + name: "submit".to_owned(), + type_bounds: ["bool".to_owned()].into(), + default_value: serde_json::Value::Bool(true), + tooltip: String::new(), + required: false, + passthrough: false, + }); + } + + let info = schema::InstructionInfo { + before: outputs + .iter() + .map(|o| o.name.clone()) + .filter(|name| name != "signature") + .chain( + inputs + .iter() + .filter(|i| i.passthrough) + .map(|i| i.name.clone()), + ) + .collect(), + signature: "signature".to_owned(), + after: Vec::new(), + }; + println!( + "adding instruction info: {}", + serde_json::to_string_pretty(&info).unwrap() + ); + Some(info) + } else { + None + }; + + let def = schema::CommandDefinition { + r#type: "native".to_owned(), + data: schema::Data { + node_definition_version: Some("0.1".to_owned()), + unique_id: Some(String::new()), + node_id, + version: "0.1".to_owned(), + display_name, + description, + tags: Some(Vec::new()), + related_to: Some( + [schema::RelatedTo { + id: String::new(), + r#type: String::new(), + relationship: String::new(), + }] + .into(), + ), + resources: Some(schema::Resources { + source_code_url: String::new(), + documentation_url: String::new(), + }), + usage: Some(schema::Usage { + license: "Apache-2.0".to_owned(), + license_url: String::new(), + pricing: schema::Pricing { + currency: "USDC".to_owned(), + purchase_price: 0, + price_per_run: 0, + custom: Some(schema::CustomPricing { + unit: "monthly".to_owned(), + value: "0".to_owned(), + }), + }, + }), + authors: Some( + [schema::Author { + name: "Space Operator".to_owned(), + contact: String::new(), + }] + .into(), + ), + instruction_info: info, + options: None, + design: None, + }, + sources: outputs, + targets: inputs, + ui_schema: serde_json::Value::Object(<_>::default()), + json_schema: serde_json::Value::Object(<_>::default()), + }; + + Ok(def) +} + +fn relative_to_pwd>(path: P) -> PathBuf { + let path = path.as_ref(); + if path.is_relative() { + return path.to_owned(); + } + let mut pwd = std::env::current_dir().unwrap_or_default(); + let mut result = PathBuf::new(); + loop { + match path.strip_prefix(&pwd) { + Ok(suffix) => { + result.push(suffix); + break; + } + Err(_) => { + pwd.pop(); + result.push(".."); + } + } + } + result +} + +async fn write_node_definition( + def: &CommandDefinition, + package: &Package, + modules: &[&str], +) -> Result, Report> { + let root = package + .manifest_path + .parent() + .ok_or_else(|| Report::new(Error::Io("find package path")))? + .as_std_path(); + + let mut path = root.join("node-definitions"); + path.extend(modules); + tokio::fs::create_dir_all(&path) + .await + .change_context(Error::Io("create dir"))?; + path.push(&format!("{}.json", def.data.node_id)); + let path = relative_to_pwd(path); + + println!("writing node definition to {}", path.display()); + if path.is_file() { + if !ask("file already exists, overwrite?").await { + return Ok(None); + } + } + let content = serde_json::to_string_pretty(def).change_context(Error::Json)?; + write_file(&path, content).await?; + Ok(Some(path)) +} + +fn value_type_to_rust_type(ty: schema::ValueType) -> TokenStream { + match ty { + schema::ValueType::Bool => quote! { bool }, + schema::ValueType::U8 => quote! { u8 }, + schema::ValueType::U16 => quote! { u16 }, + schema::ValueType::U32 => quote! { u32 }, + schema::ValueType::U64 => quote! { u64}, + schema::ValueType::U128 => quote! { u128 }, + schema::ValueType::I8 => quote! { i8 }, + schema::ValueType::I16 => quote! { i16}, + schema::ValueType::I32 => quote! { i32 }, + schema::ValueType::I64 => quote! { i64 }, + schema::ValueType::I128 => quote! { i128 }, + schema::ValueType::F32 => quote! { f32 }, + schema::ValueType::F64 => quote! { f64 }, + schema::ValueType::Decimal => quote! { Decimal }, + schema::ValueType::Pubkey => quote! { Pubkey }, + schema::ValueType::Address => quote! { String }, + schema::ValueType::Keypair => quote! { Wallet }, + schema::ValueType::Signature => quote! { Signature }, + schema::ValueType::String => quote! { String }, + schema::ValueType::Bytes => quote! { Bytes }, + schema::ValueType::Array => quote! { Vec }, + schema::ValueType::Map => quote! { ValueSet }, + schema::ValueType::Json => quote! { JsonValue }, + schema::ValueType::Free => quote! { Value }, + } +} + +fn rust_type( + bounds: &[String], + optional: bool, + default_value: &serde_json::Value, +) -> proc_macro2::TokenStream { + let ty = bounds + .get(0) + .and_then(|ty| ty.parse::().ok()) + .unwrap_or(schema::ValueType::Free); + let use_option = + optional && !(ty == ValueType::Bool && matches!(default_value, serde_json::Value::Bool(_))); + let ty = value_type_to_rust_type(ty); + let ty = if use_option { + quote! { Option<#ty> } + } else { + ty + }; + ty +} + +fn rust_type_serde_decor( + bounds: &[String], + optional: bool, + default_value: &serde_json::Value, +) -> proc_macro2::TokenStream { + let ty = bounds + .get(0) + .and_then(|ty| ty.parse::().ok()) + .unwrap_or(schema::ValueType::Free); + match ty { + ValueType::Bool => { + if optional && default_value.as_bool().is_some() { + let default = default_value.as_bool().unwrap(); + let path = if default { + "value::default::bool_true" + } else { + "value::default::bool_false" + }; + return quote! { #[serde(default = #path)]}; + } + } + ValueType::U8 => {} + ValueType::U16 => {} + ValueType::U32 => {} + ValueType::U64 => {} + ValueType::U128 => {} + ValueType::I8 => {} + ValueType::I16 => {} + ValueType::I32 => {} + ValueType::I64 => {} + ValueType::I128 => {} + ValueType::F32 => {} + ValueType::F64 => {} + ValueType::Decimal => { + return if optional { + quote! { + #[serde_as(as = "Option")] + } + } else { + quote! { + #[serde_as(as = "AsDecimal")] + } + }; + } + ValueType::Pubkey => { + return if optional { + quote! { + #[serde_as(as = "Option")] + } + } else { + quote! { + #[serde_as(as = "AsPubkey")] + } + }; + } + ValueType::Address => {} + ValueType::Keypair => {} + ValueType::Signature => { + return if optional { + quote! { + #[serde_as(as = "Option")] + } + } else { + quote! { + #[serde_as(as = "AsSignature")] + } + }; + } + ValueType::String => {} + ValueType::Bytes => {} + ValueType::Array => {} + ValueType::Map => {} + ValueType::Json => {} + ValueType::Free => {} + } + + quote! {} +} + +fn make_input_struct( + targets: impl IntoIterator>, +) -> TokenStream { + let inputs = targets.into_iter().map(|t| { + let t = t.borrow(); + let name = format_ident!("{}", t.name); + let ty = rust_type(&t.type_bounds, !t.required, &t.default_value); + let serde_decor = rust_type_serde_decor(&t.type_bounds, !t.required, &t.default_value); + quote! { + #serde_decor + #name: #ty + } + }); + quote! { + #[serde_as] + #[derive(Deserialize, Serialize, Debug)] + pub struct Input { + #(#inputs),* + } + } +} + +fn make_output_struct( + sources: impl IntoIterator>, +) -> TokenStream { + let outputs = sources.into_iter().map(|t| { + let t = t.borrow(); + let name = format_ident!("{}", t.name); + let ty = rust_type(&[t.r#type.clone()], t.optional, &t.default_value); + let serde_decor = rust_type_serde_decor(&[t.r#type.clone()], t.optional, &t.default_value); + quote! { + #serde_decor + pub #name: #ty + } + }); + quote! { + #[serde_as] + #[derive(Deserialize, Serialize, Debug)] + pub struct Output { + #(#outputs),* + } + } +} + +fn fmt_code(code: TokenStream) -> String { + syn::parse2::(code.clone()) + .map(|file| prettyplease::unparse(&file)) + .unwrap_or_else(|error| { + eprintln!("invalid code: {}", error); + code.to_string() + }) +} + +fn code_template(def: &CommandDefinition, modules: &[&str]) -> String { + let node_id = &def.data.node_id; + let node_definition_path = modules.join("/") + "/" + node_id + ".json"; + let input_struct = make_input_struct(&def.targets); + let output_struct = make_output_struct(&def.sources); + let execute = if def.data.instruction_info.is_some() { + quote! { + // call ctx.execute to emit Solana instructions + let signature = ctx.execute(Instructions::default(), value::map! {}).await?.signature; + } + } else { + quote! {} + }; + let code = quote! { + use flow_lib::command::prelude::*; + + const NAME: &str = #node_id; + + flow_lib::submit!(CommandDescription::new(NAME, |_| build())); + + fn build() -> BuildResult { + const DEFINITION: &str = flow_lib::node_definition!(#node_definition_path); + static CACHE: BuilderCache = BuilderCache::new(|| { + CmdBuilder::new(DEFINITION)?.check_name(NAME) + }); + Ok(CACHE.clone()?.build(run)) + } + + #input_struct + + #output_struct + + async fn run(mut ctx: Context, input: Input) -> Result { + tracing::info!("input: {:?}", input); + + #execute + + Err(CommandError::msg("unimplemented")) + // Ok(Output { }) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_build() { + build().unwrap(); + } + + #[tokio::test] + async fn test_run() { + let ctx = Context::default(); + + build().unwrap().run(ctx, ValueSet::new()).await.unwrap_err(); + } + } + }; + + fmt_code(code) +} + +fn find_parent_module>(path: P) -> Result> { + let path = path.as_ref(); + let parent = path.parent().ok_or_else(|| Error::Io("get parent path"))?; + + let mod_rs = parent.join("mod.rs"); + if mod_rs.is_file() { + return Ok(mod_rs); + } + + let mut mod_rs = parent.to_owned(); + mod_rs.set_extension("rs"); + if mod_rs.is_file() { + return Ok(mod_rs); + } + + let lib_rs = parent.join("lib.rs"); + if lib_rs.is_file() { + return Ok(lib_rs); + } + + Err(Report::new(Error::Io("find parent module"))) +} + +async fn update_parent_module( + def: &CommandDefinition, + module_path: &Path, +) -> Result<(), Report> { + let parent_module_path = find_parent_module(module_path)?; + + let mut parent_module = read_file(&parent_module_path).await?; + + let parsed_parent_module = + syn::parse_file(&parent_module).change_context(Error::Io("invalid rust code"))?; + let has_module = parsed_parent_module.items.iter().any(|item| { + if let syn::Item::Mod(m) = item { + return m.ident.to_string() == def.data.node_id; + } else { + false + } + }); + + if !has_module { + std::fmt::write( + &mut parent_module, + format_args!("\npub mod {};\n", def.data.node_id), + ) + .unwrap(); + println!("updating module {}", parent_module_path.display()); + write_file(&parent_module_path, parent_module).await?; + } + Ok(()) +} + +async fn write_code( + def: &CommandDefinition, + target: &Target, + modules: &[&str], +) -> Result<(), Report> { + let root = target + .src_path + .parent() + .ok_or_else(|| Report::new(Error::Io("find package source path")))? + .as_std_path(); + + let mut path = root.to_path_buf(); + path.extend(modules); + tokio::fs::create_dir_all(&path) + .await + .change_context(Error::Io("create dir"))?; + path.push(format!("{}.rs", def.data.node_id)); + let path = relative_to_pwd(path); + + println!("writing code to {}", path.display()); + if path.is_file() { + if !ask("file already exists, overwrite?").await { + return Ok(()); + } + } + + let code = code_template(def, modules); + tokio::fs::write(&path, code) + .await + .change_context(Error::Io("write file"))?; + + if let Err(error) = update_parent_module(def, &path).await { + eprintln!("failed to update parent module: {:?}", error); + } + + Ok(()) +} + +async fn new_node(allow_dirty: bool, package: &Option) -> Result<(), Report> { + if is_dirty() + .inspect_err(|error| { + eprintln!("{:?}", error); + }) + .unwrap_or(false) + { + if !allow_dirty { + eprintln!("dirty git repository"); + eprintln!("use --allow-dirty to continue"); + return Ok(()); + } + } + let meta = cargo_metadata()?; + let member = if let Some(name) = package.as_deref() { + find_target_crate_by_name(&meta, name)? + } else if let Some(member) = find_target_crate(&meta)? { + member + } else { + eprintln!("could not determine which package to update"); + eprintln!("use `-p` option to specify a package"); + let list = meta + .workspace_packages() + .iter() + .filter(|p| p.targets.iter().any(|t| t.is_lib())) + .map(|p| format!("\n {}", p.name)) + .collect::(); + eprintln!("available packages: {}", list); + return Ok(()); + }; + let lib_target = member + .targets + .iter() + .find(|p| p.is_lib()) + .ok_or_else(|| Error::NotLib(member.name.clone()))?; + println!("using package: {}", member.name); + + println!("enter ? for help"); + + let rust_module_regex = Regex::new( + r#"^(\p{XID_Start}|_)\p{XID_Continue}*(::(\p{XID_Start}|_)\p{XID_Continue}*)*$"#, + ) + .unwrap(); + let rust_module_hint = "enter valid Rust module path to save the node (empty to save at root)"; + let module_path = Prompt::builder() + .question("module path: ") + .check_regex(&rust_module_regex) + .regex_hint(rust_module_hint) + .allow_empty(true) + .build() + .prompt(&mut stdin()) + .await?; + let modules = module_path.split("::").collect::>(); + + let def = prompt_node_definition().await?; + + if let Some(nd) = write_node_definition(&def, member, &modules).await? { + write_code(&def, lib_target, &modules).await?; + + let upload = ask("upload node").await; + if upload { + upload_node(&nd, false, false).await?; + } + } + Ok(()) +} + +#[derive(bon::Builder)] +struct Prompt<'a> { + #[builder(into)] + question: Cow<'a, str>, + #[builder(default)] + allow_empty: bool, + check_regex: Option<&'a Regex>, + #[builder(into)] + regex_hint: Option>, + check_list: Option<&'a [&'a str]>, +} + +impl<'a> Prompt<'a> { + pub async fn prompt( + &self, + stdin: &mut S, + ) -> Result> { + let mut tries = 5; + loop { + match self.prompt_inner(stdin).await { + Ok(result) => break Ok(result), + Err(error) => { + tries -= 1; + if tries == 0 { + break Err(error); + } else { + eprintln!("{:?}", error); + } + } + } + } + } + + async fn prompt_inner( + &self, + stdin: &mut S, + ) -> Result> { + let result = { + loop { + if self.check_list.is_some() || self.regex_hint.is_some() { + print!("{} ", style("?").dim()); + } + print!("{}", style(&self.question).bold()); + std::io::stdout().flush().ok(); + let mut result = String::new(); + stdin.read_line(&mut result).await.ok(); + let result = result.trim(); + if let Some(hint) = &self.regex_hint { + if result == "?" { + println!("{}", hint); + continue; + } + } + if let Some(list) = self.check_list { + if result == "?" { + let availables = format!("possible values: {}", list.join(", ")); + println!("{}", availables); + continue; + } + } + break result.to_owned(); + } + }; + + if self.allow_empty && result.is_empty() { + return Ok(result.to_owned()); + } + if let Some(re) = &self.check_regex { + if !re.is_match(&result) { + let mut report = Report::new(Error::InvalidValue); + if let Some(hint) = &self.regex_hint { + report = report.attach_printable(hint.clone().into_owned()); + } + return Err(report); + } + } + if let Some(list) = self.check_list { + if !list.contains(&result.as_str()) { + let availables = format!("possible values: {}", list.join(", ")); + let report = Report::new(Error::InvalidValue).attach_printable(availables); + return Err(report); + } + } + Ok(result.to_owned()) + } +} + +async fn upload_node(path: &Path, dry_run: bool, no_confirm: bool) -> Result<(), Report> { + let mut client = ApiClient::load().await.change_context(Error::NotLogin)?; + let text = read_file(path).await?; + let def = serde_json::from_str::(&text) + .change_context(Error::ParseNodeDefinition)?; + if def.r#type != "native" { + return Err( + Error::Unimplemented("we only support uploading native nodes at the moment").into(), + ); + } + println!("node: {}", def.data.node_id); + match client.get_my_native_node(&def.data.node_id).await? { + Some((id, db)) => { + if print_diff(&def, &db) { + if dry_run { + return Ok(()); + } + if !no_confirm { + let yes = ask("update node?").await; + if !yes { + return Ok(()); + } + } + let (_, url_path) = client.update_node(id, &def).await?; + println!("updated node, id={}", id); + let url = format!("https://spaceoperator.com/dashboard/nodes/{}", url_path); + println!("view your node:\n{}", url); + } + } + None => { + println!("command is not in database"); + if dry_run { + return Ok(()); + } + if !no_confirm { + let yes = ask("upload?").await; + if !yes { + return Ok(()); + } + } + let (id, url_path) = client.insert_node(&def).await?; + println!("inserted new node, id={}", id); + let url = format!("https://spaceoperator.com/dashboard/nodes/{}", url_path); + println!("view your node:\n{}", url); + } + } + + Ok(()) +} + +type AsyncStdin = AllowStdIo>; + +fn stdin() -> AsyncStdin { + AllowStdIo::new(BufReader::new(std::io::stdin())) +} + +async fn generate_input_struct(path: impl AsRef) -> Result<(), Report> { + let nd = read_file(path).await?; + let nd = serde_json::from_str::(&nd) + .change_context(Error::Json) + .attach_printable("not a valid node definition file")?; + let input_struct = make_input_struct(&nd.targets); + let code = fmt_code(input_struct); + println!("{}", code); + Ok(()) +} + +async fn generate_output_struct(path: impl AsRef) -> Result<(), Report> { + let nd = read_file(path).await?; + let nd = serde_json::from_str::(&nd) + .change_context(Error::Json) + .attach_printable("not a valid node definition file")?; + let output_struct = make_output_struct(&nd.sources); + let code = fmt_code(output_struct); + println!("{}", code); + Ok(()) +} + +fn strip_slash(mut s: String) -> String { + if s.ends_with("/") { + s.pop(); + } + s +} + +async fn generate_flow_server_config(path: impl AsRef) -> Result<(), Report> { + let client = ApiClient::load().await.change_context(Error::NotLogin)?; + + let apikey = client.config.apikey; + let anon_key = client.config.info.anon_key; + let endpoint = strip_slash(client.config.info.supabase_url.to_string()); + let upstream_url = strip_slash(client.config.flow_server.to_string()); + + #[rustfmt::skip] + let config = toml::toml! { +host = "0.0.0.0" +port = 8080 + +local_storage = "_data/guest_local_storage" + +cors_origins = [ "*" ] + +[supabase] +anon_key = anon_key +endpoint = endpoint + +[db] +upstream_url = upstream_url +api_keys = [ apikey ] + }; + + let text = toml::to_string_pretty(&config).change_context(Error::SerializeData)?; + + let path = path.as_ref(); + if write_file(path, text).await? { + println!("generated {}", relative_to_pwd(path).display()); + } + + Ok(()) +} + +fn make_absolute(path: impl AsRef) -> Result> { + let path = path.as_ref(); + if path.is_relative() { + let pwd = std::env::current_dir().change_context(Error::Io("pwd"))?; + Ok(pwd.join(path)) + } else { + Ok(path.to_owned()) + } +} + +async fn start_flow_server( + config: &Option, + mut docker: bool, +) -> Result<(), Report> { + if docker && config.is_some() { + eprintln!("both configuration file and --docker flag are set"); + eprintln!("ignoring --docker"); + docker = false; + }; + let meta = + cargo_metadata().attach_printable("make sure you are inside flow-backend repository")?; + find_target_crate_by_name(&meta, "flow-server") + .attach_printable("make sure you are inside flow-backend repository")?; + + let config_path = if docker { + let local_config_path = meta + .workspace_root + .join("docker/.local-config.toml") + .into_std_path_buf(); + if !std::fs::exists(&local_config_path) + .change_context_lazy(|| Error::ReadFile(local_config_path.clone())) + .attach_printable("could not read configuration file")? + { + println!("generating local configuration"); + let base_config_path = meta + .workspace_root + .join("docker/.config.toml") + .into_std_path_buf(); + if !std::fs::exists(&base_config_path) + .change_context_lazy(|| Error::ReadFile(base_config_path.clone())) + .attach_printable("could not read configuration file")? + { + return Err(Report::new(Error::DockerComposeNotRunning)); + } + let config = std::fs::read_to_string(&base_config_path) + .change_context_lazy(|| Error::ReadFile(base_config_path.clone()))?; + let mut config = toml::from_str::(&config) + .change_context_lazy(|| Error::ReadFile(base_config_path.clone()))?; + + config["supabase"]["endpoint"] = "http://127.0.0.1:8000".into(); + config["db"]["host"] = "127.0.0.1".into(); + config["local_storage"] = "_data/local_storage".into(); + println!( + "writing to {}", + relative_to_pwd(&local_config_path).display() + ); + std::fs::write( + &local_config_path, + toml::to_string(&config).change_context(Error::SerializeData)?, + ) + .change_context_lazy(|| Error::WriteFile(local_config_path.clone()))?; + } + local_config_path + } else { + match config { + Some(config) => make_absolute(config)?, + None => { + let path = meta.workspace_root.join("config.toml"); + if !path.is_file() { + generate_flow_server_config(&path).await?; + } + path.into_std_path_buf() + } + } + }; + + if matches!(std::env::current_dir(), Ok(dir) if dir != meta.workspace_root) { + println!("$ cd {}", relative_to_pwd(&meta.workspace_root).display()); + std::env::set_current_dir(&meta.workspace_root).change_context(Error::Io("chdir"))?; + } + let config_path = relative_to_pwd(config_path); + + spawn_blocking(move || -> Result<(), Report> { + let sh = Shell::new().change_context(Error::Shell)?; + cmd!(sh, "cargo build --bin flow-server") + .run() + .change_context(Error::Subprocess)?; + let flow_server = relative_to_pwd(meta.target_directory.join("debug/flow-server")); + let rust_log = std::env::var("RUST_LOG") + .unwrap_or_else(|_| "info,actix_web=debug,flow_server=debug".to_owned()); + cmd!(sh, "{flow_server} {config_path}") + .env("RUST_LOG", rust_log) + .run() + .change_context(Error::Subprocess)?; + Ok(()) + }) + .await + .change_context(Error::Thread)??; + + Ok(()) +} + +async fn get_latest_version() -> Result> { + let versions = CLIENT + .get("https://index.crates.io/sp/ac/space-operator-cli") + .send() + .await + .change_context(Error::Http)? + .text() + .await + .change_context(Error::Http)?; + let latest = versions.lines().last().unwrap_or_default(); + #[derive(Deserialize)] + struct Meta { + vers: String, + } + let latest = serde_json::from_str::(latest).change_context(Error::Json)?; + Version::parse(&latest.vers).change_context(Error::Version) +} + +async fn check_latest_version() -> Result<(), Report> { + let name = env!("CARGO_PKG_NAME"); + assert!(name == "space-operator-cli"); + let version = Version::parse(env!("CARGO_PKG_VERSION")).change_context(Error::Version)?; + match get_latest_version().await { + Ok(latest) => { + if version.cmp_precedence(&latest) == Ordering::Less { + let string = format!("new version available: {}, please update!", latest); + eprintln!("{}", style(&string).yellow()); + } + } + Err(error) => { + eprintln!("{:?}", error); + } + } + + Ok(()) +} + +async fn run() -> Result<(), Report> { + let args = Args::parse(); + let flow_server = args + .url + .unwrap_or_else(|| Url::parse("https://dev-api.spaceoperator.com").unwrap()); + match &args.command { + Some(Commands::Login {}) => { + check_latest_version().await?; + println!("Go to https://spaceoperator.com/dashboard/profile/apikey go generate a key"); + println!("Please paste your API key below"); + let mut key = String::new(); + let mut stdin = stdin(); + stdin.read_line(&mut key).await.ok(); + let key = key.trim().to_owned(); + + let mut client = ApiClient::new(flow_server, key).await?; + let username = client.get_username().await?.unwrap_or_default(); + println!("Logged in as {:?}", username); + client.save_application_data().await?; + } + Some(Commands::Start { config, docker }) => { + check_latest_version().await?; + start_flow_server(config, *docker).await?; + } + Some(Commands::Node { command }) => match command { + NodeCommands::New { + allow_dirty, + package, + } => { + check_latest_version().await?; + new_node(*allow_dirty, package).await?; + } + NodeCommands::Upload { + path, + dry_run, + no_confirm, + } => { + check_latest_version().await?; + upload_node(path, *dry_run, *no_confirm).await?; + } + }, + Some(Commands::Generate { command }) => match command { + GenerateCommands::Input { path } => generate_input_struct(path).await?, + GenerateCommands::Output { path } => generate_output_struct(path).await?, + GenerateCommands::Config { path } => match path { + Some(path) => generate_flow_server_config(path).await?, + None => generate_flow_server_config("config.toml").await?, + }, + }, + None => { + Args::command().print_long_help().ok(); + } + } + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let color_mode = match get_color() { + ColorChoice::Auto => { + if console::colors_enabled_stderr() { + error_stack::fmt::ColorMode::Color + } else { + error_stack::fmt::ColorMode::None + } + } + ColorChoice::Always => error_stack::fmt::ColorMode::Color, + ColorChoice::Never => error_stack::fmt::ColorMode::None, + }; + Report::set_color_mode(color_mode); + Report::install_debug_hook::(|_, _| {}); + if let Err(error) = run().await { + eprintln!("{:#?}", error); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap_markdown::help_markdown; + + #[test] + fn print_markdown_help() { + println!("{}", help_markdown::()); + } +} diff --git a/lib/space-operator-cli/src/schema.rs b/lib/space-operator-cli/src/schema.rs new file mode 100644 index 00000000..467ad0d4 --- /dev/null +++ b/lib/space-operator-cli/src/schema.rs @@ -0,0 +1,151 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstructionInfo { + pub before: Vec, + pub signature: String, + pub after: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, strum::EnumString, strum::EnumIter, strum::IntoStaticStr)] +#[strum(serialize_all = "snake_case")] +pub enum ValueType { + Bool, + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + F32, + F64, + #[strum(serialize = "number")] + #[strum(serialize = "decimal")] + Decimal, + Pubkey, + // Wormhole address + Address, + Keypair, + Signature, + String, + Bytes, + #[strum(serialize = "array")] + #[strum(serialize = "list")] + Array, + #[strum(serialize = "object")] + #[strum(serialize = "map")] + Map, + Json, + Free, +} + +// ID in database +pub type CommandId = i64; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Data { + pub node_definition_version: Option, + pub unique_id: Option, + pub node_id: String, + pub version: String, + pub display_name: String, + pub description: String, + pub tags: Option>, + pub related_to: Option>, + pub resources: Option, + pub usage: Option, + pub authors: Option>, + pub design: Option, + pub options: Option, + pub instruction_info: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct RelatedTo { + pub id: String, + pub r#type: String, + pub relationship: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Resources { + pub source_code_url: String, + pub documentation_url: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Usage { + pub license: String, + pub license_url: String, + pub pricing: Pricing, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Pricing { + pub currency: String, + pub purchase_price: u64, + pub price_per_run: u64, + pub custom: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CustomPricing { + pub unit: String, + pub value: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Author { + pub name: String, + pub contact: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Design { + pub width: u64, + pub height: u64, + pub icon_url: String, + #[serde(rename = "backgroundColor")] + pub background_color: String, + #[serde(rename = "backgroundColorDark")] + pub background_color_dark: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Source { + pub name: String, + pub r#type: String, + #[serde(rename = "defaultValue")] + pub default_value: JsonValue, + pub tooltip: String, + #[serde(default)] + pub optional: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Target { + pub name: String, + pub type_bounds: Vec, + pub required: bool, + #[serde(rename = "defaultValue")] + pub default_value: JsonValue, + pub tooltip: String, + pub passthrough: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct CommandDefinition { + pub r#type: String, + pub data: Data, + pub sources: Vec, + pub targets: Vec, + #[serde(rename = "targets_form.ui_schema")] + pub ui_schema: JsonValue, + #[serde(rename = "targets_form.json_schema")] + pub json_schema: JsonValue, +} diff --git a/lib/spo-helius/Cargo.toml b/lib/spo-helius/Cargo.toml new file mode 100644 index 00000000..42c2bc47 --- /dev/null +++ b/lib/spo-helius/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "spo-helius" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_with = "3.6.1" +reqwest = { version = "0.12", features = ["json"] } +tracing = "*" +bs58 = "0.4" diff --git a/lib/spo-helius/src/lib.rs b/lib/spo-helius/src/lib.rs new file mode 100644 index 00000000..b973ade2 --- /dev/null +++ b/lib/spo-helius/src/lib.rs @@ -0,0 +1,242 @@ +use anyhow::{anyhow, bail, ensure}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use serde_with::skip_serializing_none; +use std::sync::atomic::AtomicU64; + +pub use reqwest::Client as HttpClient; + +#[derive(Debug)] +pub struct Helius { + client: HttpClient, + mainnet_url: String, + devnet_url: String, + id: AtomicU64, +} + +pub fn is_pubkey(s: &str) -> Result<&str, anyhow::Error> { + let mut buf = [0u8; 32]; + let written = bs58::decode(s).into(&mut buf)?; + ensure!(written == buf.len(), "invalid pubkey"); + Ok(s) +} + +#[skip_serializing_none] +#[derive(Serialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct GetPriorityFeeEstimateRequest { + pub transaction: Option, + pub account_keys: Option>, + pub options: Option, +} + +#[skip_serializing_none] +#[derive(Serialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct GetPriorityFeeEstimateOptions { + pub priority_level: Option, + pub include_all_priority_fee_levels: Option, + pub transaction_encoding: Option, + pub lookback_slots: Option, +} + +#[derive(Serialize, Debug)] +pub enum PriorityLevel { + None, // 0th percentile + Low, // 25th percentile + Medium, // 50th percentile + High, // 75th percentile + VeryHigh, // 95th percentile + // labelled unsafe to prevent people using and draining their funds by accident + UnsafeMax, // 100th percentile + Default, // 50th percentile +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GetPriorityFeeEstimateResponse { + pub priority_fee_estimate: Option, + pub priority_fee_levels: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MicroLamportPriorityFeeLevels { + pub none: f64, + pub low: f64, + pub medium: f64, + pub high: f64, + pub very_high: f64, + pub unsafe_max: f64, +} + +impl Helius { + pub fn new(client: HttpClient, apikey: &str) -> Self { + Self { + client, + mainnet_url: format!("https://mainnet.helius-rpc.com/?api-key={apikey}"), + devnet_url: format!("https://devnet.helius-rpc.com/?api-key={apikey}"), + id: AtomicU64::new(0), + } + } + + fn next_id(&self) -> String { + self.id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + .to_string() + } + + pub fn get_url(&self, solana_net: &str) -> Result<&str, anyhow::Error> { + match solana_net { + "devnet" => Ok(&self.devnet_url), + "mainnet" | "mainnet-beta" => Ok(&self.mainnet_url), + _ => bail!("unknown solana_net: {}", solana_net), + } + } + + pub async fn get_priority_fee_estimate( + &self, + solana_net: &str, + req: GetPriorityFeeEstimateRequest, + ) -> Result { + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": self.next_id(), + "method": "getPriorityFeeEstimate", + "params": [req], + }); + + #[derive(Deserialize)] + struct HeliusResponse { + result: GetPriorityFeeEstimateResponse, + } + + #[derive(Deserialize)] + struct ErrorBody { + message: String, + } + + #[derive(Deserialize)] + struct HeliusError { + error: ErrorBody, + } + + let url = self.get_url(solana_net)?; + let resp = self.client.post(url).json(&req).send().await?; + if resp.status().is_success() { + Ok(resp.json::().await?.result) + } else { + Err(anyhow!(resp.json::().await?.error.message)) + } + } + + pub async fn get_assets_by_group( + &self, + solana_net: &str, + collection: &str, + ) -> Result, anyhow::Error> { + #[derive(Deserialize)] + struct HeliusResult { + items: Vec, + total: u64, + // #[serde(flatten)] + // extra: JsonValue, + } + + #[derive(Deserialize)] + struct HeliusResponse { + result: HeliusResult, + } + + const LIMIT: u64 = 1000; + + is_pubkey(collection)?; + let url = self.get_url(solana_net)?; + + let mut page = 1; + let mut assets = Vec::new(); + + let mut req = serde_json::json!( + { + "jsonrpc": "2.0", + "id": "", + "method": "getAssetsByGroup", + "params": { + "groupKey": "collection", + "groupValue": collection, + "page": 1, + "limit": LIMIT, + "sortBy": { + "sortBy": "created" + }, + "displayOptions": { + "showUnverifiedCollections": false, + "showCollectionMetadata": false, + "showGrandTotal": false, + "showInscription": false, + } + } + } + ); + + loop { + req["id"] = JsonValue::from(self.next_id()); + req["params"]["page"] = JsonValue::from(page); + let resp = self + .client + .post(url) + .json(&req) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + assets.extend(resp.result.items); + if resp.result.total < LIMIT { + break; + } else { + page += 1; + } + } + + Ok(assets) + } + + pub async fn get_asset( + &self, + solana_net: &str, + mint_account: &str, + ) -> Result { + #[derive(Deserialize)] + struct HeliusResponse { + result: JsonValue, + } + + is_pubkey(mint_account)?; + let url = self.get_url(solana_net)?; + + let req = serde_json::json!( + { + "jsonrpc": "2.0", + "id": self.next_id(), + "method": "getAsset", + "params": { + "id": mint_account, + } + } + ); + + let resp = self + .client + .post(url) + .json(&req) + .send() + .await? + .error_for_status()? + .json::() + .await?; + + Ok(resp.result) + } +} diff --git a/node-definition.json b/node-definition.json new file mode 100644 index 00000000..71e3387f --- /dev/null +++ b/node-definition.json @@ -0,0 +1,89 @@ +{ + "type": "native", + "data": { + "node_id": "", + "display_name": "", + "description": "", + "node_definition_version": "0.1", + "unique_id": "", + "version": "0.1", + "tags": [], + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "resources": { + "source_code_url": "", + "documentation_url": "" + }, + "usage": { + "license": "Apache-2.0", + "license_url": "", + "pricing": { + "currency": "USDC", + "purchase_price": 0, + "price_per_run": 0, + "custom": { + "unit": "monthly", + "value": "0" + } + } + }, + "authors": [ + { + "name": "Space Operator", + "contact": "" + } + ], + "design": { + "width": 0, + "height": 0, + "icon_url": "", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff" + }, + "options": {} + }, + "targets": [ + { + "name": "input", + "type_bounds": [ + "free" + ], + "required": true, + "passthrough": false, + "defaultValue": null, + "tooltip": "" + } + ], + "sources": [ + { + "name": "output", + "type": "free", + "optional": false, + "defaultValue": "", + "tooltip": "" + } + ], + "targets_form.json_schema": { + "type": "object", + "title": "", + "properties": { + "input": { + "title": "input", + "type": "string" + } + } + }, + "targets_form.ui_schema": { + "input": { + "ui:widget": "textarea" + }, + "ui:order": [ + "input" + ] + } +} \ No newline at end of file diff --git a/schema/node-definition.schema.json b/schema/node-definition.schema.json new file mode 100644 index 00000000..420b4e1b --- /dev/null +++ b/schema/node-definition.schema.json @@ -0,0 +1,151 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schema.spaceoperator.com/node-definition.schema.json", + "title": "Node Definition", + "$comment": "Node definition is used to upload nodes", + "type": "object", + "required": [ + "type", + "targets" + ], + "properties": { + "type": { + "title": "Node type", + "type": "string", + "enum": [ + "native", + "WASM", + "deno", + "mock" + ] + }, + "targets": { + "title": "Inputs", + "type": "array", + "items": { + "$ref": "#/definitions/input" + } + }, + "sources": { + "title": "Outputs", + "type": "array", + "items": { + "$ref": "#/definitions/output" + } + }, + "data": { + "$ref": "#/definitions/data" + } + }, + "definitions": { + "input": { + "title": "Input port", + "type": "object", + "required": [ + "name", + "type_bounds" + ], + "properties": { + "name": { + "type": "string" + }, + "type_bounds": { + "type": "array", + "items": { + "type": "string" + } + }, + "required": { + "type": "boolean", + "default": true + }, + "passthrough": { + "type": "boolean", + "default": false + }, + "defaultValue": { + "$ref": "https://schema.spaceoperator.com/value.schema.json" + }, + "tooltip": { + "type": "string" + } + } + }, + "output": { + "title": "Output port", + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": true + }, + "tooltip": { + "type": "string" + } + } + }, + "data": { + "type": "object", + "required": [ + "node_id", + "version", + "display_name" + ], + "properties": { + "node_id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "instruction_info": { + "type": "object", + "required": [ + "before", + "signature", + "after" + ], + "properties": { + "before": { + "type": "array", + "items": { + "type": "string" + } + }, + "signature": { + "type": "string" + }, + "after": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } +} diff --git a/schema/value.schema.json b/schema/value.schema.json new file mode 100644 index 00000000..53c19250 --- /dev/null +++ b/schema/value.schema.json @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schema.spaceoperator.com/value.schema.json", + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/N" + }, + { + "$ref": "#/definitions/S" + }, + { + "$ref": "#/definitions/B" + }, + { + "$ref": "#/definitions/U" + }, + { + "$ref": "#/definitions/I" + }, + { + "$ref": "#/definitions/U1" + }, + { + "$ref": "#/definitions/I1" + }, + { + "$ref": "#/definitions/F" + }, + { + "$ref": "#/definitions/D" + }, + { + "$ref": "#/definitions/B3" + }, + { + "$ref": "#/definitions/B6" + }, + { + "$ref": "#/definitions/BY" + }, + { + "$ref": "#/definitions/A" + }, + { + "$ref": "#/definitions/M" + } + ], + "definitions": { + "N": { + "title": "Null", + "description": "Null value", + "type": "object", + "properties": { + "N": { + "const": 0 + } + }, + "required": [ + "N" + ], + "additionalProperties": false + }, + "S": { + "title": "String", + "description": "String value", + "type": "object", + "properties": { + "S": { + "type": "string" + } + }, + "required": [ + "S" + ], + "additionalProperties": false + }, + "B": { + "title": "Boolean", + "description": "Boolean value", + "type": "object", + "properties": { + "B": { + "type": "boolean" + } + }, + "required": [ + "B" + ], + "additionalProperties": false + }, + "U": { + "title": "U64", + "description": "Unsigned 64-bit integer", + "type": "object", + "properties": { + "U": { + "type": "string" + } + }, + "required": [ + "U" + ], + "additionalProperties": false + }, + "I": { + "title": "I64", + "description": "64-bit integer", + "type": "object", + "properties": { + "I": { + "type": "string" + } + }, + "required": [ + "I" + ], + "additionalProperties": false + }, + "U1": { + "title": "U128", + "description": "Unsigned 128-bit integer", + "type": "object", + "properties": { + "U1": { + "type": "string" + } + }, + "required": [ + "U1" + ], + "additionalProperties": false + }, + "I1": { + "title": "I128", + "description": "128-bit integer", + "type": "object", + "properties": { + "I1": { + "type": "string" + } + }, + "required": [ + "I1" + ], + "additionalProperties": false + }, + "F": { + "title": "Float", + "description": "64-bit floating-point number", + "type": "object", + "properties": { + "F": { + "type": "string" + } + }, + "required": [ + "F" + ], + "additionalProperties": false + }, + "D": { + "title": "Decimal", + "description": "Decimal using rust_decimal library", + "type": "object", + "properties": { + "D": { + "type": "string" + } + }, + "required": [ + "D" + ], + "additionalProperties": false + }, + "B3": { + "title": "32-bytes", + "description": "32-bytes binary value", + "type": "object", + "properties": { + "B3": { + "type": "string" + } + }, + "required": [ + "B3" + ], + "additionalProperties": false + }, + "B6": { + "title": "64-bytes", + "description": "64-bytes binary value", + "type": "object", + "properties": { + "B6": { + "type": "string" + } + }, + "required": [ + "B6" + ], + "additionalProperties": false + }, + "BY": { + "title": "Bytes", + "description": "Binary value", + "type": "object", + "properties": { + "BY": { + "type": "string" + } + }, + "required": [ + "BY" + ], + "additionalProperties": false + }, + "A": { + "title": "Array", + "description": "Array of values", + "type": "object", + "properties": { + "A": { + "type": "array", + "items": { + "$ref": "#" + } + } + }, + "required": [ + "A" + ], + "additionalProperties": false + }, + "M": { + "title": "Map", + "description": "Key-value map", + "type": "object", + "properties": { + "M": { + "type": "object", + "patternProperties": { + "": { + "$ref": "#" + } + } + } + }, + "required": [ + "M" + ], + "additionalProperties": false + } + } +} diff --git a/scripts/build_images.bash b/scripts/build_images.bash new file mode 100755 index 00000000..4a95cc6d --- /dev/null +++ b/scripts/build_images.bash @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -Eexuo pipefail + +CMD="podman" +BUILD="podman build --pull=always" +if [[ "${1:-}" == docker ]]; then + CMD="docker" + BUILD="docker build --pull" +fi + +echo Using $CMD + +PROFILE=${PROFILE:-release} + +NAME="space-operator/flow-server" +DOCKERFILE="crates/flow-server/Dockerfile" + +DIRTY="" +if [[ "$(git describe --always --dirty)" == *-dirty ]]; then + DIRTY="-dirty" +fi + +set -x +time $BUILD --target rustc -t "$NAME-rustc:latest" -f "$DOCKERFILE" . +time $BUILD --target planner -t "$NAME-planner:latest" -f "$DOCKERFILE" . +time $BUILD --target cacher --build-arg PROFILE=$PROFILE -t "$NAME-cacher:$PROFILE-latest" -f "$DOCKERFILE" . + +BUILDER_TAG=$RANDOM +time $BUILD --target builder --build-arg PROFILE=$PROFILE -t "$NAME-builder:$BUILDER_TAG" -f "$DOCKERFILE" . + +COMMIT="$(git rev-parse --verify HEAD)$DIRTY" +time $BUILD -t "$NAME:$COMMIT" -f "$DOCKERFILE" . + +BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)$DIRTY}" +$CMD tag $NAME:$COMMIT $NAME:$BRANCH + +$CMD image rm "$NAME-builder:$BUILDER_TAG" diff --git a/scripts/build_wasm_tests.bash b/scripts/build_wasm_tests.bash new file mode 100755 index 00000000..24484a4e --- /dev/null +++ b/scripts/build_wasm_tests.bash @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +TARGET_DIR="$PWD/target/" + +for d in ./crates/space-wasm/tests/* ; do + if [ -d "$d" ]; then + echo $d + pushd "$d" > /dev/null + if ! [ -d "target/" ] && [ "${1-}" = "sub" ]; then + btrfs subvolume create target/ + fi + cargo build --release --quiet + popd > /dev/null + fi +done diff --git a/scripts/ecr-push.bash b/scripts/ecr-push.bash new file mode 100755 index 00000000..549a4caa --- /dev/null +++ b/scripts/ecr-push.bash @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -Eeuxo pipefail + +CMD="podman" +if [[ "${1:-}" == docker ]]; then + CMD="docker" +fi + + +ORG=space-operator +IMAGE=flow-server +NAME="$ORG/$IMAGE" + +if [ "${1:-}" = "login" ]; then + aws ecr-public get-login-password --region us-east-1 | + $CMD login --username AWS --password-stdin "public.ecr.aws/$ORG" +fi + +DIRTY="" +if [[ "$(git describe --always --dirty)" == *-dirty ]]; then + DIRTY="-dirty" +fi + +BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)$DIRTY}" +COMMIT="$(git rev-parse --verify HEAD)$DIRTY" + +$CMD tag $NAME:$COMMIT public.ecr.aws/$NAME:$COMMIT +$CMD push public.ecr.aws/$NAME:$COMMIT + +$CMD tag $NAME:$BRANCH public.ecr.aws/$NAME:$BRANCH +$CMD push public.ecr.aws/$NAME:$BRANCH + +if [[ "$BRANCH" == "main" ]]; then + $CMD tag $NAME:$COMMIT public.ecr.aws/$NAME:latest + $CMD push public.ecr.aws/$NAME:latest +fi diff --git a/test_files/HTTP Request.json b/test_files/HTTP Request.json new file mode 100644 index 00000000..ec93088d --- /dev/null +++ b/test_files/HTTP Request.json @@ -0,0 +1,1506 @@ +{ + "flow": { + "id": 0, + "user_id": "00000000-0000-0000-0000-000000000000", + "name": "HTTP Request", + "isPublic": true, + "description": "Flow Description", + "tags": [], + "created_at": "2025-01-22", + "parent_flow": null, + "viewport": { + "x": 463.2111171121666, + "y": -312.6008430652921, + "zoom": 0.6800149394278289 + }, + "uuid": "00000000-0000-0000-0000-000000000000", + "updated_at": "2025-01-22T19:39:26.28098", + "lastest_flow_run_id": null, + "custom_networks": [], + "current_network": { + "id": "01000000-0000-8000-8000-000000000000", + "url": "https://api.devnet.solana.com", + "type": "default", + "wallet": "Solana", + "cluster": "devnet" + }, + "instructions_bundling": "Off", + "guide": null, + "environment": { + "RUST_LOG": "trace" + }, + "nodes": [ + { + "width": 192, + "height": 280, + "id": "3c2f2360-6a27-4826-b843-c126eac73764", + "data": { + "flow": { + "minimized": false + }, + "tags": [ + "std", + "network" + ], + "type": "native", + "design": { + "backgroundColor": "#fff", + "backgroundColorDark": "#000000" + }, + "node_id": "http_request", + "sources": [ + { + "id": "f2fef5a5-04f6-471c-acc1-58dbfe24ebac", + "name": "body", + "type": "free", + "tooltip": "", + "defaultValue": "" + }, + { + "id": "c00fc2d4-8326-4c26-9c5b-be1ebacf6d3e", + "name": "headers", + "type": "object", + "tooltip": "", + "defaultValue": "" + } + ], + "targets": [ + { + "id": "6c81e32d-75b9-4f90-b799-19b56655a438", + "name": "url", + "tooltip": "Request's URL", + "required": true, + "passthrough": false, + "type_bounds": [ + "string" + ], + "defaultValue": null + }, + { + "id": "21cec176-911d-4de2-b246-0b24501d7835", + "name": "method", + "tooltip": "GET, POST, PATCH, etc.", + "required": false, + "passthrough": false, + "type_bounds": [ + "string" + ], + "defaultValue": "GET" + }, + { + "id": "0c71b2cb-324c-4f4a-b45c-fe315c101657", + "name": "headers", + "tooltip": "", + "required": false, + "passthrough": false, + "type_bounds": [ + "kv" + ], + "defaultValue": null + }, + { + "id": "1bc8288d-b001-4568-93f0-91d85817949b", + "name": "basic_auth", + "tooltip": "e.g. {\"user\": \"\", \"password\": \"\"}", + "required": false, + "passthrough": false, + "type_bounds": [ + "object" + ], + "defaultValue": null + }, + { + "id": "35355c7b-8439-469f-9aa1-45b28138afd7", + "name": "query_params", + "tooltip": "", + "required": false, + "passthrough": false, + "type_bounds": [ + "array" + ], + "defaultValue": null + }, + { + "id": "81501de7-4ee6-4434-a220-02619dfb72a1", + "name": "body", + "tooltip": "Request's JSON body", + "required": false, + "passthrough": false, + "type_bounds": [ + "json" + ], + "defaultValue": null + }, + { + "id": "64187369-5198-4373-ab6e-b5b4cfd3ad8a", + "name": "form", + "tooltip": "content-type will be automatically set to multipart/form-data", + "required": false, + "passthrough": false, + "type_bounds": [ + "kv" + ], + "defaultValue": null + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "HTTP Request", + "targets_form": { + "extra": { + "supabase_id": 618 + }, + "form_data": {}, + "ui_schema": {}, + "json_schema": {} + }, + "unique_node_id": "http_request.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "extent": null, + "dragging": false, + "position": { + "x": 781.700477593937, + "y": 626.3791554701614 + }, + "selected": false, + "parentNode": null, + "positionAbsolute": { + "x": 781.700477593937, + "y": 626.3791554701614 + } + }, + { + "width": 428, + "height": 302, + "id": "7f4d02b0-8720-432e-8f90-17b1a157764c", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "b6f75a55-555a-4603-81df-bc043df305ab", + "name": "headers", + "type": "free", + "tooltip": "", + "defaultValue": [] + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "kv", + "selectType": false, + "supabase_id": 138 + }, + "form_data": { + "label": "headers", + "form_label": [] + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "headers" + }, + "form_label": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string", + "title": "Key" + }, + { + "type": "string", + "title": "Value" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": -566.0665112031566, + "y": 178.74588890043174 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": -566.0665112031566, + "y": 178.74588890043174 + } + }, + { + "width": 428, + "height": 330, + "id": "0f0e0d37-4862-42f1-8d78-a692529efef4", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "2f953f56-65f7-4fbb-9448-9fe23fbf249f", + "name": "url", + "type": "free", + "tooltip": "", + "defaultValue": "https://dev-api.spaceoperator.com/flow/start_unverified/2941" + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "string", + "selectType": false, + "supabase_id": 138 + }, + "form_data": { + "label": "url", + "form_label": "https://dev-api.spaceoperator.com/flow/start_unverified/2941" + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "url" + }, + "form_label": { + "type": "string", + "title": "Value" + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": 158.3710469668755, + "y": -55.67612459202242 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": 158.3710469668755, + "y": -55.67612459202242 + } + }, + { + "width": 428, + "height": 330, + "id": "ddfaecf7-5fc8-4f4e-8967-108256579118", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "ea260d0e-031c-4074-a4df-4d13dd18194b", + "name": "method", + "type": "free", + "tooltip": "", + "defaultValue": "POST" + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "string", + "selectType": false, + "supabase_id": 138 + }, + "form_data": { + "label": "method", + "form_label": "POST" + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "method" + }, + "form_label": { + "type": "string", + "title": "Value" + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": 134.25212266465292, + "y": 298.59213121729806 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": 134.25212266465292, + "y": 298.59213121729806 + } + }, + { + "width": 311, + "height": 200, + "id": "3b207d18-7bf7-4bf7-81fd-f9d44f8c2ae7", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#dea10a" + }, + "node_id": "flow_output", + "sources": [], + "targets": [ + { + "id": "55b11abd-a5e1-4f47-abdb-bb43160b1a50", + "name": "body", + "tooltip": "", + "required": true, + "passthrough": false, + "type_bounds": [ + "free" + ], + "defaultValue": "" + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Output", + "targets_form": { + "extra": { + "type": "free", + "supabase_id": 137 + }, + "form_data": { + "label": "body" + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + } + }, + "unique_node_id": "flow_output.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": 1047.3193710269059, + "y": 203.44140818157342 + }, + "selected": false, + "positionAbsolute": { + "x": 1047.3193710269059, + "y": 203.44140818157342 + } + }, + { + "width": 311, + "height": 200, + "id": "2c301028-007e-495d-9f63-16f23ca7534c", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#dea10a" + }, + "node_id": "flow_output", + "sources": [], + "targets": [ + { + "id": "94973509-2530-4be9-8c63-5f0a46de365e", + "name": "headers", + "tooltip": "", + "required": true, + "passthrough": false, + "type_bounds": [ + "free" + ], + "defaultValue": "" + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Output", + "targets_form": { + "extra": { + "type": "free", + "supabase_id": 137 + }, + "form_data": { + "label": "headers" + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + } + }, + "unique_node_id": "flow_output.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": 1188.432165078095, + "y": 502.38987665886714 + }, + "selected": false, + "positionAbsolute": { + "x": 1188.432165078095, + "y": 502.38987665886714 + } + }, + { + "width": 428, + "height": 500, + "id": "4437cbc3-a204-4e1b-aecf-1f88dbf199f1", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "98d07043-55be-4297-9fb3-d2ce7a1cd9eb", + "name": "basic_auth", + "type": "free", + "tooltip": "", + "defaultValue": "" + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "json", + "selectType": true, + "supabase_id": 138 + }, + "form_data": { + "label": "basic_auth", + "option": "json", + "form_label": "" + }, + "ui_schema": { + "label": { + "ui:classNames": "items-title-mui nodrag" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "" + }, + "form_label": { + "type": "string", + "title": "Value", + "option": "json" + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": -548.9652313387705, + "y": 725.4487042745988 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": -548.9652313387705, + "y": 725.4487042745988 + } + }, + { + "width": 428, + "height": 321, + "id": "4f0f2577-4b33-4cb3-b268-77f859331ba4", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "3a746c36-e4f9-4e9c-8eaf-c025ffdbf68d", + "name": "query_params", + "type": "free", + "tooltip": "", + "defaultValue": [] + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "array", + "selectType": false, + "supabase_id": 138 + }, + "form_data": { + "label": "query_params", + "form_label": [] + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "query_params" + }, + "form_label": { + "type": "array", + "items": { + "type": "string", + "title": "value" + }, + "title": "Array of strings" + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": -439.65560942043305, + "y": 1265.0009431505846 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": -439.65560942043305, + "y": 1265.0009431505846 + } + }, + { + "width": 428, + "height": 450, + "id": "a41eb155-824e-4e5c-a2d6-10af84a2a3c0", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "77c44eae-7baf-42cc-b972-281feed3deb6", + "name": "form", + "type": "free", + "tooltip": "", + "defaultValue": [] + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "unknown", + "selectType": true, + "supabase_id": 138 + }, + "form_data": { + "label": "form", + "form_label": [] + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "form" + }, + "form_label": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string", + "title": "Key" + }, + { + "type": "string", + "title": "Value" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "dragging": false, + "position": { + "x": 490.29918087829367, + "y": 1620.7151968814044 + }, + "selected": false, + "draggable": true, + "positionAbsolute": { + "x": 490.29918087829367, + "y": 1620.7151968814044 + } + }, + { + "width": 240, + "height": 253, + "id": "153d07d6-9100-4a3f-ae09-a684d2df2af7", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#fff", + "backgroundColorDark": "#000000" + }, + "node_id": "rhai_script_1x1", + "sources": [ + { + "id": "8254fa95-2103-45e0-8f74-d364ce6bc38b", + "name": "u", + "type": "free", + "tooltip": "", + "defaultValue": "" + } + ], + "targets": [ + { + "id": "cfe41f63-1a95-4447-abdd-b7c23c20a309", + "name": "source", + "tooltip": "Script's source code", + "required": true, + "passthrough": false, + "type_bounds": [ + "string" + ], + "defaultValue": null + }, + { + "id": "04a82909-c51f-4655-8422-b02a728d3ff3", + "name": "a", + "tooltip": "", + "required": false, + "passthrough": false, + "type_bounds": [ + "free" + ], + "defaultValue": null + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "RHAI Script 1x1", + "targets_form": { + "extra": { + "supabase_id": 1037 + }, + "form_data": { + "source": "let u;\n\nif a ==\"\"{\n u =();\n} else {\n u = a;\n}" + }, + "ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + }, + "json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "type": "string", + "title": "source" + } + } + } + }, + "unique_node_id": "rhai_script_1x1.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "extent": null, + "dragging": false, + "position": { + "x": -66.69996279446153, + "y": 800.309185469744 + }, + "selected": false, + "draggable": true, + "parentNode": null, + "positionAbsolute": { + "x": -66.69996279446153, + "y": 800.309185469744 + } + }, + { + "width": 240, + "height": 273, + "id": "dbf139ba-3239-4096-8930-9293bcfeaec1", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#fff", + "backgroundColorDark": "#000000" + }, + "node_id": "rhai_script_1x1", + "sources": [ + { + "id": "4ff9cf6f-cd26-4e4a-97fc-826cfe075f1d", + "name": "u", + "type": "free", + "tooltip": "", + "defaultValue": "" + } + ], + "targets": [ + { + "id": "b77c8fa0-608a-4303-a3c0-f9d899512105", + "name": "source", + "tooltip": "Script's source code", + "required": true, + "passthrough": false, + "type_bounds": [ + "string" + ], + "defaultValue": null + }, + { + "id": "9384d43d-d5fa-4e84-85f9-0ee5000225f5", + "name": "a", + "tooltip": "", + "required": false, + "passthrough": false, + "type_bounds": [ + "free" + ], + "defaultValue": null + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "cloned_RHAI Script 1x1", + "targets_form": { + "extra": { + "supabase_id": 1037 + }, + "form_data": { + "source": "let u;\n\nif a ==\"\"{\n u =();\n} else {\n u = a;\n}" + }, + "ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + }, + "json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "type": "string", + "title": "source" + } + } + } + }, + "unique_node_id": "rhai_script_1x1.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "extent": null, + "dragging": false, + "position": { + "x": 176.24334598705815, + "y": 1034.1275779506286 + }, + "selected": false, + "draggable": true, + "parentNode": null, + "positionAbsolute": { + "x": 176.24334598705815, + "y": 1034.1275779506286 + } + }, + { + "width": 240, + "height": 273, + "id": "8de554a3-9bc2-4fac-9342-5de755dce619", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#fff", + "backgroundColorDark": "#000000" + }, + "node_id": "rhai_script_1x1", + "sources": [ + { + "id": "06d019c7-e91f-402b-a4c6-9369f1b061c9", + "name": "u", + "type": "free", + "tooltip": "", + "defaultValue": "" + } + ], + "targets": [ + { + "id": "fe4562f6-37d0-4f1f-9159-15c38d94575b", + "name": "source", + "tooltip": "Script's source code", + "required": true, + "passthrough": false, + "type_bounds": [ + "string" + ], + "defaultValue": null + }, + { + "id": "e60e398a-c98d-440b-9b45-23fc733c0ae0", + "name": "a", + "tooltip": "", + "required": false, + "passthrough": false, + "type_bounds": [ + "free" + ], + "defaultValue": null + } + ], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "cloned_cloned_RHAI Script 1x1", + "targets_form": { + "extra": { + "supabase_id": 1037 + }, + "form_data": { + "source": "let u;\n\nif a ==\"\"{\n u =();\n} else {\n u = a;\n}" + }, + "ui_schema": { + "source": { + "ui:widget": "textarea" + }, + "ui:order": [ + "source" + ] + }, + "json_schema": { + "type": "object", + "title": "RHAI script", + "properties": { + "source": { + "type": "string", + "title": "source" + } + } + } + }, + "unique_node_id": "rhai_script_1x1.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "extent": null, + "dragging": false, + "position": { + "x": 708.8861724964662, + "y": 1213.535401112062 + }, + "selected": false, + "draggable": true, + "parentNode": null, + "positionAbsolute": { + "x": 708.8861724964662, + "y": 1213.535401112062 + } + }, + { + "width": 428, + "height": 447, + "id": "dc3e0fcb-df6b-4e73-9a1c-6b1879255beb", + "data": { + "flow": { + "minimized": false + }, + "tags": [], + "type": "native", + "design": { + "backgroundColor": "#f2fcff", + "backgroundColorDark": "#0491d6" + }, + "node_id": "flow_input", + "sources": [ + { + "id": "f41b2dfd-e8c8-4d5e-9e34-97ac559bed02", + "name": "body", + "type": "free", + "tooltip": "", + "defaultValue": { + "url": { + "S": "https://dev-api.spaceoperator.com/flow/start_unverified/2941" + }, + "body": { + "M": { + "inputs": { + "M": { + "uri": { + "S": "https://j45eekk7guujcl6hxt4xxjkaceqhomvgkgnda6uz76hrmymj7zea.arweave.net/TzpCKV81KJEvx7z5e6VAESB3MqZRmjB6mf-PFmGJ_kg" + }, + "supply": { + "D": "100000" + }, + "decimals": { + "D": "9" + }, + "fee_payer": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + }, + "is_mutable": { + "B": false + }, + "mint_authority": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + } + } + } + } + }, + "method": { + "S": "POST" + }, + "headers": { + "M": { + "Content-Type": { + "S": "application/json" + }, + "Authorization": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + } + } + } + } + } + ], + "targets": [], + "version": "0.1", + "related_to": [ + { + "id": "", + "type": "", + "relationship": "" + } + ], + "description": "", + "display_name": "Flow Input", + "targets_form": { + "extra": { + "type": "json", + "selectType": false, + "supabase_id": 138 + }, + "form_data": { + "label": "body", + "form_label": { + "url": { + "S": "https://dev-api.spaceoperator.com/flow/start_unverified/2941" + }, + "body": { + "M": { + "inputs": { + "M": { + "uri": { + "S": "https://j45eekk7guujcl6hxt4xxjkaceqhomvgkgnda6uz76hrmymj7zea.arweave.net/TzpCKV81KJEvx7z5e6VAESB3MqZRmjB6mf-PFmGJ_kg" + }, + "supply": { + "D": "100000" + }, + "decimals": { + "D": "9" + }, + "fee_payer": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + }, + "is_mutable": { + "B": false + }, + "mint_authority": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + } + } + } + } + }, + "method": { + "S": "POST" + }, + "headers": { + "M": { + "Content-Type": { + "S": "application/json" + }, + "Authorization": { + "S": "98BTD7A8WKPz6CnZZrV5tBkdGQtqfjJwigW2V6daAihi" + } + } + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "body" + }, + "form_label": { + "type": "string", + "title": "Value", + "option": "json" + } + } + } + }, + "unique_node_id": "flow_input.0.1", + "node_definition_version": "0.1" + }, + "type": "native", + "extent": null, + "dragging": false, + "position": { + "x": 51.912134022193925, + "y": 1472.328515159905 + }, + "selected": false, + "draggable": true, + "parentNode": null, + "positionAbsolute": { + "x": 51.912134022193925, + "y": 1472.328515159905 + } + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "8e347422-2337-4470-ae8c-5e0388441d2d", + "source": "7f4d02b0-8720-432e-8f90-17b1a157764c", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "selected": false, + "sourceHandle": "b6f75a55-555a-4603-81df-bc043df305ab", + "targetHandle": "0c71b2cb-324c-4f4a-b45c-fe315c101657" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "014c308d-e7eb-45b3-aeed-73749f51c376", + "source": "ddfaecf7-5fc8-4f4e-8967-108256579118", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "selected": false, + "sourceHandle": "ea260d0e-031c-4074-a4df-4d13dd18194b", + "targetHandle": "21cec176-911d-4de2-b246-0b24501d7835" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "742ff413-819e-47bb-a2b0-7f50b483a2e8", + "source": "3c2f2360-6a27-4826-b843-c126eac73764", + "target": "3b207d18-7bf7-4bf7-81fd-f9d44f8c2ae7", + "selected": false, + "sourceHandle": "f2fef5a5-04f6-471c-acc1-58dbfe24ebac", + "targetHandle": "55b11abd-a5e1-4f47-abdb-bb43160b1a50" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "68c42bdb-645c-4e32-bcaa-e796754ef15b", + "source": "3c2f2360-6a27-4826-b843-c126eac73764", + "target": "2c301028-007e-495d-9f63-16f23ca7534c", + "selected": false, + "sourceHandle": "c00fc2d4-8326-4c26-9c5b-be1ebacf6d3e", + "targetHandle": "94973509-2530-4be9-8c63-5f0a46de365e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "0f91bf5b-ba6b-4fe7-bb8a-f3e69856f612", + "source": "4437cbc3-a204-4e1b-aecf-1f88dbf199f1", + "target": "153d07d6-9100-4a3f-ae09-a684d2df2af7", + "zIndex": 5, + "selected": false, + "sourceHandle": "98d07043-55be-4297-9fb3-d2ce7a1cd9eb", + "targetHandle": "04a82909-c51f-4655-8422-b02a728d3ff3" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "0a0f7cbd-202b-4dec-868d-f2dceddd819a", + "source": "153d07d6-9100-4a3f-ae09-a684d2df2af7", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "zIndex": 5, + "selected": false, + "sourceHandle": "8254fa95-2103-45e0-8f74-d364ce6bc38b", + "targetHandle": "1bc8288d-b001-4568-93f0-91d85817949b" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "f1bdaa3c-0f9d-4dde-9283-8fa43c5cabc4", + "source": "4f0f2577-4b33-4cb3-b268-77f859331ba4", + "target": "dbf139ba-3239-4096-8930-9293bcfeaec1", + "zIndex": 5, + "selected": false, + "sourceHandle": "3a746c36-e4f9-4e9c-8eaf-c025ffdbf68d", + "targetHandle": "9384d43d-d5fa-4e84-85f9-0ee5000225f5" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "9ac9a2a4-1aba-4c96-8072-3a2268e5cf8c", + "source": "dbf139ba-3239-4096-8930-9293bcfeaec1", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "zIndex": 5, + "selected": false, + "sourceHandle": "4ff9cf6f-cd26-4e4a-97fc-826cfe075f1d", + "targetHandle": "35355c7b-8439-469f-9aa1-45b28138afd7" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "f3067a4d-6427-4700-996b-05899885f61a", + "source": "a41eb155-824e-4e5c-a2d6-10af84a2a3c0", + "target": "8de554a3-9bc2-4fac-9342-5de755dce619", + "zIndex": 5, + "selected": false, + "sourceHandle": "77c44eae-7baf-42cc-b972-281feed3deb6", + "targetHandle": "e60e398a-c98d-440b-9b45-23fc733c0ae0" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "979622bc-adab-4de2-b444-d58f500ab879", + "source": "8de554a3-9bc2-4fac-9342-5de755dce619", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "zIndex": 5, + "selected": false, + "sourceHandle": "06d019c7-e91f-402b-a4c6-9369f1b061c9", + "targetHandle": "64187369-5198-4373-ab6e-b5b4cfd3ad8a" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "6bfba016-8a80-44b0-a68f-9e23d2254259", + "source": "0f0e0d37-4862-42f1-8d78-a692529efef4", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "zIndex": 5, + "sourceHandle": "2f953f56-65f7-4fbb-9448-9fe23fbf249f", + "targetHandle": "6c81e32d-75b9-4f90-b799-19b56655a438", + "selected": false + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "id": "ee6f832b-095b-4fb5-80a4-5810f9a9c49e", + "source": "dc3e0fcb-df6b-4e73-9a1c-6b1879255beb", + "target": "3c2f2360-6a27-4826-b843-c126eac73764", + "zIndex": 5, + "sourceHandle": "f41b2dfd-e8c8-4d5e-9e34-97ac559bed02", + "targetHandle": "81501de7-4ee6-4434-a220-02619dfb72a1", + "selected": false + } + ], + "mosaic": null, + "start_shared": true, + "start_unverified": true, + "gg_marketplace": null + }, + "bookmarks": [] +} diff --git a/test_files/const_form_data.json b/test_files/const_form_data.json new file mode 100644 index 00000000..edfbdbe9 --- /dev/null +++ b/test_files/const_form_data.json @@ -0,0 +1,750 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "const_form_data", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 72.22803347280335 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-01-11", + "parent_flow": null, + "viewport": { + "x": 797.4599780495628, + "y": 277.53672489217666, + "zoom": 0.6172813034467697 + }, + "nodes": [ + { + "width": 300, + "height": 180, + "selected": false, + "id": "de75d62b-49e2-40b6-a2d4-a0ef872f8eaa", + "type": "native", + "position": { + "x": -1020, + "y": -345 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "d936d1df-60ac-49f8-a668-5c745e225bd7", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "56e8b7de-07ee-46eb-97b0-f58f554a14a6" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "S": "Hello" + }, + "type": "String" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": -345 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "1c0cc7f6-f859-40c3-b467-1cb90ddd366b", + "type": "native", + "position": { + "x": -1020, + "y": -150 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "e972a0de-cf02-452c-af75-c19f5f57db03", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "c702398b-1931-4f33-8514-3f523a20e7d2" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "{\"key\": \"value\"}", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": -150 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "28e4be12-b67b-4596-8cd4-8fbece721bcf", + "type": "native", + "position": { + "x": -1020, + "y": 45 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "6fac7f97-f292-446c-b4ef-631dd79bd2f9", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "pubkey", + "defaultValue": "", + "tooltip": "", + "id": "8657d45c-2517-413c-b76f-b58d0a8d5eb3" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "B3": "ESxeViFP4r7THzVx9hJDkhj4HrNGSjJSFRPbGaAb97hN" + }, + "type": "Pubkey" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": 45 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 300, + "selected": false, + "id": "6d40f44d-702e-421e-9977-160d247e061c", + "type": "native", + "position": { + "x": -1020, + "y": 240 + }, + "style": { + "height": 300, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "9059f277-b1c4-470d-82d1-e578e89f9052", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "e44b9c92-7802-4d12-a63b-f3956a3a0e70" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "S": "violin lock water level drift skill device cart ginger hello orange energy" + }, + "type": "SeedPhrase" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": 240 + }, + "dragging": false + }, + { + "width": 300, + "height": 120, + "selected": false, + "id": "348975ed-22fb-4259-8182-316c2c8dfe5b", + "type": "native", + "position": { + "x": -1020, + "y": 555 + }, + "style": { + "height": 120, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "a6b1dbc0-0db4-4870-b8ba-7885443fd529", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "bool", + "defaultValue": "", + "tooltip": "", + "id": "172e1b91-a3e9-44b3-b113-e95da7526005" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "B": true + }, + "type": "BoolTrue" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": 555 + }, + "dragging": false + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "74a55807-6cb7-4b5c-a83d-7a4eaed44983", + "type": "native", + "position": { + "x": -150, + "y": -345 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "b392b61d-454a-4cef-9174-8deb3b2c0579", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "decimal", + "defaultValue": "", + "tooltip": "", + "id": "19f990fa-b0fe-4cbd-9a63-91cf7370c308" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "D": "3.1415926535897932384626433833" + }, + "type": "Decimal" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": -345 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "b33bfdc6-8230-4e93-8116-8244806579d2", + "type": "native", + "position": { + "x": -150, + "y": -150 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "e58ac8cf-e5a7-438d-8378-4fcd2d183d8c", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "u64", + "defaultValue": "", + "tooltip": "", + "id": "aefacd17-9419-484c-b6e1-ac8d53d4372e" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "U": "18446744073709551615" + }, + "type": "U64" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": -150 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "ba7bd339-05bd-46e8-bf4f-f8f620409596", + "type": "native", + "position": { + "x": -150, + "y": 45 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "2b48e75c-e725-4cff-934d-7d170066246a", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "i64", + "defaultValue": "", + "tooltip": "", + "id": "86fa7df3-2e05-4ae2-90b1-c11ba66b964c" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "I": "-9223372036854775808" + }, + "type": "I64" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": 45 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "d0b08726-5971-4a67-8402-e68da4274bd9", + "type": "native", + "position": { + "x": -150, + "y": 240 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "f1347a0f-2684-4c0f-974a-9b687a4441a5", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "u128", + "defaultValue": "", + "tooltip": "", + "id": "3fb37c8c-aeac-4e85-b8e8-fab09abe9c20" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "U1": "340282366920938463463374607431768211455" + }, + "type": "U128" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": 240 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "cc0d1c74-2058-4993-9641-0d0d444c47da", + "type": "native", + "position": { + "x": -150, + "y": 435 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "d03bb6fd-1e4e-4da1-bbf9-65449b3695e4", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "i128", + "defaultValue": "", + "tooltip": "", + "id": "b8de3024-ca27-4df6-8cd6-c773e8117e44" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "I1": "-170141183460469231731687303715884105728" + }, + "type": "I128" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": 435 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 180, + "selected": false, + "id": "36acaae1-607d-4474-bf0f-dc4259bc01db", + "type": "native", + "position": { + "x": -150, + "y": 630 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "acd6e290-c843-4bf1-b1b7-2caeec7c2f51", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "f64", + "defaultValue": "", + "tooltip": "", + "id": "41574a05-652e-4d95-9aaf-efa8a350c62e" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "F": "1.7976931348623157E308" + }, + "type": "F64" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -150, + "y": 630 + }, + "dragging": false, + "draggable": true + }, + { + "width": 300, + "height": 120, + "selected": false, + "id": "c609b42d-4eb5-46ad-98f2-6fa880fcf705", + "type": "native", + "position": { + "x": -1020, + "y": 690 + }, + "style": { + "height": 120, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "b0b8e3d5-fa83-427a-b43d-c647f0223952", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "bool", + "defaultValue": "", + "tooltip": "", + "id": "dde483dc-24cd-4c45-a4a9-d379bfff2baf" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "B": false + }, + "type": "BoolFalse" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "positionAbsolute": { + "x": -1020, + "y": 690 + }, + "dragging": false + } + ], + "edges": [], + "uuid": "5220dd48-702e-4845-9ec1-2f3fe7e744bd", + "network": "devnet", + "updated_at": "2023-01-11T14:14:44.416931", + "lastest_flow_run_id": null, + "environment": null, + "current_rpc": null, + "custom_rpc": null + }, + "bookmarks": [] +} diff --git a/test_files/file_upload.json b/test_files/file_upload.json new file mode 100644 index 00000000..7ec92d3b --- /dev/null +++ b/test_files/file_upload.json @@ -0,0 +1,338 @@ +{ + "flow": { + "id": 111, + "user_id": "ad3dedf8-7b31-4baf-85a2-c336db90ad7f", + "name": "file_upload", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 80 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2022-11-02", + "parent_flow": null, + "viewport": { + "x": 1052.7430352078377, + "y": -17.95044120031298, + "zoom": 1.042465760841123 + }, + "nodes": [ + { + "width": 250, + "height": 200, + "selected": false, + "id": "afd3cef5-1d56-444e-bba1-31d6885de41f", + "type": "native", + "style": { + "height": 200, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "b0404679-20e6-4f24-b1b1-326a49fd9a53", + "unique_node_id": "arweave_file_upload.0.1", + "node_id": "arweave_file_upload", + "name": "Arweave File Upload", + "sources": [ + { + "name": "file_url", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "e431b4e9-80a7-447f-8a9e-b8a6c83c9052" + } + ], + "targets": [ + { + "name": "fee_payer", + "type_bounds": ["keypair", "string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": true, + "id": "719ebf09-e1e2-4508-87e4-784b623f76b0" + }, + { + "name": "file_path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "260d8474-0049-4c2c-b398-4cbaf8824e1e" + }, + { + "name": "fund_bundlr", + "type_bounds": ["bool"], + "required": true, + "defaultValue": true, + "tooltip": "", + "passthrough": false, + "id": "66e196c5-8b30-4764-b02a-854d0e0fe2aa" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 120 + } + } + }, + "position": { + "x": 45, + "y": 225 + }, + "positionAbsolute": { + "x": 45, + "y": 225 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "a364764a-a92a-46b1-a171-cf49807dfa69", + "type": "native", + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "4a09f605-3bf3-49f2-904b-9e672157626f", + "unique_node_id": null, + "node_id": "foreach", + "name": "Foreach", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "eb503929-71dd-4f93-9aad-a9e323e2e137" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "c0722160-34b4-4558-828c-c6625237c336" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "position": { + "x": -450, + "y": 300 + }, + "positionAbsolute": { + "x": -450, + "y": 300 + }, + "dragging": false + }, + { + "width": 301, + "height": 200, + "selected": false, + "id": "df8e0259-75f4-4ffe-b98b-50d72f2b50c1", + "type": "native", + "style": { + "height": 200, + "width": 301, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "e3ed992f-3348-45ab-ac32-9c70f94368e2", + "unique_node_id": "const.0.1", + "node_id": "const", + "name": "Const", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "e3c51d79-c4f7-4e11-8389-82c58193ab92", + "value": "" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "S": "56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu" + }, + "type": "String" + } + } + }, + "position": { + "x": -495, + "y": 90 + }, + "positionAbsolute": { + "x": -495, + "y": 90 + }, + "dragging": false + }, + { + "width": 350, + "height": 200, + "selected": false, + "id": "24b18410-1f44-42d0-813d-bd00b62e5370", + "type": "native", + "style": { + "height": 200, + "width": 350, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "393924ae-103e-439d-a1d6-f2b6a7dfc016", + "unique_node_id": "const.0.1", + "node_id": "const", + "name": "Const", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "7e9703bf-1531-4edb-9c1b-43094d4be124" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": [ + "https://hyjboblkjeevkzaqsyxe.supabase.co/storage/v1/object/public/flow-files/24b18410-1f44-42d0-813d-bd00b62e5370/file_upload.json", + "https://hyjboblkjeevkzaqsyxe.supabase.co/storage/v1/object/public/flow-files/24b18410-1f44-42d0-813d-bd00b62e5370/subflow.json" + ], + "type": "File" + } + } + }, + "position": { + "x": -855, + "y": 300 + }, + "positionAbsolute": { + "x": -855, + "y": 300 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "a364764a-a92a-46b1-a171-cf49807dfa69", + "sourceHandle": "eb503929-71dd-4f93-9aad-a9e323e2e137", + "target": "afd3cef5-1d56-444e-bba1-31d6885de41f", + "targetHandle": "260d8474-0049-4c2c-b398-4cbaf8824e1e", + "id": "reactflow__edge-a364764a-a92a-46b1-a171-cf49807dfa69eb503929-71dd-4f93-9aad-a9e323e2e137-afd3cef5-1d56-444e-bba1-31d6885de41f260d8474-0049-4c2c-b398-4cbaf8824e1e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "df8e0259-75f4-4ffe-b98b-50d72f2b50c1", + "sourceHandle": "e3c51d79-c4f7-4e11-8389-82c58193ab92", + "target": "afd3cef5-1d56-444e-bba1-31d6885de41f", + "targetHandle": "719ebf09-e1e2-4508-87e4-784b623f76b0", + "id": "reactflow__edge-df8e0259-75f4-4ffe-b98b-50d72f2b50c1e3c51d79-c4f7-4e11-8389-82c58193ab92-afd3cef5-1d56-444e-bba1-31d6885de41f719ebf09-e1e2-4508-87e4-784b623f76b0" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "24b18410-1f44-42d0-813d-bd00b62e5370", + "sourceHandle": "7e9703bf-1531-4edb-9c1b-43094d4be124", + "target": "a364764a-a92a-46b1-a171-cf49807dfa69", + "targetHandle": "c0722160-34b4-4558-828c-c6625237c336", + "id": "reactflow__edge-24b18410-1f44-42d0-813d-bd00b62e53707e9703bf-1531-4edb-9c1b-43094d4be124-a364764a-a92a-46b1-a171-cf49807dfa69c0722160-34b4-4558-828c-c6625237c336" + } + ], + "uuid": "cacedd35-ab09-4b22-aa9e-f1fb63371016", + "network": "devnet", + "updated_at": "2022-11-02T11:45:38.041142", + "lastest_flow_run_id": null + }, + "bookmarks": [] +} diff --git a/test_files/foreach.json b/test_files/foreach.json new file mode 100644 index 00000000..686d5866 --- /dev/null +++ b/test_files/foreach.json @@ -0,0 +1,688 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "AmuseParched", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 73.90167364016736 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-01-11", + "parent_flow": null, + "viewport": { + "x": -28.84972016863776, + "y": 399.0374381179979, + "zoom": 0.6643429070482563 + }, + "nodes": [ + { + "width": 300, + "height": 180, + "selected": false, + "id": "a0156fd4-631e-49cd-a185-9eb4512ad1ea", + "type": "native", + "position": { + "x": 90, + "y": -330 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "aca02f1a-41d6-47fc-a8d3-91189763d228", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "2a391ca2-af6b-4e4b-ae4d-cd2fdee3e1e7" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": "[\n{\n\"s\": \"letter advice cage absurd amount doctor acoustic avoid letter advice cage above\",\n\"p\": \"Hunter1!\"\n},\n{\"s\": \"letter advice cage absurd amount doctor acoustic avoid letter advice cage above\",\n\"p\": \"Hunter2!\"\n},\n{\n\"s\": \"select ensure paddle panic hole install math call zero rely puppy exist\",\n\"p\": \"password\"\n}\n]", + "type": "JSON" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "dragging": false, + "draggable": true, + "positionAbsolute": { + "x": 90, + "y": -330 + } + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "d9caf81c-2954-4a73-a781-de16a7d95993", + "type": "native", + "position": { + "x": 465, + "y": -330 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "048d782c-3806-4c07-8a1f-51e7725008a1", + "unique_node_id": "foreach.0.1", + "node_id": "foreach", + "version": "0.1", + "description": "Loop over elements of an array", + "name": "Foreach", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "element", + "type": "json", + "defaultValue": null, + "tooltip": "", + "id": "bcb28f7a-fe7d-4027-9aad-22cbb7ac72f3" + } + ], + "targets": [ + { + "name": "array", + "type_bounds": ["json"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "705bf70b-d6b5-447b-97ad-4a0cd490e955" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "Foreach", + "properties": { + "array": { + "title": "array", + "type": "string", + "default": "[]" + } + } + }, + "ui_schema": { + "array": { + "ui:widget": "textarea" + }, + "ui:order": ["array"] + }, + "form_data": { + "array": "[]" + }, + "extra": { + "supabase_id": 302 + } + } + }, + "positionAbsolute": { + "x": 465, + "y": -330 + }, + "dragging": false + }, + { + "width": 250, + "height": 150, + "selected": false, + "id": "699ddb59-c989-450f-8861-abcb045c38a3", + "type": "native", + "position": { + "x": 795, + "y": -465 + }, + "style": { + "height": 150, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "08ae943f-d804-45b8-b911-511e9b807c62", + "unique_node_id": "json_extract.0.1", + "node_id": "json_extract", + "version": "0.1", + "description": "Extracts a field from a JSON", + "name": "Json Extract", + "backgroundColorDark": "#000000", + "backgroundColor": "#ffd9b3", + "sources": [ + { + "name": "value", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "08fa238c-33f8-485c-96eb-bf3912e72ec2" + }, + { + "name": "trimmed_json", + "type": "json", + "defaultValue": "", + "tooltip": "", + "id": "b2df7e74-6d29-40ef-bd46-9e51ab3abb99" + } + ], + "targets": [ + { + "name": "json_input", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "57cf8aa6-e2a7-4390-ad3f-32006143a893" + }, + { + "name": "field_path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "e.g. /data/records/0/fields/url to select the url field value\nnote the /0/ is equivalent to [0], to select the first index in an array", + "passthrough": false, + "id": "35c9dd67-28e0-4ae0-abaa-3d0ca7b5dea3" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "JSON Extract", + "properties": { + "json_input": { + "title": "JSON Input", + "type": "string" + }, + "field_path": { + "title": "Field Path", + "type": "string" + } + } + }, + "ui_schema": { + "json_input": { + "ui:widget": "textarea" + }, + "ui:order": ["json_input", "field_path"] + }, + "form_data": { + "field_path": "s" + }, + "extra": { + "supabase_id": 278 + } + } + }, + "positionAbsolute": { + "x": 795, + "y": -465 + }, + "dragging": false + }, + { + "width": 250, + "height": 150, + "selected": false, + "id": "a515c6c0-05c5-47ff-888c-affe94f1ffae", + "type": "native", + "position": { + "x": 795, + "y": -285 + }, + "style": { + "height": 150, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "4e12d377-5181-4b6e-8ddc-16284af508cc", + "unique_node_id": "json_extract.0.1", + "node_id": "json_extract", + "version": "0.1", + "description": "Extracts a field from a JSON", + "name": "Json Extract", + "backgroundColorDark": "#000000", + "backgroundColor": "#ffd9b3", + "sources": [ + { + "name": "value", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "9efb14a8-9395-4e9e-a39b-f34f1b0ecf71" + }, + { + "name": "trimmed_json", + "type": "json", + "defaultValue": "", + "tooltip": "", + "id": "6a2387be-a4ec-4abc-a2f3-8730ffaa23e9" + } + ], + "targets": [ + { + "name": "json_input", + "type_bounds": ["json"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "0878eece-3b6b-4331-b285-ff8340c569ba" + }, + { + "name": "field_path", + "type_bounds": ["string"], + "required": true, + "defaultValue": "", + "tooltip": "e.g. /data/records/0/fields/url to select the url field value\nnote the /0/ is equivalent to [0], to select the first index in an array", + "passthrough": false, + "id": "62a160db-f1b5-4acf-ba58-69c167e446b1" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "title": "JSON Extract", + "properties": { + "json_input": { + "title": "JSON Input", + "type": "string" + }, + "field_path": { + "title": "Field Path", + "type": "string" + } + } + }, + "ui_schema": { + "json_input": { + "ui:widget": "textarea" + }, + "ui:order": ["json_input", "field_path"] + }, + "form_data": { + "field_path": "p" + }, + "extra": { + "supabase_id": 278 + } + } + }, + "positionAbsolute": { + "x": 795, + "y": -285 + }, + "dragging": false + }, + { + "width": 250, + "height": 200, + "selected": false, + "id": "56c6c8c4-fba2-49de-b362-9fff0549c0bb", + "type": "native", + "position": { + "x": 1125, + "y": -405 + }, + "style": { + "height": 200, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "107a3f4a-9223-40fa-8d05-b552cb523e1c", + "unique_node_id": "generate_keypair.0.1", + "node_id": "generate_keypair", + "version": "0.1", + "description": "Generate or load a keypair and it's pubkey.\n\nWill generate a random keypair every run if no inputs are provided. This is useful for testing purpose.", + "name": "Generate Keypair", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "pubkey", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "e337cd7e-525e-455f-a072-8994c71cb599" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": null, + "tooltip": "", + "id": "573317f8-9a41-4545-8d91-b0a2ce986118" + } + ], + "targets": [ + { + "name": "seed", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "12 word BIP39 mnemonic seed phrase", + "passthrough": false, + "id": "c04c5a6a-2c5b-4683-bd7f-823b9c9eadd5" + }, + { + "name": "private_key", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "Load using a base 58 string, ignores seed/passphrase", + "passthrough": false, + "id": "83aaaafc-a049-4adc-abe7-8b1549121277" + }, + { + "name": "passphrase", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "1f9354e0-453b-4d18-8182-439dcca1d7cf" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 110 + } + } + }, + "positionAbsolute": { + "x": 1125, + "y": -405 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "3378ea08-effa-44c5-94bf-ffe4937209c1", + "type": "native", + "position": { + "x": 1455, + "y": -360 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "655a8187-c823-4e7e-bb9d-d1781910fb14", + "unique_node_id": "collect.0.1", + "node_id": "collect", + "version": "0.1", + "description": "Collect inputs into an array", + "name": "Collect", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "array", + "type": "free", + "defaultValue": null, + "tooltip": "", + "id": "f1c82628-77d6-4c80-8ded-4cdae3695dd5" + } + ], + "targets": [ + { + "name": "element", + "type_bounds": ["free"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "415dbba2-5e0a-451e-bd55-f82dbf8461f0" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 303 + } + } + }, + "positionAbsolute": { + "x": 1455, + "y": -360 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "5e158532-3db5-4498-9cf8-6d0c674bdd08", + "type": "native", + "position": { + "x": 1770, + "y": -360 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "25a4a7cb-d856-46dd-95e3-fd6080d76086", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "keypairs", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "d56ab632-1213-44a8-801e-3efa521cb4ed" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "keypairs" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 1770, + "y": -360 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "a0156fd4-631e-49cd-a185-9eb4512ad1ea", + "sourceHandle": "2a391ca2-af6b-4e4b-ae4d-cd2fdee3e1e7", + "target": "d9caf81c-2954-4a73-a781-de16a7d95993", + "targetHandle": "705bf70b-d6b5-447b-97ad-4a0cd490e955", + "id": "reactflow__edge-a0156fd4-631e-49cd-a185-9eb4512ad1ea2a391ca2-af6b-4e4b-ae4d-cd2fdee3e1e7-d9caf81c-2954-4a73-a781-de16a7d95993705bf70b-d6b5-447b-97ad-4a0cd490e955" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "d9caf81c-2954-4a73-a781-de16a7d95993", + "sourceHandle": "bcb28f7a-fe7d-4027-9aad-22cbb7ac72f3", + "target": "699ddb59-c989-450f-8861-abcb045c38a3", + "targetHandle": "57cf8aa6-e2a7-4390-ad3f-32006143a893", + "id": "reactflow__edge-d9caf81c-2954-4a73-a781-de16a7d95993bcb28f7a-fe7d-4027-9aad-22cbb7ac72f3-699ddb59-c989-450f-8861-abcb045c38a357cf8aa6-e2a7-4390-ad3f-32006143a893" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "d9caf81c-2954-4a73-a781-de16a7d95993", + "sourceHandle": "bcb28f7a-fe7d-4027-9aad-22cbb7ac72f3", + "target": "a515c6c0-05c5-47ff-888c-affe94f1ffae", + "targetHandle": "0878eece-3b6b-4331-b285-ff8340c569ba", + "id": "reactflow__edge-d9caf81c-2954-4a73-a781-de16a7d95993bcb28f7a-fe7d-4027-9aad-22cbb7ac72f3-a515c6c0-05c5-47ff-888c-affe94f1ffae0878eece-3b6b-4331-b285-ff8340c569ba" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "699ddb59-c989-450f-8861-abcb045c38a3", + "sourceHandle": "08fa238c-33f8-485c-96eb-bf3912e72ec2", + "target": "56c6c8c4-fba2-49de-b362-9fff0549c0bb", + "targetHandle": "c04c5a6a-2c5b-4683-bd7f-823b9c9eadd5", + "id": "reactflow__edge-699ddb59-c989-450f-8861-abcb045c38a308fa238c-33f8-485c-96eb-bf3912e72ec2-56c6c8c4-fba2-49de-b362-9fff0549c0bbc04c5a6a-2c5b-4683-bd7f-823b9c9eadd5" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "a515c6c0-05c5-47ff-888c-affe94f1ffae", + "sourceHandle": "9efb14a8-9395-4e9e-a39b-f34f1b0ecf71", + "target": "56c6c8c4-fba2-49de-b362-9fff0549c0bb", + "targetHandle": "1f9354e0-453b-4d18-8182-439dcca1d7cf", + "id": "reactflow__edge-a515c6c0-05c5-47ff-888c-affe94f1ffae9efb14a8-9395-4e9e-a39b-f34f1b0ecf71-56c6c8c4-fba2-49de-b362-9fff0549c0bb1f9354e0-453b-4d18-8182-439dcca1d7cf" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "56c6c8c4-fba2-49de-b362-9fff0549c0bb", + "sourceHandle": "573317f8-9a41-4545-8d91-b0a2ce986118", + "target": "3378ea08-effa-44c5-94bf-ffe4937209c1", + "targetHandle": "415dbba2-5e0a-451e-bd55-f82dbf8461f0", + "id": "reactflow__edge-56c6c8c4-fba2-49de-b362-9fff0549c0bb573317f8-9a41-4545-8d91-b0a2ce986118-3378ea08-effa-44c5-94bf-ffe4937209c1415dbba2-5e0a-451e-bd55-f82dbf8461f0" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "3378ea08-effa-44c5-94bf-ffe4937209c1", + "sourceHandle": "f1c82628-77d6-4c80-8ded-4cdae3695dd5", + "target": "5e158532-3db5-4498-9cf8-6d0c674bdd08", + "targetHandle": "d56ab632-1213-44a8-801e-3efa521cb4ed", + "id": "reactflow__edge-3378ea08-effa-44c5-94bf-ffe4937209c1f1c82628-77d6-4c80-8ded-4cdae3695dd5-5e158532-3db5-4498-9cf8-6d0c674bdd08d56ab632-1213-44a8-801e-3efa521cb4ed" + } + ], + "uuid": "29dd8255-c54f-4dcb-8b32-78c344191fa4", + "network": "devnet", + "updated_at": "2023-01-11T12:25:04.633772", + "lastest_flow_run_id": "95f98b3b-1d26-49e2-b51e-3ce123e80b2a", + "environment": null, + "current_rpc": null, + "custom_rpc": null + }, + "bookmarks": [] +} diff --git a/test_files/generate_keypair.json b/test_files/generate_keypair.json new file mode 100644 index 00000000..44b7181e --- /dev/null +++ b/test_files/generate_keypair.json @@ -0,0 +1,531 @@ +{ + "flow": { + "id": 0, + "user_id": "3b93d159-b9d1-4230-ad4b-e498d7f1b796", + "name": "generate_keypair", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 80 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2023-01-11", + "parent_flow": null, + "viewport": { + "x": 340.57708035867427, + "y": 240.4346397026846, + "zoom": 1.032398535483242 + }, + "nodes": [ + { + "width": 300, + "height": 180, + "selected": false, + "id": "ab76fedd-ed20-43c3-b356-a6f38a8f1f4f", + "type": "native", + "position": { + "x": -480, + "y": 75 + }, + "style": { + "height": 180, + "width": 300, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "8c64a0c7-10bc-4120-a0a1-ef6afa361217", + "unique_node_id": "const.0.1", + "node_id": "const", + "version": "0.1", + "description": "", + "name": "Const", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "", + "tooltip": "", + "id": "2770cfa3-e1f3-4ccb-a6c0-028f6209573c" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "value": { + "S": "letter advice cage absurd amount doctor acoustic avoid letter advice cage above" + }, + "type": "String" + }, + "extra": { + "supabase_id": 117 + } + } + }, + "draggable": true, + "dragging": false, + "positionAbsolute": { + "x": -480, + "y": 75 + } + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "63a087dd-0406-4678-ad24-fcf5e1415d58", + "type": "native", + "position": { + "x": -435, + "y": 315 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "615eb36e-66ac-49e8-b3de-aa688aa0dd7e", + "unique_node_id": "flow_input.0.1", + "node_id": "flow_input", + "version": "0.1", + "description": "", + "name": "Flow Input", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [ + { + "name": "password", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "737c7b47-2b93-41d1-9d9d-49039c1a68e6" + } + ], + "targets": [], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "" + }, + "form_label": { + "type": "string", + "title": "password" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "password", + "form_label": "Hunter1!" + }, + "extra": { + "supabase_id": 138 + } + } + }, + "positionAbsolute": { + "x": -435, + "y": 315 + }, + "dragging": false + }, + { + "width": 250, + "height": 200, + "selected": false, + "id": "710dba45-ee24-4252-8d37-4feccb80659b", + "type": "native", + "position": { + "x": -135, + "y": 165 + }, + "style": { + "height": 200, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "4f8f9327-46bb-4377-8d46-cbb12cfd8e33", + "unique_node_id": "generate_keypair.0.1", + "node_id": "generate_keypair", + "version": "0.1", + "description": "Generate or load a keypair and it's pubkey.\n\nWill generate a random keypair every run if no inputs are provided. This is useful for testing purpose.", + "name": "Generate Keypair", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "pubkey", + "type": "pubkey", + "defaultValue": null, + "tooltip": "", + "id": "01e3e80b-4247-4296-a00d-fe42b1589802" + }, + { + "name": "keypair", + "type": "keypair", + "defaultValue": null, + "tooltip": "", + "id": "14c1e090-c304-4021-98f0-6ea13825a1ab" + } + ], + "targets": [ + { + "name": "seed", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "12 word BIP39 mnemonic seed phrase", + "passthrough": false, + "id": "e9a16ec4-f45c-4600-b55b-e85ca3006c99" + }, + { + "name": "private_key", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "Load using a base 58 string, ignores seed/passphrase", + "passthrough": false, + "id": "5a390ab6-9544-4d62-ba8f-1da6f528786d" + }, + { + "name": "passphrase", + "type_bounds": ["string"], + "required": false, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "700375cc-69fd-41c2-a58b-3328dc44c09e" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 110 + } + } + }, + "positionAbsolute": { + "x": -135, + "y": 165 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "e8891849-db4b-4d90-aa20-3e324600779c", + "type": "native", + "position": { + "x": 150, + "y": 165 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "72ad5888-9b69-4d28-a4df-7631fb2150dd", + "unique_node_id": "get_balance.0.1", + "node_id": "get_balance", + "version": "0.1", + "description": "Get the balance of the account", + "name": "Get Balance", + "backgroundColorDark": "#000000", + "backgroundColor": "#fff", + "sources": [ + { + "name": "balance", + "type": "u64", + "defaultValue": null, + "tooltip": "", + "id": "b7235930-7af7-4633-938b-59dd1f6be6a8" + } + ], + "targets": [ + { + "name": "pubkey", + "type_bounds": ["pubkey", "keypair", "string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "a97488db-10a5-4e25-bce5-5b9499aada18" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 112 + } + } + }, + "positionAbsolute": { + "x": 150, + "y": 165 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "87cdf699-fa1f-4cea-a91c-8752ec738326", + "type": "native", + "position": { + "x": 435, + "y": 165 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "a4b4518c-3253-446b-b951-c4581c48d4a0", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "balance", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "34a2873b-f9fc-4aee-bd46-1b001c9359f3" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "balance" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 435, + "y": 165 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "3efdad71-4231-41e2-9d80-a7f819876e48", + "type": "native", + "position": { + "x": 150, + "y": 270 + }, + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "29f94810-97a8-438e-9076-a7292f49a11b", + "unique_node_id": "flow_output.0.1", + "node_id": "flow_output", + "version": "0.1", + "description": "", + "name": "Flow Output", + "backgroundColorDark": "#000000", + "backgroundColor": "#f2fcff", + "sources": [], + "targets": [ + { + "name": "key", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "0a04fa35-db60-4e38-a353-c1bffc212ff8" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "key" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "positionAbsolute": { + "x": 150, + "y": 270 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "ab76fedd-ed20-43c3-b356-a6f38a8f1f4f", + "sourceHandle": "2770cfa3-e1f3-4ccb-a6c0-028f6209573c", + "target": "710dba45-ee24-4252-8d37-4feccb80659b", + "targetHandle": "e9a16ec4-f45c-4600-b55b-e85ca3006c99", + "id": "reactflow__edge-ab76fedd-ed20-43c3-b356-a6f38a8f1f4f2770cfa3-e1f3-4ccb-a6c0-028f6209573c-710dba45-ee24-4252-8d37-4feccb80659be9a16ec4-f45c-4600-b55b-e85ca3006c99" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "63a087dd-0406-4678-ad24-fcf5e1415d58", + "sourceHandle": "737c7b47-2b93-41d1-9d9d-49039c1a68e6", + "target": "710dba45-ee24-4252-8d37-4feccb80659b", + "targetHandle": "700375cc-69fd-41c2-a58b-3328dc44c09e", + "id": "reactflow__edge-63a087dd-0406-4678-ad24-fcf5e1415d58737c7b47-2b93-41d1-9d9d-49039c1a68e6-710dba45-ee24-4252-8d37-4feccb80659b700375cc-69fd-41c2-a58b-3328dc44c09e" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "710dba45-ee24-4252-8d37-4feccb80659b", + "sourceHandle": "01e3e80b-4247-4296-a00d-fe42b1589802", + "target": "e8891849-db4b-4d90-aa20-3e324600779c", + "targetHandle": "a97488db-10a5-4e25-bce5-5b9499aada18", + "id": "reactflow__edge-710dba45-ee24-4252-8d37-4feccb80659b01e3e80b-4247-4296-a00d-fe42b1589802-e8891849-db4b-4d90-aa20-3e324600779ca97488db-10a5-4e25-bce5-5b9499aada18" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "e8891849-db4b-4d90-aa20-3e324600779c", + "sourceHandle": "b7235930-7af7-4633-938b-59dd1f6be6a8", + "target": "87cdf699-fa1f-4cea-a91c-8752ec738326", + "targetHandle": "34a2873b-f9fc-4aee-bd46-1b001c9359f3", + "id": "reactflow__edge-e8891849-db4b-4d90-aa20-3e324600779cb7235930-7af7-4633-938b-59dd1f6be6a8-87cdf699-fa1f-4cea-a91c-8752ec73832634a2873b-f9fc-4aee-bd46-1b001c9359f3" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "710dba45-ee24-4252-8d37-4feccb80659b", + "sourceHandle": "14c1e090-c304-4021-98f0-6ea13825a1ab", + "target": "3efdad71-4231-41e2-9d80-a7f819876e48", + "targetHandle": "0a04fa35-db60-4e38-a353-c1bffc212ff8", + "id": "reactflow__edge-710dba45-ee24-4252-8d37-4feccb80659b14c1e090-c304-4021-98f0-6ea13825a1ab-3efdad71-4231-41e2-9d80-a7f819876e480a04fa35-db60-4e38-a353-c1bffc212ff8" + } + ], + "uuid": "9b4d23da-1354-4805-8d56-54e770885189", + "network": "devnet", + "updated_at": "2023-01-11T13:12:01.174599", + "lastest_flow_run_id": "cce0f6b7-0eb6-488b-9a3e-e7d6aabaf378", + "environment": null, + "current_rpc": null, + "custom_rpc": null + }, + "bookmarks": [] +} diff --git a/test_files/interflow_simple.json b/test_files/interflow_simple.json new file mode 100644 index 00000000..046b8129 --- /dev/null +++ b/test_files/interflow_simple.json @@ -0,0 +1,152 @@ +{ + "flow": { + "id": 95, + "user_id": "ad3dedf8-7b31-4baf-85a2-c336db90ad7f", + "name": "interflow_simple", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 72.22803347280335 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2022-10-29", + "parent_flow": null, + "viewport": { + "x": 509.9395723573103, + "y": -43.31380762122933, + "zoom": 1.2869892473012707 + }, + "nodes": [ + { + "width": 400, + "height": 145, + "selected": false, + "id": "eec5911d-962b-4cab-865e-d480ac8e66ad", + "type": "native", + "style": { + "height": 145, + "width": 400, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "37d42c3a-4e5c-4739-b87c-d656f82ecf31", + "node_id": "interflow", + "name": "Interflow", + "sources": [], + "targets": [ + { + "name": "flow", + "type_bounds": ["string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "0ae558e5-96c1-49da-8bf5-5e19f15da898" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": { + "id": 94 + } + } + }, + "position": { + "x": -15, + "y": 240 + }, + "positionAbsolute": { + "x": -15, + "y": 240 + }, + "dragging": false + }, + { + "width": 301, + "height": 200, + "selected": false, + "id": "2fbcd854-6a92-4362-a634-3acc636a5327", + "type": "native", + "style": { + "height": 200, + "width": 301, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "703cbb24-7439-495c-a121-5f6626d07947", + "node_id": "const", + "name": "Const", + "sources": [ + { + "name": "Source", + "type": "string", + "defaultValue": "56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu", + "tooltip": "", + "id": "0469bf25-b896-4f7f-93cd-8a526a1024b1", + "value": "56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu" + } + ], + "targets": [], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": "56Ngo8EY5ZWmYKDZAmKYcUf2y2LZVRSMMnptGp9JtQuSZHyU3Pwhhkmj5YVf89VTQZqrzkabhybWdWwJWCa74aYu" + } + }, + "position": { + "x": -390, + "y": 240 + }, + "positionAbsolute": { + "x": -390, + "y": 240 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "2fbcd854-6a92-4362-a634-3acc636a5327", + "sourceHandle": "0469bf25-b896-4f7f-93cd-8a526a1024b1", + "target": "eec5911d-962b-4cab-865e-d480ac8e66ad", + "targetHandle": "7ef80a10-7a88-4fa1-8d6c-77a7ab3da5c2", + "id": "reactflow__edge-2fbcd854-6a92-4362-a634-3acc636a53270469bf25-b896-4f7f-93cd-8a526a1024b1-eec5911d-962b-4cab-865e-d480ac8e66ad7ef80a10-7a88-4fa1-8d6c-77a7ab3da5c2" + } + ], + "uuid": "5e384ce6-0a56-479e-bfd0-5c5ff715002c", + "network": "devnet", + "updated_at": "2022-10-31T06:20:58.496357", + "lastest_flow_run_id": null + }, + "bookmarks": [] +} diff --git a/test_files/subflow.json b/test_files/subflow.json new file mode 100644 index 00000000..7c3ec0e2 --- /dev/null +++ b/test_files/subflow.json @@ -0,0 +1,259 @@ +{ + "flow": { + "id": 94, + "user_id": "ad3dedf8-7b31-4baf-85a2-c336db90ad7f", + "name": "subflow", + "mosaic": { + "direction": "row", + "first": "SidePanel", + "second": { + "direction": "row", + "first": "Flow", + "second": { + "direction": "column", + "first": "PropertyPanel", + "second": "", + "splitPercentage": 100 + }, + "splitPercentage": 73.59882005899705 + }, + "splitPercentage": 0, + "prevSplitPercentage": 10 + }, + "isPublic": false, + "description": "Flow Description", + "tags": [], + "state": "edit", + "startFlowTime": null, + "created_at": "2022-10-29", + "parent_flow": null, + "viewport": { + "x": -263.80346089400143, + "y": 28.892705212665305, + "zoom": 1.3986160827384624 + }, + "nodes": [ + { + "width": 250, + "height": 100, + "selected": false, + "id": "9b235db3-084e-454a-b05e-38a87b93ee15", + "type": "native", + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "db65cd84-6a44-4b0b-859a-b75c0e92f69d", + "node_id": "flow_input", + "name": "Flow Input", + "sources": [ + { + "name": "", + "type": "free", + "defaultValue": "", + "tooltip": "", + "id": "7ef80a10-7a88-4fa1-8d6c-77a7ab3da5c2" + } + ], + "targets": [], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Label the input parameter", + "default": "" + }, + "form_label": { + "type": "string", + "title": "pubkey" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "pubkey" + }, + "extra": { + "supabase_id": 138 + } + } + }, + "position": { + "x": 225, + "y": 210 + }, + "positionAbsolute": { + "x": 225, + "y": 210 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "568aba30-f22d-4247-91ba-6623a8a5490b", + "type": "native", + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "576fdf9a-eea7-4de9-b3a6-9e7712f168c3", + "node_id": "get_balance", + "name": "Get Balance", + "sources": [ + { + "name": "balance", + "type": "u64", + "defaultValue": null, + "tooltip": "", + "id": "c2f88f3e-ab6b-4da8-8a61-6085db89f0bd" + } + ], + "targets": [ + { + "name": "pubkey", + "type_bounds": ["pubkey", "keypair", "string"], + "required": true, + "defaultValue": null, + "tooltip": "", + "passthrough": false, + "id": "bea1e69a-1d94-442f-85be-4c9aaccac2fc" + } + ], + "targets_form": { + "json_schema": {}, + "ui_schema": {}, + "form_data": {}, + "extra": { + "supabase_id": 112 + } + } + }, + "position": { + "x": 540, + "y": 210 + }, + "positionAbsolute": { + "x": 540, + "y": 210 + }, + "dragging": false + }, + { + "width": 250, + "height": 100, + "selected": false, + "id": "6561d475-6b65-4297-aef6-53a76185f156", + "type": "native", + "style": { + "height": 100, + "width": 250, + "backgroundColorDark": "#000000", + "backgroundColor": "transparent" + }, + "className": "", + "data": { + "className": "", + "type": "native", + "id": "96f4e22f-279e-4200-a7bb-ea011c4c737e", + "node_id": "flow_output", + "name": "Flow Output", + "sources": [], + "targets": [ + { + "name": "", + "type_bounds": ["free"], + "required": true, + "defaultValue": "", + "tooltip": "", + "passthrough": false, + "id": "0a95ba13-9ff8-4136-b492-0a9df3029a69" + } + ], + "targets_form": { + "json_schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "title": "Output Label", + "default": "" + } + } + }, + "ui_schema": { + "label": { + "ui:emptyValue": "" + } + }, + "form_data": { + "label": "balance" + }, + "extra": { + "supabase_id": 137 + } + } + }, + "position": { + "x": 840, + "y": 210 + }, + "positionAbsolute": { + "x": 840, + "y": 210 + }, + "dragging": false + } + ], + "edges": [ + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "9b235db3-084e-454a-b05e-38a87b93ee15", + "sourceHandle": "7ef80a10-7a88-4fa1-8d6c-77a7ab3da5c2", + "target": "568aba30-f22d-4247-91ba-6623a8a5490b", + "targetHandle": "bea1e69a-1d94-442f-85be-4c9aaccac2fc", + "id": "reactflow__edge-9b235db3-084e-454a-b05e-38a87b93ee157ef80a10-7a88-4fa1-8d6c-77a7ab3da5c2-568aba30-f22d-4247-91ba-6623a8a5490bbea1e69a-1d94-442f-85be-4c9aaccac2fc" + }, + { + "style": { + "stroke": "#fef08a", + "strokeWidth": 2 + }, + "type": "custom", + "source": "568aba30-f22d-4247-91ba-6623a8a5490b", + "sourceHandle": "c2f88f3e-ab6b-4da8-8a61-6085db89f0bd", + "target": "6561d475-6b65-4297-aef6-53a76185f156", + "targetHandle": "0a95ba13-9ff8-4136-b492-0a9df3029a69", + "id": "reactflow__edge-568aba30-f22d-4247-91ba-6623a8a5490bc2f88f3e-ab6b-4da8-8a61-6085db89f0bd-6561d475-6b65-4297-aef6-53a76185f1560a95ba13-9ff8-4136-b492-0a9df3029a69" + } + ], + "uuid": "15407ef3-a6ae-4775-b759-bd4f0d8f302e", + "network": "devnet", + "updated_at": "2022-10-29T08:54:12.254651", + "lastest_flow_run_id": null + }, + "bookmarks": [] +}