Skip to content

Commit

Permalink
feat(backend): Fund use of paid signer API (#2495)
Browse files Browse the repository at this point in the history
# Motivation
The chain fusion signer has paid endpoints. To use these, users need to
get an ICRC-2 approval from the backend.

# Changes
- Add a method to call ICRC-2 approve for a given user.

# Tests
This flow is tested extensively in the PAPI repository:
https://github.com/dfinity/papi/blob/main/src/example/paid_service/tests/it/patron_pays_icrc2_cycles.rs#L29

Once the frontend calls this method and a paid endpoint, we can also
have an integration/e2e test.
  • Loading branch information
bitdivine authored Oct 2, 2024
1 parent f344281 commit c591390
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 40 deletions.
34 changes: 26 additions & 8 deletions src/backend/backend.did
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@ type AddUserCredentialRequest = record {
current_user_version : opt nat64;
credential_spec : CredentialSpec;
};
type AllowSigningError = variant {
ApproveError : ApproveError;
Other : text;
FailedToContactCyclesLedger;
};
type ApiEnabled = variant { ReadOnly; Enabled; Disabled };
type ApproveError = variant {
GenericError : record { message : text; error_code : nat };
TemporarilyUnavailable;
Duplicate : record { duplicate_of : nat };
BadFee : record { expected_fee : nat };
AllowanceChanged : record { current_allowance : nat };
CreatedInFuture : record { ledger_time : nat64 };
TooOld;
Expired : record { ledger_time : nat64 };
InsufficientFunds : record { balance : nat };
};
type Arg = variant { Upgrade; Init : InitArg };
type ArgumentValue = variant { Int : int32; String : text };
type BitcoinNetwork = variant { mainnet; regtest; testnet };
Expand Down Expand Up @@ -112,13 +128,14 @@ type OisyUser = record {
};
type Outpoint = record { txid : blob; vout : nat32 };
type Result = variant { Ok; Err : AddUserCredentialError };
type Result_1 = variant {
type Result_1 = variant { Ok; Err : AllowSigningError };
type Result_2 = variant {
Ok : SelectedUtxosFeeResponse;
Err : SelectedUtxosFeeError;
};
type Result_2 = variant { Ok : UserProfile; Err : GetUserProfileError };
type Result_3 = variant { Ok : MigrationReport; Err : text };
type Result_4 = variant { Ok; Err : text };
type Result_3 = variant { Ok : UserProfile; Err : GetUserProfileError };
type Result_4 = variant { Ok : MigrationReport; Err : text };
type Result_5 = variant { Ok; Err : text };
type SelectedUtxosFeeError = variant { InternalError : record { msg : text } };
type SelectedUtxosFeeRequest = record {
network : BitcoinNetwork;
Expand Down Expand Up @@ -167,19 +184,20 @@ type UserTokenId = record { chain_id : nat64; contract_address : text };
type Utxo = record { height : nat32; value : nat64; outpoint : Outpoint };
service : (Arg) -> {
add_user_credential : (AddUserCredentialRequest) -> (Result);
btc_select_user_utxos_fee : (SelectedUtxosFeeRequest) -> (Result_1);
allow_signing : () -> (Result_1);
btc_select_user_utxos_fee : (SelectedUtxosFeeRequest) -> (Result_2);
bulk_up : (blob) -> ();
config : () -> (Config) query;
create_user_profile : () -> (UserProfile);
get_canister_status : () -> (CanisterStatusResultV2);
get_user_profile : () -> (Result_2) query;
get_user_profile : () -> (Result_3) query;
http_request : (HttpRequest) -> (HttpResponse) query;
list_custom_tokens : () -> (vec CustomToken) query;
list_user_tokens : () -> (vec UserToken) query;
list_users : (ListUsersRequest) -> (ListUsersResponse) query;
migrate_user_data_to : (principal) -> (Result_3);
migrate_user_data_to : (principal) -> (Result_4);
migration : () -> (opt MigrationReport) query;
migration_stop_timer : () -> (Result_4);
migration_stop_timer : () -> (Result_5);
remove_user_token : (UserTokenId) -> ();
set_custom_token : (CustomToken) -> ();
set_guards : (Guards) -> ();
Expand Down
14 changes: 14 additions & 0 deletions src/backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use shared::types::user_profile::{
use shared::types::{
Arg, Config, Guards, InitArg, Migration, MigrationProgress, MigrationReport, Stats,
};
use signer::AllowSigningError;
use std::cell::RefCell;
use std::time::Duration;
use types::{
Expand All @@ -49,6 +50,7 @@ mod heap_state;
mod impls;
mod migrate;
mod oisy_user;
mod signer;
mod state;
mod token;
mod types;
Expand Down Expand Up @@ -398,6 +400,18 @@ fn get_user_profile() -> Result<UserProfile, GetUserProfileError> {
})
}

/// An endpoint to be called by users on first login, to enable them to
/// use the chain fusion signer together with Oisy.
///
/// Note:
/// - The chain fusion signer performs threshold key operations including providing
/// public keys, creating signatures and assisting with performing signed Bitcoin
/// and Ethereum transactions.
#[update(guard = "may_read_user_data")]
async fn allow_signing() -> Result<(), AllowSigningError> {
signer::allow_signing().await
}

#[query(guard = "caller_is_allowed")]
#[allow(clippy::needless_pass_by_value)]
fn list_users(request: ListUsersRequest) -> ListUsersResponse {
Expand Down
84 changes: 84 additions & 0 deletions src/backend/src/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Code for inetracting with the chain fusion signer.
use crate::state::{CYCLES_LEDGER, SIGNER};
use candid::{CandidType, Deserialize, Nat, Principal};
use ic_cycles_ledger_client::{Account, ApproveArgs, ApproveError, Service as CyclesLedgerService};
use ic_ledger_types::Subaccount;
use serde_bytes::ByteBuf;

#[derive(CandidType, Deserialize, Debug, Clone, Eq, PartialEq)]
pub enum AllowSigningError {
Other(String),
FailedToContactCyclesLedger,
ApproveError(ApproveError),
}

/// Current ledger fee in cycles. Historically stable.
///
/// <https://github.com/dfinity/cycles-ledger/blob/1de0e55c6d4fba4bde3e81547e5726df92b881dc/cycles-ledger/src/config.rs#L6>
const LEDGER_FEE: u64 = 1_000_000_000u64;
/// Typical signer fee in cycles. Unstable and subject to change.
/// Note:
/// - The endpoint prices can be seen here: <https://github.com/dfinity/chain-fusion-signer/blob/main/src/signer/canister/src/lib.rs>
/// - At the time of writing, the endpoint prices in the chain fusion signer repo are placeholders. Initial measurements indicate that a typical real fee will be about 80T.
/// - PAPI is likely to offer an endpoint returning a pricelist in future, so we can periodically check the price and adjust this value.
const SIGNER_FEE: u64 = 80_000_000_000;
/// A reasonable number of signing operations per user per login.
///
/// Projected uses:
/// - Getting Ethereum address (1x per login)
/// - Getting Bitcoin address (1x per login)
/// - Signing operations (10x per login)
///
/// Margin of error: 3x (given that the signer fee is subject to change in the next few days and weeks)
const SIGNING_OPS_PER_LOGIN: u64 = 36;
const fn per_user_cycles_allowance() -> u64 {
// Creating the allowance costs 1 ledger fee.
// Every usage costs 1 ledger fee + 1 signer fee.
LEDGER_FEE + (LEDGER_FEE + SIGNER_FEE) * SIGNING_OPS_PER_LOGIN
}

/// Enables the user to sign transactions.
///
/// Signing costs cycles. Managing that cycle payment can be painful so we take care of that.
pub async fn allow_signing() -> Result<(), AllowSigningError> {
let cycles_ledger: Principal = *CYCLES_LEDGER;
let signer: Principal = *SIGNER;
let caller = ic_cdk::caller();
let amount = Nat::from(per_user_cycles_allowance());
CyclesLedgerService(cycles_ledger)
.icrc_2_approve(&ApproveArgs {
spender: Account {
owner: signer,
subaccount: Some(principal2account(&caller)),
},
amount,
created_at_time: None,
expected_allowance: None,
expires_at: None,
fee: None,
from_subaccount: None,
memo: None,
})
.await
.map_err(|_| AllowSigningError::FailedToContactCyclesLedger)?
.0
.map_err(AllowSigningError::ApproveError)?;
Ok(())
}

const SUB_ACCOUNT_ZERO: Subaccount = Subaccount([0; 32]);
#[must_use]
pub fn principal2account(principal: &Principal) -> ByteBuf {
// Note: The AccountIdentifier type contains bytes but has no API to access them.
// There is a ticket to address this here: https://github.com/dfinity/cdk-rs/issues/519
// TODO: Simplify this when an API that provides bytes is available.
let hex_str = ic_ledger_types::AccountIdentifier::new(principal, &SUB_ACCOUNT_ZERO).to_hex();
hex::decode(&hex_str)
.unwrap_or_else(|_| {
unreachable!(
"Failed to decode hex account identifier we just created: {}",
hex_str
)
})
.into()
}
34 changes: 26 additions & 8 deletions src/declarations/backend/backend.did
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@ type AddUserCredentialRequest = record {
current_user_version : opt nat64;
credential_spec : CredentialSpec;
};
type AllowSigningError = variant {
ApproveError : ApproveError;
Other : text;
FailedToContactCyclesLedger;
};
type ApiEnabled = variant { ReadOnly; Enabled; Disabled };
type ApproveError = variant {
GenericError : record { message : text; error_code : nat };
TemporarilyUnavailable;
Duplicate : record { duplicate_of : nat };
BadFee : record { expected_fee : nat };
AllowanceChanged : record { current_allowance : nat };
CreatedInFuture : record { ledger_time : nat64 };
TooOld;
Expired : record { ledger_time : nat64 };
InsufficientFunds : record { balance : nat };
};
type Arg = variant { Upgrade; Init : InitArg };
type ArgumentValue = variant { Int : int32; String : text };
type BitcoinNetwork = variant { mainnet; regtest; testnet };
Expand Down Expand Up @@ -112,13 +128,14 @@ type OisyUser = record {
};
type Outpoint = record { txid : blob; vout : nat32 };
type Result = variant { Ok; Err : AddUserCredentialError };
type Result_1 = variant {
type Result_1 = variant { Ok; Err : AllowSigningError };
type Result_2 = variant {
Ok : SelectedUtxosFeeResponse;
Err : SelectedUtxosFeeError;
};
type Result_2 = variant { Ok : UserProfile; Err : GetUserProfileError };
type Result_3 = variant { Ok : MigrationReport; Err : text };
type Result_4 = variant { Ok; Err : text };
type Result_3 = variant { Ok : UserProfile; Err : GetUserProfileError };
type Result_4 = variant { Ok : MigrationReport; Err : text };
type Result_5 = variant { Ok; Err : text };
type SelectedUtxosFeeError = variant { InternalError : record { msg : text } };
type SelectedUtxosFeeRequest = record {
network : BitcoinNetwork;
Expand Down Expand Up @@ -167,19 +184,20 @@ type UserTokenId = record { chain_id : nat64; contract_address : text };
type Utxo = record { height : nat32; value : nat64; outpoint : Outpoint };
service : (Arg) -> {
add_user_credential : (AddUserCredentialRequest) -> (Result);
btc_select_user_utxos_fee : (SelectedUtxosFeeRequest) -> (Result_1);
allow_signing : () -> (Result_1);
btc_select_user_utxos_fee : (SelectedUtxosFeeRequest) -> (Result_2);
bulk_up : (blob) -> ();
config : () -> (Config) query;
create_user_profile : () -> (UserProfile);
get_canister_status : () -> (CanisterStatusResultV2);
get_user_profile : () -> (Result_2) query;
get_user_profile : () -> (Result_3) query;
http_request : (HttpRequest) -> (HttpResponse) query;
list_custom_tokens : () -> (vec CustomToken) query;
list_user_tokens : () -> (vec UserToken) query;
list_users : (ListUsersRequest) -> (ListUsersResponse) query;
migrate_user_data_to : (principal) -> (Result_3);
migrate_user_data_to : (principal) -> (Result_4);
migration : () -> (opt MigrationReport) query;
migration_stop_timer : () -> (Result_4);
migration_stop_timer : () -> (Result_5);
remove_user_token : (UserTokenId) -> ();
set_custom_token : (CustomToken) -> ();
set_guards : (Guards) -> ();
Expand Down
34 changes: 26 additions & 8 deletions src/declarations/backend/backend.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ export interface AddUserCredentialRequest {
current_user_version: [] | [bigint];
credential_spec: CredentialSpec;
}
export type AllowSigningError =
| { ApproveError: ApproveError }
| { Other: string }
| { FailedToContactCyclesLedger: null };
export type ApiEnabled = { ReadOnly: null } | { Enabled: null } | { Disabled: null };
export type ApproveError =
| {
GenericError: { message: string; error_code: bigint };
}
| { TemporarilyUnavailable: null }
| { Duplicate: { duplicate_of: bigint } }
| { BadFee: { expected_fee: bigint } }
| { AllowanceChanged: { current_allowance: bigint } }
| { CreatedInFuture: { ledger_time: bigint } }
| { TooOld: null }
| { Expired: { ledger_time: bigint } }
| { InsufficientFunds: { balance: bigint } };
export type Arg = { Upgrade: null } | { Init: InitArg };
export type ArgumentValue = { Int: number } | { String: string };
export type BitcoinNetwork = { mainnet: null } | { regtest: null } | { testnet: null };
Expand Down Expand Up @@ -127,10 +143,11 @@ export interface Outpoint {
vout: number;
}
export type Result = { Ok: null } | { Err: AddUserCredentialError };
export type Result_1 = { Ok: SelectedUtxosFeeResponse } | { Err: SelectedUtxosFeeError };
export type Result_2 = { Ok: UserProfile } | { Err: GetUserProfileError };
export type Result_3 = { Ok: MigrationReport } | { Err: string };
export type Result_4 = { Ok: null } | { Err: string };
export type Result_1 = { Ok: null } | { Err: AllowSigningError };
export type Result_2 = { Ok: SelectedUtxosFeeResponse } | { Err: SelectedUtxosFeeError };
export type Result_3 = { Ok: UserProfile } | { Err: GetUserProfileError };
export type Result_4 = { Ok: MigrationReport } | { Err: string };
export type Result_5 = { Ok: null } | { Err: string };
export type SelectedUtxosFeeError = { InternalError: { msg: string } };
export interface SelectedUtxosFeeRequest {
network: BitcoinNetwork;
Expand Down Expand Up @@ -186,19 +203,20 @@ export interface Utxo {
}
export interface _SERVICE {
add_user_credential: ActorMethod<[AddUserCredentialRequest], Result>;
btc_select_user_utxos_fee: ActorMethod<[SelectedUtxosFeeRequest], Result_1>;
allow_signing: ActorMethod<[], Result_1>;
btc_select_user_utxos_fee: ActorMethod<[SelectedUtxosFeeRequest], Result_2>;
bulk_up: ActorMethod<[Uint8Array | number[]], undefined>;
config: ActorMethod<[], Config>;
create_user_profile: ActorMethod<[], UserProfile>;
get_canister_status: ActorMethod<[], CanisterStatusResultV2>;
get_user_profile: ActorMethod<[], Result_2>;
get_user_profile: ActorMethod<[], Result_3>;
http_request: ActorMethod<[HttpRequest], HttpResponse>;
list_custom_tokens: ActorMethod<[], Array<CustomToken>>;
list_user_tokens: ActorMethod<[], Array<UserToken>>;
list_users: ActorMethod<[ListUsersRequest], ListUsersResponse>;
migrate_user_data_to: ActorMethod<[Principal], Result_3>;
migrate_user_data_to: ActorMethod<[Principal], Result_4>;
migration: ActorMethod<[], [] | [MigrationReport]>;
migration_stop_timer: ActorMethod<[], Result_4>;
migration_stop_timer: ActorMethod<[], Result_5>;
remove_user_token: ActorMethod<[UserTokenId], undefined>;
set_custom_token: ActorMethod<[CustomToken], undefined>;
set_guards: ActorMethod<[Guards], undefined>;
Expand Down
Loading

0 comments on commit c591390

Please sign in to comment.