Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:

jobs:
build:
# only run in forks — non-fork PRs get a build via preview-deployment.yml
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-ic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- run: npm run build

- name: Install icp-cli
run: npm i -g @icp-sdk/icp-cli@0.2.0 @icp-sdk/ic-wasm
run: npm i -g @icp-sdk/icp-cli@0.2.6 @icp-sdk/ic-wasm

- name: Import deploy identity
run: |
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/pr-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: PR Cleanup
on:
pull_request:
types: [closed]

jobs:
release_preview_canister:
# do not run in forks
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event.pull_request.number || github.event.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
with:
python-version: "3.10"
- run: |
pip install icp-py-core "cbor2<6"
python3 .github/workflows/scripts/release-canister.py ${{ github.event.pull_request.number }}
env:
POOL_CONTROLLER_IDENTITY: ${{ secrets.POOL_CONTROLLER_IDENTITY }}
POOL_CANISTER_ID: ${{ secrets.POOL_CANISTER_ID }}

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.delete(context, github, maybeComment.id);
}
95 changes: 95 additions & 0 deletions .github/workflows/preview-deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: PR Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
build_and_deploy:
# do not run in forks
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event.pull_request.number || github.event.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Initialize examples submodule
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --depth 1 .sources/examples

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Your PR preview is being built...`);
} else {
await comments.create(context, github, `🤖 Your PR preview is being built...`);
}

- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
with:
python-version: "3.10"

- name: Install icp-cli
run: npm i -g @icp-sdk/icp-cli@0.2.6 @icp-sdk/ic-wasm

- run: npm ci

- name: Build & Deploy
run: |
# Setup identity
mkdir -p ~/.local/share/icp-cli/identity/keys
echo $POOL_CONTROLLER_IDENTITY | base64 -d > ~/.local/share/icp-cli/identity/keys/preview-deploy.pem
sed -i 's/\\r\\n/\r\n/g' ~/.local/share/icp-cli/identity/keys/preview-deploy.pem
icp identity import preview-deploy --from-pem ~/.local/share/icp-cli/identity/keys/preview-deploy.pem --storage plaintext
icp identity default preview-deploy

# Request preview canister from the pool
pip install icp-py-core "cbor2<6"
canister_id=$(python3 .github/workflows/scripts/request-canister.py ${{ github.event.pull_request.number }})

# Override canister ID mapping for ic environment
echo "{\"frontend\":\"$canister_id\"}" > .icp/data/mappings/ic.ids.json

echo "PREVIEW_CANISTER_ID=$canister_id" >> $GITHUB_ENV

# Deploy (icp.yaml recipe handles the build)
icp deploy frontend -e ic --mode reinstall

env:
POOL_CONTROLLER_IDENTITY: ${{ secrets.POOL_CONTROLLER_IDENTITY }}
POOL_CANISTER_ID: ${{ secrets.POOL_CANISTER_ID }}

- name: Report build error
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
if: ${{ failure() }}
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Preview build failed.`);
} else {
await comments.create(context, github, `🤖 Preview build failed.`);
}

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Here's your preview: https://${process.env.PREVIEW_CANISTER_ID}.icp0.io`);
} else {
await comments.create(context, github, `🤖 Here's your preview: https://${process.env.PREVIEW_CANISTER_ID}.icp0.io`);
}
39 changes: 39 additions & 0 deletions .github/workflows/scripts/comments.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const MARKER = '<!-- pr-preview -->';

exports.get = async function (context, github) {
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
repo: context.repo.repo,
owner: context.repo.owner,
});

return comments.data.find(
(c) => c.user.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body.includes(MARKER)
);
};

exports.create = function (context, github, body) {
return github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `${MARKER}\n${body}`,
});
};

exports.update = function (context, github, id, body) {
return github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: id,
body: `${MARKER}\n${body}`,
});
};

exports.delete = function (context, github, id) {
return github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: id,
});
};
30 changes: 30 additions & 0 deletions .github/workflows/scripts/pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from icp_core import Agent, Client, Identity, encode, Types
import os
import sys
import base64


#
# Interact with preview canister pool: https://github.com/dfinity/preview-canister-pool
#

