diff --git a/candid/swap.did b/candid/swap.did index d772cb8..01f88ee 100644 --- a/candid/swap.did +++ b/candid/swap.did @@ -31,6 +31,12 @@ type DerivedState = record { }; type DirectInvestment = record { buyer_principal : text }; type Err = record { description : opt text; error_type : opt int32 }; +type Err_1 = record { error_type : opt int32 }; +type Err_2 = record { + invalid_user_amount : opt InvalidUserAmount; + existing_ticket : opt Ticket; + error_type : int32; +}; type ErrorRefundIcpRequest = record { source_principal_id : opt principal }; type ErrorRefundIcpResponse = record { result : opt Result }; type FailedUpdate = record { @@ -38,13 +44,13 @@ type FailedUpdate = record { dapp_canister_id : opt principal; }; type FinalizeSwapResponse = record { + set_dapp_controllers_call_result : opt SetDappControllersCallResult; settle_community_fund_participation_result : opt SettleCommunityFundParticipationResult; error_message : opt text; - set_dapp_controllers_result : opt SetDappControllersCallResult; - sns_governance_normal_mode_enabled : opt SetModeCallResult; - sweep_icp : opt SweepResult; - sweep_sns : opt SweepResult; - create_neuron : opt SweepResult; + set_mode_call_result : opt SetModeCallResult; + sweep_icp_result : opt SweepResult; + claim_neuron_result : opt SweepResult; + sweep_sns_result : opt SweepResult; }; type GetBuyerStateRequest = record { principal_id : opt principal }; type GetBuyerStateResponse = record { buyer_state : opt BuyerState }; @@ -55,8 +61,10 @@ type GetDerivedStateResponse = record { }; type GetInitResponse = record { init : opt Init }; type GetLifecycleResponse = record { lifecycle : opt int32 }; +type GetOpenTicketResponse = record { result : opt Result_1 }; type GetStateResponse = record { swap : opt Swap; derived : opt DerivedState }; type GovernanceError = record { error_message : text; error_type : int32 }; +type Icrc1Account = record { owner : opt principal; subaccount : opt vec nat8 }; type Init = record { sns_root_canister_id : text; fallback_controller_principal_ids : vec text; @@ -67,6 +75,10 @@ type Init = record { sns_ledger_canister_id : text; sns_governance_canister_id : text; }; +type InvalidUserAmount = record { + min_amount_icp_e8s_included : nat64; + max_amount_icp_e8s_included : nat64; +}; type Investor = variant { CommunityFund : CfInvestment; Direct : DirectInvestment; @@ -76,7 +88,13 @@ type NeuronBasketConstructionParameters = record { dissolve_delay_interval_seconds : nat64; count : nat64; }; +type NewSaleTicketRequest = record { + subaccount : opt vec nat8; + amount_icp_e8s : nat64; +}; +type NewSaleTicketResponse = record { result : opt Result_2 }; type Ok = record { block_height : opt nat64 }; +type Ok_1 = record { ticket : opt Ticket }; type OpenRequest = record { cf_participants : vec CfParticipant; params : opt Params; @@ -92,12 +110,12 @@ type Params = record { max_participant_icp_e8s : nat64; min_icp_e8s : nat64; }; -type Possibility = variant { Ok : Response; Err : CanisterCallError }; -type Possibility_1 = variant { +type Possibility = variant { Ok : SetDappControllersResponse; Err : CanisterCallError; }; -type Possibility_2 = variant { Err : CanisterCallError }; +type Possibility_1 = variant { Ok : Response; Err : CanisterCallError }; +type Possibility_2 = variant { Ok : record {}; Err : CanisterCallError }; type RefreshBuyerTokensRequest = record { buyer : text }; type RefreshBuyerTokensResponse = record { icp_accepted_participation_e8s : nat64; @@ -105,14 +123,17 @@ type RefreshBuyerTokensResponse = record { }; type Response = record { governance_error : opt GovernanceError }; type Result = variant { Ok : Ok; Err : Err }; -type SetDappControllersCallResult = record { possibility : opt Possibility_1 }; +type Result_1 = variant { Ok : Ok_1; Err : Err_1 }; +type Result_2 = variant { Ok : Ok_1; Err : Err_2 }; +type SetDappControllersCallResult = record { possibility : opt Possibility }; type SetDappControllersResponse = record { failed_updates : vec FailedUpdate }; type SetModeCallResult = record { possibility : opt Possibility_2 }; type SettleCommunityFundParticipationResult = record { - possibility : opt Possibility; + possibility : opt Possibility_1; }; type SnsNeuronRecipe = record { sns : opt TransferableAmount; + claimed_status : opt int32; neuron_attributes : opt NeuronAttributes; investor : opt Investor; }; @@ -126,7 +147,19 @@ type Swap = record { params : opt Params; open_sns_token_swap_proposal_id : opt nat64; }; -type SweepResult = record { failure : nat32; skipped : nat32; success : nat32 }; +type SweepResult = record { + failure : nat32; + skipped : nat32; + invalid : nat32; + success : nat32; + global_failures : nat32; +}; +type Ticket = record { + creation_time : nat64; + ticket_id : nat64; + account : opt Icrc1Account; + amount_icp_e8s : nat64; +}; type TransferableAmount = record { transfer_start_timestamp_seconds : nat64; amount_e8s : nat64; @@ -141,7 +174,9 @@ service : (Init) -> { get_derived_state : (record {}) -> (GetDerivedStateResponse) query; get_init : (record {}) -> (GetInitResponse) query; get_lifecycle : (record {}) -> (GetLifecycleResponse) query; + get_open_ticket : (record {}) -> (GetOpenTicketResponse) query; get_state : (record {}) -> (GetStateResponse) query; + new_sale_ticket : (NewSaleTicketRequest) -> (NewSaleTicketResponse); open : (OpenRequest) -> (record {}); refresh_buyer_tokens : (RefreshBuyerTokensRequest) -> ( RefreshBuyerTokensResponse, diff --git a/src/commands/get_sale_ticket.rs b/src/commands/get_sale_ticket.rs new file mode 100644 index 0000000..2d54eb1 --- /dev/null +++ b/src/commands/get_sale_ticket.rs @@ -0,0 +1,58 @@ +use candid::Encode; +use clap::Parser; + +use crate::{ + commands::transfer::HexSubaccount, + lib::{ + signing::{sign_ingress_with_request_status_query, IngressWithRequestId}, + AnyhowResult, TargetCanister, + }, + SnsCanisterIds, +}; + +/// Get the sale ticket of the caller. If there is no open ticket yet, create a new ticket with specified arguments. +#[derive(Parser)] +pub struct GetSaleTicketOpts { + /// The amount of ICP tokens in e8s. + #[clap(long)] + amount_icp_e8s: u64, + + /// The subaccount of the account to create sale ticket. For example: e000d80101 + #[clap(long)] + subaccount: Option, +} + +// TODO: SDK-954 - use ic_sns_swap when it is available +#[derive(candid::CandidType, candid::Deserialize)] +struct NewSaleTicketRequest { + amount_icp_e8s: u64, + subaccount: Option<[u8; 32]>, +} + +#[derive(candid::CandidType, candid::Deserialize)] +struct GetOpenTicketArg {} + +pub fn exec( + pem: &str, + sns_canister_ids: &SnsCanisterIds, + opts: GetSaleTicketOpts, +) -> AnyhowResult> { + let req1 = sign_ingress_with_request_status_query( + pem, + "get_open_ticket", + Encode!(&GetOpenTicketArg {})?, + TargetCanister::Swap(sns_canister_ids.swap_canister_id.get().0), + )?; + + let message = NewSaleTicketRequest { + amount_icp_e8s: opts.amount_icp_e8s, + subaccount: opts.subaccount.map(|sub| sub.0), + }; + let req2 = sign_ingress_with_request_status_query( + pem, + "new_sale_ticket", + Encode!(&message)?, + TargetCanister::Swap(sns_canister_ids.swap_canister_id.get().0), + )?; + Ok(vec![req1, req2]) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 346d260..a17e28b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ use tokio::runtime::Runtime; mod account_balance; mod configure_dissolve_delay; mod generate; +mod get_sale_ticket; mod get_swap_refund; mod list_deployed_snses; mod make_proposal; @@ -49,6 +50,8 @@ pub enum Command { /// holders. MakeProposal(make_proposal::MakeProposalOpts), GetSwapRefund(get_swap_refund::GetSwapRefundOpts), + /// Get the sale ticket of the caller. If there is no open ticket yet, create a new ticket with specified arguments. + GetSaleTicket(get_sale_ticket::GetSaleTicketOpts), ListDeployedSnses(list_deployed_snses::ListDeployedSnsesOpts), /// Signs a ManageNeuron message to register a vote for a proposal. Registering a vote will /// update the ballot of the given proposal and could trigger followees to vote. When @@ -111,6 +114,11 @@ pub fn exec( let canister_ids = require_canister_ids(sns_canister_ids)?; get_swap_refund::exec(&pem, &canister_ids, opts).and_then(|out| print_vec(qr, &out)) } + Command::GetSaleTicket(opts) => { + let pem = require_pem(private_key_pem)?; + let canister_ids = require_canister_ids(sns_canister_ids)?; + get_sale_ticket::exec(&pem, &canister_ids, opts).and_then(|out| print_vec(qr, &out)) + } Command::ListDeployedSnses(opts) => { runtime.block_on(async { list_deployed_snses::exec(opts).await }) } diff --git a/src/commands/send.rs b/src/commands/send.rs index 189d2b7..57e7808 100644 --- a/src/commands/send.rs +++ b/src/commands/send.rs @@ -146,6 +146,8 @@ enum SupportedResponse { IcpTransfer, RefreshBuyerTokens, GetSnsCanistersSummary, + GetOpenTicket, + NewSaleTicket, } impl FromStr for SupportedResponse { @@ -160,6 +162,8 @@ impl FromStr for SupportedResponse { "send_dfx" => Ok(SupportedResponse::IcpTransfer), "refresh_buyer_tokens" => Ok(SupportedResponse::RefreshBuyerTokens), "get_sns_canisters_summary" => Ok(SupportedResponse::GetSnsCanistersSummary), + "get_open_ticket" => Ok(SupportedResponse::GetOpenTicket), + "new_sale_ticket" => Ok(SupportedResponse::NewSaleTicket), unsupported_response => Err(anyhow!( "{} is not a supported response", unsupported_response @@ -200,7 +204,23 @@ fn print_response(blob: Vec, method_name: String) -> AnyhowResult { let response = Decode!(blob.as_slice(), GetSnsCanistersSummaryResponse)?; println!("Response: {:#?\n}", response); } + SupportedResponse::GetOpenTicket => { + let response = Decode!(blob.as_slice(), GetOpenTicketResponse)?; + println!("Response: {:#?\n}", response); + } + SupportedResponse::NewSaleTicket => { + let response = Decode!(blob.as_slice(), NewSaleTicketResponse)?; + println!("Response: {:#?\n}", response); + } } Ok(()) } + +// TODO: SDK-954 - use ic_sns_swap when it is available +#[derive(Debug, candid::CandidType, candid::Deserialize)] +struct GetOpenTicketResponse {} + +// TODO: SDK-954 - use ic_sns_swap when it is available +#[derive(Debug, candid::CandidType, candid::Deserialize)] +struct NewSaleTicketResponse {} diff --git a/tests/commands/get-sale-ticket.sh b/tests/commands/get-sale-ticket.sh new file mode 100644 index 0000000..5f5a238 --- /dev/null +++ b/tests/commands/get-sale-ticket.sh @@ -0,0 +1 @@ +${CARGO_TARGET_DIR:-../target}/debug/sns-quill --canister-ids-file ./canister_ids.json --pem-file - get-sale-ticket --amount-icp-e8s 100000 --subaccount e000d80101 | ${CARGO_TARGET_DIR:-../target}/debug/sns-quill send --dry-run - diff --git a/tests/outputs/get-sale-ticket.txt b/tests/outputs/get-sale-ticket.txt new file mode 100644 index 0000000..5b8d7e7 --- /dev/null +++ b/tests/outputs/get-sale-ticket.txt @@ -0,0 +1,19 @@ +Sending message with + + Call type: update + Sender: fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae + Canister id: rkp4c-7iaaa-aaaaa-aaaca-cai + Method name: get_open_ticket + Arguments: (record {}) +Sending message with + + Call type: update + Sender: fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae + Canister id: rkp4c-7iaaa-aaaaa-aaaca-cai + Method name: new_sale_ticket + Arguments: ( + record { + subaccount = opt blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\e0\00\d8\01\01"; + amount_icp_e8s = 100_000 : nat64; + }, +) diff --git a/tests/outputs/refund.txt b/tests/outputs/refund.txt index 173b687..eb7d64d 100644 --- a/tests/outputs/refund.txt +++ b/tests/outputs/refund.txt @@ -8,4 +8,4 @@ Sending message with record { source_principal_id = opt principal "fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae"; }, -) \ No newline at end of file +) diff --git a/tests/outputs/status.txt b/tests/outputs/status.txt index b784c38..822fdd5 100644 --- a/tests/outputs/status.txt +++ b/tests/outputs/status.txt @@ -4,4 +4,4 @@ Sending message with Sender: 2vxsx-fae Canister id: r7inp-6aaaa-aaaaa-aaabq-cai Method name: get_sns_canisters_summary - Arguments: (record { update_canister_list = null }) \ No newline at end of file + Arguments: (record { update_canister_list = null })