Skip to content
This repository has been archived by the owner on Jun 8, 2023. It is now read-only.

feat: Implement the new sale payment flow for the ticketing system #33

Closed
wants to merge 12 commits into from
Closed
57 changes: 46 additions & 11 deletions candid/swap.did
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,26 @@ 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 {
err : opt CanisterCallError;
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 };
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -92,27 +110,30 @@ 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;
icp_ledger_account_balance_e8s : nat64;
};
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;
};
Expand All @@ -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;
Expand All @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions src/commands/get_sale_ticket.rs
Original file line number Diff line number Diff line change
@@ -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<HexSubaccount>,
}

// 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 {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct GetOpenTicketArg {}
struct GetOpenTicketRequest {}


pub fn exec(
pem: &str,
sns_canister_ids: &SnsCanisterIds,
opts: GetSaleTicketOpts,
) -> AnyhowResult<Vec<IngressWithRequestId>> {
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),
)?;
Comment on lines +51 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like sns-quill makes this conditional message execution pretty difficult. I.e. if get_open_ticket does return a ticket id, then the subsequent call to new_sale_ticket will fail. This may be a confusing experience for users so I've asked Mario if he can introduce a third API to get_or_create. You can see the slack thread here https://dfinity.slack.com/archives/C03H6QEPW5D/p1675465230570649?thread_ts=1675169129.448279&cid=C03H6QEPW5D. If that change is impossible, I only see two ways of proceeding:

  1. Continue with what is already implemented. The user will have to parse the results.
  2. Separate these into two separate commands and have user understand the sale payment flow.

I think (1) is the best option right now but less than ideal. Lets see how he responds on Monday.

Ok(vec![req1, req2])
}
8 changes: 8 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })
}
Expand Down
20 changes: 20 additions & 0 deletions src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ enum SupportedResponse {
IcpTransfer,
RefreshBuyerTokens,
GetSnsCanistersSummary,
GetOpenTicket,
NewSaleTicket,
}

impl FromStr for SupportedResponse {
Expand All @@ -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
Expand Down Expand Up @@ -200,7 +204,23 @@ fn print_response(blob: Vec<u8>, 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 {}
1 change: 1 addition & 0 deletions tests/commands/get-sale-ticket.sh
Original file line number Diff line number Diff line change
@@ -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 -
19 changes: 19 additions & 0 deletions tests/outputs/get-sale-ticket.txt
Original file line number Diff line number Diff line change
@@ -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;
},
)
2 changes: 1 addition & 1 deletion tests/outputs/refund.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Sending message with
record {
source_principal_id = opt principal "fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae";
},
)
)
2 changes: 1 addition & 1 deletion tests/outputs/status.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Arguments: (record { update_canister_list = null })