private_key = base64.b64decode(os.environ["POOL_CONTROLLER_IDENTITY"]).decode("utf-8")
pool_id = os.environ["POOL_CANISTER_ID"]

identity = Identity.from_pem(private_key)
client = Client()
agent = Agent(identity, client)

def release_canister():
res = agent.update_raw(
pool_id, "release_canister", encode([{'type': Types.Text, 'value': sys.argv[1]}]),
verify_certificate=False)
return res


def request_canister():
res = agent.update_raw(
pool_id, "request_canister", encode([{'type': Types.Text, 'value': sys.argv[1]}]),
return_type=Types.Principal,
verify_certificate=False)
return res
22 changes: 22 additions & 0 deletions .github/workflows/scripts/release-canister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os
import sys
import traceback

if len(sys.argv) != 2:
print("Usage: python3 release-canister.py <ref>")
exit(1)

for v in ["POOL_CONTROLLER_IDENTITY","POOL_CANISTER_ID"]:
if not v in os.environ:
print(f"release-canister.py: {v} env variable missing")
exit(1)


from pool import release_canister

try:
release_canister()
except Exception as e:
print(f"release-canister.py: failed to release canister: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
exit(1)
25 changes: 25 additions & 0 deletions .github/workflows/scripts/request-canister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
import sys
import traceback

if len(sys.argv) != 2:
print("Usage: python3 request_canister.py <ref>")
exit(1)

for v in ["POOL_CONTROLLER_IDENTITY","POOL_CANISTER_ID"]:
if not v in os.environ:
print(f"request-canister.py: {v} env variable missing")
exit(1)

from pool import request_canister

try:
result = request_canister()
canister_id = result[0]['value'].to_str()
print(canister_id)
except Exception as e:
print(f"request-canister.py: failed to request canister: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
if 'result' in dir():
print(f"request-canister.py: raw result: {result}", file=sys.stderr)
exit(1)
65 changes: 64 additions & 1 deletion docs/references/protocol-canisters.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,38 @@ The Bitcoin integration canisters connect ICP to the Bitcoin network. They track
- `bitcoin_send_transaction`: submits a signed Bitcoin transaction
- `bitcoin_get_current_fee_percentiles`: returns fee percentiles in millisatoshi/vbyte
- `bitcoin_get_block_headers`: returns block headers for a range of heights
- `get_blockchain_info`: returns chain tip height, block hash, timestamp, difficulty, and UTXO count

For integration patterns, see the [Bitcoin guide](../guides/chain-fusion/bitcoin.md).
### Cycle costs

All Bitcoin canister calls require cycles attached. In Rust, the `ic-cdk-bitcoin-canister` crate handles this automatically. In Motoko, attach cycles explicitly with `(with cycles = amount)`.

| Endpoint | Testnet / Regtest | Mainnet |
|---|---|---|
| `bitcoin_get_balance` | 40,000,000 | 100,000,000 |
| `bitcoin_get_utxos` | 4,000,000,000 | 10,000,000,000 |
| `bitcoin_send_transaction` (base) | 2,000,000,000 | 5,000,000,000 |
| `bitcoin_send_transaction` (per byte) | 8,000,000 | 20,000,000 |
| `bitcoin_get_current_fee_percentiles` | 40,000,000 | 100,000,000 |
| `bitcoin_get_block_headers` | 4,000,000,000 | 10,000,000,000 |
| `get_blockchain_info` | 40,000,000 | 100,000,000 |

For integration patterns and code examples, see the [Bitcoin guide](../guides/chain-fusion/bitcoin.md).

## Dogecoin canister

The Dogecoin canister is a system-level canister that connects ICP to the Dogecoin network using the same architecture as the Bitcoin integration. It syncs blocks from the Dogecoin peer-to-peer network, maintains the UTXO set, and exposes an API for querying Dogecoin state and submitting transactions.

For the current canister ID, see the [Dogecoin canister repository](https://github.com/dfinity/dogecoin-canister).

### Key endpoints

- `dogecoin_get_utxos`: returns UTXOs for a Dogecoin address
- `dogecoin_get_balance`: returns the balance of a Dogecoin address in koinu (1 DOGE = 100,000,000 koinu)
- `dogecoin_get_current_fee_percentiles`: returns fee percentiles from recent Dogecoin transactions
- `dogecoin_send_transaction`: submits a signed transaction to the Dogecoin network

For integration patterns, see the [Dogecoin guide](../guides/chain-fusion/dogecoin.md).

## ckBTC minter

Expand Down Expand Up @@ -69,6 +99,16 @@ For canister IDs, see [Chain-Key Token Canister IDs: ckBTC](chain-key-canister-i
- `get_minter_info`: returns current minter parameters
- `get_events(start, length)`: returns the minter's internal event log

### Withdrawal fee

The minter fee for a Bitcoin withdrawal transaction is `146 × inputs + 4 × outputs + 26` satoshi. This formula covers the cost of threshold ECDSA signatures and Bitcoin transaction broadcasting. When multiple withdrawal requests are batched into one transaction, the fee is split among all outputs.

<!-- Needs DeFi team verification: minter fee formula and UTXO consolidation parameters below were sourced from Learn Hub (now retired) and have not yet been independently verified against the live minter code. -->

### UTXO consolidation

As deposits accumulate, the minter manages a growing set of UTXOs. If the UTXO count exceeds 10,000, the minter periodically creates consolidation transactions that merge the 1,000 smallest UTXOs into 2 new outputs, funded from the minter's fee subaccount. This prevents the UTXO set from growing large enough to make withdrawals impossible (a Bitcoin transaction is limited to 100 KB).

### KYT checker

The ckBTC checker canister (`oltsj-fqaaa-aaaar-qal5q-cai`) performs know-your-transaction compliance checks on incoming Bitcoin UTXOs. It is called internally by the minter on deposit and is not part of the developer-facing API.
Expand Down Expand Up @@ -191,6 +231,28 @@ By default, the canister requires all providers to agree (`Equality` consensus).

For integration examples, see the [Ethereum guide](../guides/chain-fusion/ethereum.md).

## SOL RPC canister

The SOL RPC canister proxies JSON-RPC calls to the Solana network via HTTPS outcalls. It follows the same pattern as the EVM RPC canister: each request is forwarded to multiple independent RPC providers and the results are compared for consensus before being returned to the caller. No API keys are required.

| Field | Value |
|---|---|
| Canister ID | [`2xib7-jqaaa-aaaar-qai6q-cai`](https://dashboard.internetcomputer.org/canister/2xib7-jqaaa-aaaar-qai6q-cai) |
| Source | [dfinity/sol-rpc-canister](https://github.com/dfinity/sol-rpc-canister) |

### Built-in RPC providers

| Provider |
|---|
| [Alchemy](https://www.alchemy.com/) |
| [Ankr](https://www.ankr.com/) |
| [Chainstack](https://chainstack.com/) |
| [dRPC](https://drpc.org/) |
| [Helius](https://www.helius.dev/) |
| [PublicNode](https://publicnode.com/) |

For integration examples, see the [Solana guide](../guides/chain-fusion/solana.md).

## Exchange rate canister (XRC)

The exchange rate canister (XRC) uses HTTPS outcalls to fetch cryptocurrency and foreign exchange rates from major exchanges. It runs on the `uzr34` system subnet and is used by the cycles minting canister (CMC) to convert ICP to cycles at a stable XDR-pegged price.
Expand Down Expand Up @@ -293,6 +355,7 @@ For governance context, see the [SNS documentation](https://learn.internetcomput
| ckDOGE Minter | `eqltq-xqaaa-aaaar-qb3vq-cai` | DOGE ↔ ckDOGE minting and burning |
| ckSOL Minter | `lh22c-kyaaa-aaaar-qb5nq-cai` | SOL ↔ ckSOL minting and burning |
| EVM RPC | `7hfb6-caaaa-aaaar-qadga-cai` | Ethereum JSON-RPC proxy |
| SOL RPC | `2xib7-jqaaa-aaaar-qai6q-cai` | Solana JSON-RPC proxy |
| Exchange Rate (XRC) | `uf6dk-hyaaa-aaaaq-qaaaq-cai` | Crypto and forex exchange rates |
| SNS-W | `qaa6y-5yaaa-aaaaa-aaafa-cai` | SNS deployment and upgrades |

Expand Down
Loading