Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): Fund use of paid signer API #2495

Merged
merged 33 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2cb7b64
Add the allow signing placeholder
bitdivine Sep 24, 2024
527dee0
Add cycles_canister as an init arg
bitdivine Sep 24, 2024
560cd87
Update bindings
bitdivine Sep 24, 2024
c25d7bf
Add cycles ledger bindings
bitdivine Sep 24, 2024
8ce790d
Revert "Add cycles ledger bindings"
bitdivine Sep 24, 2024
1265647
Add rust types for the cycles ledger
bitdivine Sep 24, 2024
563ffb7
Add cycles ledger as a dependency
bitdivine Sep 24, 2024
7be8e00
Add signer canister ID
bitdivine Sep 24, 2024
ed8b350
fix
bitdivine Sep 24, 2024
088fad0
clippy
bitdivine Sep 24, 2024
2e0ccdb
Merge remote-tracking branch 'origin/main' into papi
bitdivine Sep 24, 2024
9e36981
Add ledger types
bitdivine Sep 24, 2024
493eb98
Update bindings
bitdivine Sep 24, 2024
cdc428a
Implement icrc2 approval
bitdivine Sep 24, 2024
032b5a5
Merge remote-tracking branch 'origin/main' into papi
bitdivine Sep 30, 2024
23643dc
Update declarations
bitdivine Sep 30, 2024
d5f33c5
rename
bitdivine Sep 30, 2024
6b36021
Update dockerfile
bitdivine Sep 30, 2024
f53463c
Merge remote-tracking branch 'origin/main' into papi
bitdivine Sep 30, 2024
967f9b5
simplify
bitdivine Sep 30, 2024
af0499e
Merge remote-tracking branch 'origin/main' into papi
bitdivine Oct 1, 2024
2d4712e
Merge remote-tracking branch 'origin/main' into papi
bitdivine Oct 1, 2024
07acbe7
Merge remote-tracking branch 'origin/main' into papi
bitdivine Oct 1, 2024
d751497
Update bindings
bitdivine Oct 2, 2024
23d7be4
clippy
bitdivine Oct 2, 2024
27c4965
update bindings
bitdivine Oct 2, 2024
3962271
Merge remote-tracking branch 'origin/main' into papi
bitdivine Oct 2, 2024
8454165
Comment endpoint
bitdivine Oct 2, 2024
b568921
++
bitdivine Oct 2, 2024
5117f66
++
bitdivine Oct 2, 2024
03f3471
fmt
bitdivine Oct 2, 2024
a1a6602
tweak
bitdivine Oct 2, 2024
4acffd0
Merge branch 'main' into papi
bitdivine Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
bitdivine marked this conversation as resolved.
Show resolved Hide resolved
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;
bitdivine marked this conversation as resolved.
Show resolved Hide resolved
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 {
bitdivine marked this conversation as resolved.
Show resolved Hide resolved
// 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