|
| 1 | +import { createClient } from 'polkadot-api'; |
| 2 | +import { Binary } from '@polkadot-api/substrate-bindings'; |
| 3 | +import { getWsProvider } from 'polkadot-api/ws-provider'; |
| 4 | +import { getPolkadotSigner } from '@polkadot-api/signer'; |
| 5 | +import { Keyring } from '@polkadot/keyring'; |
| 6 | +import { cryptoWaitReady } from '@polkadot/util-crypto'; |
| 7 | +import { create } from 'ipfs-http-client'; |
| 8 | +import { cidFromBytes } from './common.js'; |
| 9 | +import { bulletin } from './.papi/descriptors/dist/index.mjs'; |
| 10 | + |
| 11 | +async function authorizeAccount(typedApi, sudoPair, who, transactions, bytes) { |
| 12 | + console.log('Creating authorizeAccount transaction...'); |
| 13 | + |
| 14 | + const authorizeTx = typedApi.tx.TransactionStorage.authorize_account({ |
| 15 | + who, |
| 16 | + transactions, |
| 17 | + bytes |
| 18 | + }); |
| 19 | + |
| 20 | + const sudoTx = typedApi.tx.Sudo.sudo({ |
| 21 | + call: authorizeTx.decodedCall |
| 22 | + }); |
| 23 | + |
| 24 | + // Wait for a new block. |
| 25 | + return new Promise((resolve, reject) => { |
| 26 | + const sub = sudoTx |
| 27 | + .signSubmitAndWatch(sudoPair) |
| 28 | + .subscribe({ |
| 29 | + next: (ev) => { |
| 30 | + if (ev.type === "txBestBlocksState" && ev.found) { |
| 31 | + console.log("📦 Included in block:", ev.block.hash); |
| 32 | + sub.unsubscribe(); |
| 33 | + resolve(ev); |
| 34 | + } |
| 35 | + }, |
| 36 | + error: (err) => { |
| 37 | + console.log("Error:", err); |
| 38 | + sub.unsubscribe(); |
| 39 | + reject(err); |
| 40 | + }, |
| 41 | + complete: () => { |
| 42 | + console.log("Subscription complete"); |
| 43 | + } |
| 44 | + }); |
| 45 | + }) |
| 46 | +} |
| 47 | + |
| 48 | +async function store(typedApi, pair, data) { |
| 49 | + console.log('Storing data:', data); |
| 50 | + const cid = cidFromBytes(data); |
| 51 | + |
| 52 | + // Convert data to Uint8Array then wrap in Binary for PAPI typed API |
| 53 | + const dataBytes = typeof data === 'string' ? |
| 54 | + new Uint8Array(Buffer.from(data)) : |
| 55 | + new Uint8Array(data); |
| 56 | + |
| 57 | + // Wrap in Binary object for typed API - pass as an object with 'data' property |
| 58 | + const binaryData = Binary.fromBytes(dataBytes); |
| 59 | + const tx = typedApi.tx.TransactionStorage.store({ data: binaryData }); |
| 60 | + |
| 61 | + // Wait for a new block. |
| 62 | + return new Promise((resolve, reject) => { |
| 63 | + const sub = tx |
| 64 | + .signSubmitAndWatch(pair) |
| 65 | + .subscribe({ |
| 66 | + next: (ev) => { |
| 67 | + if (ev.type === "txBestBlocksState" && ev.found) { |
| 68 | + console.log("📦 Included in block:", ev.block.hash); |
| 69 | + sub.unsubscribe(); |
| 70 | + resolve(cid); |
| 71 | + } |
| 72 | + }, |
| 73 | + error: (err) => { |
| 74 | + console.log("Error:", err); |
| 75 | + sub.unsubscribe(); |
| 76 | + reject(err); |
| 77 | + }, |
| 78 | + complete: () => { |
| 79 | + console.log("Subscription complete"); |
| 80 | + } |
| 81 | + }); |
| 82 | + }) |
| 83 | +} |
| 84 | + |
| 85 | +// Connect to a local IPFS gateway (e.g. Kubo) |
| 86 | +const ipfs = create({ |
| 87 | + url: 'http://127.0.0.1:5001', // Local IPFS API |
| 88 | +}); |
| 89 | + |
| 90 | +async function read_from_ipfs(cid) { |
| 91 | + // Fetch the block (downloads via Bitswap if not local) |
| 92 | + console.log('Trying to get cid: ', cid); |
| 93 | + try { |
| 94 | + const block = await ipfs.block.get(cid, {timeout: 10000}); |
| 95 | + console.log('Received block: ', block); |
| 96 | + if (block.length !== 0) { |
| 97 | + return block; |
| 98 | + } |
| 99 | + } catch (error) { |
| 100 | + console.log('Block not found directly, trying cat...', error.message); |
| 101 | + } |
| 102 | + |
| 103 | + // Fetch the content from IPFS |
| 104 | + console.log('Trying to chunk cid: ', cid); |
| 105 | + const chunks = []; |
| 106 | + for await (const chunk of ipfs.cat(cid)) { |
| 107 | + chunks.push(chunk); |
| 108 | + } |
| 109 | + |
| 110 | + const content = Buffer.concat(chunks); |
| 111 | + return content; |
| 112 | +} |
| 113 | + |
| 114 | +// Global client reference for cleanup |
| 115 | +let client; |
| 116 | + |
| 117 | +async function main() { |
| 118 | + await cryptoWaitReady(); |
| 119 | + |
| 120 | + // Create PAPI client with WebSocket provider |
| 121 | + client = createClient(getWsProvider('ws://localhost:10000')); |
| 122 | + |
| 123 | + // Get typed API with generated descriptors |
| 124 | + const typedApi = client.getTypedApi(bulletin); |
| 125 | + |
| 126 | + // Create keyring and accounts |
| 127 | + const keyring = new Keyring({ type: 'sr25519' }); |
| 128 | + const sudoAccount = keyring.addFromUri('//Alice'); |
| 129 | + const whoAccount = keyring.addFromUri('//Alice'); |
| 130 | + |
| 131 | + // Create PAPI-compatible signers using @polkadot-api/signer |
| 132 | + // getPolkadotSigner expects (publicKey: Uint8Array, signingType, sign function) |
| 133 | + const sudoSigner = getPolkadotSigner( |
| 134 | + sudoAccount.publicKey, |
| 135 | + 'Sr25519', |
| 136 | + (input) => sudoAccount.sign(input) |
| 137 | + ); |
| 138 | + const whoSigner = getPolkadotSigner( |
| 139 | + whoAccount.publicKey, |
| 140 | + 'Sr25519', |
| 141 | + (input) => whoAccount.sign(input) |
| 142 | + ); |
| 143 | + |
| 144 | + // Data |
| 145 | + const who = whoAccount.address; |
| 146 | + const transactions = 32; // u32 - regular number |
| 147 | + const bytes = 64n * 1024n * 1024n; // u64 - BigInt for large numbers |
| 148 | + |
| 149 | + console.log('Doing authorization...'); |
| 150 | + await authorizeAccount(typedApi, sudoSigner, who, transactions, bytes); |
| 151 | + console.log('Authorized!'); |
| 152 | + |
| 153 | + console.log('Storing data ...'); |
| 154 | + const dataToStore = "Hello, Bulletin with PAPI - " + new Date().toString(); |
| 155 | + let cid = await store(typedApi, whoSigner, dataToStore); |
| 156 | + console.log('Stored data with CID: ', cid); |
| 157 | + |
| 158 | + console.log('Reading content... cid: ', cid); |
| 159 | + let content = await read_from_ipfs(cid); |
| 160 | + console.log('Content as bytes:', content); |
| 161 | + console.log('Content as string:', content.toString()); |
| 162 | + |
| 163 | + client.destroy(); |
| 164 | +} |
| 165 | + |
| 166 | +main().catch(console.error).finally(() => { |
| 167 | + if (client) client.destroy(); |
| 168 | +}); |
| 169 | + |
0 commit comments