gpg's notebooks

  • mnsig key generation example - /gpg/mnsig-key-generation
    Last edited 6 years ago
    const mnsig = require('mnsig-client') const result = mnsig.generateKey('password123', 'bip44', 'testnet') console.log(result.xprivEnc) mnsig.decryptKey('password123', result.xprivEnc, 'bip44', 'testnet').toBase58()
  • mnsig local key generation - /gpg/mnsig-local-key-generation
    Last edited 6 years ago
    const mnsig = require('mnsig-client');
  • mnsig client proxy - /gpg/mnsig-client-proxy
    Last edited 6 years ago
    When integrating with mnsig and you're using a language other than javascript, you will want to use the mnsig client proxy: git clone https://gitlab.com/mnsig/mnsig-proxy-js.git cd mnsig-proxy-js npm install node server.js This will start a server that accepts requests at http://127.0.0.1:7711. In order to listen on a different host and/or port, specify the HOST and PORT environment variables. Now you can start using it. To check it's up and running, try: curl -X POST http://localhost:7711/config The expected response is {"success":true} * Obtain a new address curl -X POST http://localhost:7711/wallet/<wallet name>/address \ -H 'Instance: <your instance>' -H 'Token: <your token>' * Expected response format: { "data": { "wallet": <wallet name> "address": "2N5pmYg6BcvfGoiNqgkyk4AF91ZNVxcwnCM", "success": true, "extra": { "path": "m/0/3", "script": "00206ab3173b63d100bf5f650b043f4f8ce5317fac1579790dc02da476c60b358924" } }, "code": 200, "isError": false } The content for "extra" depends on the type of the wallet. In this example it's a bitcoin testnet with segwit wallet: "m/0/3" is the derivation path in the HD wallet (some libraries might require using "0/3" instead of "m/0/3" as you're never deriving straight from the master key). It's possible your integration doesn't need to look at this extra content. * Get wallet balance curl -X GET http://localhost:7711/wallet/<wallet name>/balance \ -H 'Instance: <your instance>' -H 'Token: <your token>' * Expected response format: { "data": { "timestamp": "1516118201.64854", "confirmed": "1.29998894", "total": "1.29998894", "success": true, "wallet": <wallet name> }, "code": 200, "isError": false } The total amount is expected to always be greater than or equal to the confirmed amount. Pending proposals or unconfirmed deposits will affect these numbers. * Getting a fee rate estimation Commonly this is only required if for some reason the initial fee rate for a proposal gets lost or if you want to display this information to others. curl -X GET http://localhost:7711/wallet/<wallet name>/estimatefee/<target> \ -H 'Instance: <your instance>' -H 'Token: <your token>' target is an integer representing the expected number of blocks before a transaction gets included * Expected response format: { "data": { "target_blocks": 4, "fee_rate": 0.00005, "success": true }, "code": 200, "isError": false } ** Withdraw ** mnsig requires a sequence of steps for doing a withdrawal, it starts with what it calls a proposal. The proposal starts without signatures (and for all wallets but Ethereum) in a state called "unprepared". The proposal gets prepared and then it's able to start receiving signatures. After enough signatures are added to it, it's marked as "complete". A proposal that is "complete" can be broadcasted to the network, which is then marked as "broadcasted". * Start a proposal for one recipient curl -X POST http://localhost:7711/transaction/proposal \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": {"wallet": <your wallet>, "dest": <destination address>, "amount": <amount as a string>}}' To send 0.1 testnet BTC the amount would be "0.1" * Expected response format: { "data": { "utxo": [ { "redeemScript": "00204d744b0ced5e458eb6da39db18c360a1b14e53283b1e6a8dc2bcfc14752aa8e9", "account": "", "vout": 0, "safe": true, "txid": "c92ebdad64220368bf956131c8c3be89ca9f8c1d3c0948e1381cbb094c0b8b10", "amount": 0.125, "confirmations": 314, "address": "2MsVp2rH9yjs2QnBRa9CsNo8rKPPSmrXk3t", "spendable": false, "path": "m/0/0", "solvable": true, "scriptPubKey": "a91402c23dff18a54522d2d5f657b16a096209405f9c87" } ], "xpub_list": [<wallet xpubs >], "xpub_signed": [], "recipients": { "2N5pmYg6BcvfGoiNqgkyk4AF91ZNVxcwnCM": 10000000 }, "dest_address": "2N5pmYg6BcvfGoiNqgkyk4AF91ZNVxcwnCM", "wallet_type": "testbitcoin_segwit", "id": 25, "fee": null, "success": true, "created_at": "2018-01-16T16:34:26.682499", "m": 1, "state": "unprepared", "created_by": "test1", "fee_rate": "0.00005000", "note": null, "wallet": <wallet name>, "amount": 10000000, "external_id": null, "change_address": { "address": "2Mxn1DJwVti8y3tJNNHJ5RsgnaFo2nFQbRw" } }, "code": 200, "isError": false } It's recommended to store the returned id, 25 in this example, as it's one of the ways to get and consult this proposal through the API later on. The other method is by specifying an external id and fetching proposals based on it. The amount indicated in the response is always an integer based on the wallet type. In this case it is 10000000, which means the amount specified in the request was "0.1". Note that the "external_id" field is null in this example. It's a very good idea to specify a unique external_id when starting a proposal so you can avoid sending out the same transaction more than once. The current state for this proposal is "unprepared", which means no real bitcoin transaction has been created yet (not even one without signatures). It's always expected that a subsequent request is sent right away to move it from "unprepared" to "no_signatures". For Ethereum wallets the proposals always started as "no_signatures", which is the next step after "unprepared". * Move a proposal from "unprepared" to "no_signatures" curl -X PUT http://localhost:7711/transaction/proposal \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": ....}' The content for the proposal key is the contents of the data key returned in the request above. * Expected response format: { "data": { "updated": true, "proposal": { "utxo": [ { "redeemScript": "00204d744b0ced5e458eb6da39db18c360a1b14e53283b1e6a8dc2bcfc14752aa8e9", "account": "", "vout": 0, "safe": true, "txid": "c92ebdad64220368bf956131c8c3be89ca9f8c1d3c0948e1381cbb094c0b8b10", "amount": 0.125, "confirmations": 319, "address": "2MsVp2rH9yjs2QnBRa9CsNo8rKPPSmrXk3t", "spendable": false, "path": "m/0/0", "solvable": true, "scriptPubKey": "a91402c23dff18a54522d2d5f657b16a096209405f9c87" } ], "xpub_list": [<wallet xpubs>], "fee": 765, "recipients": { "2N5pmYg6BcvfGoiNqgkyk4AF91ZNVxcwnCM": 10000000 }, "wallet_type": "testbitcoin_segwit", "dest_address": "2N5pmYg6BcvfGoiNqgkyk4AF91ZNVxcwnCM", "created_at": "2018-01-16T17:09:57.259592", "m": 1, "state": "no_signatures", "created_by": "test1", "note": null, "wallet": "test-btc1", "amount": 10000000, "xpub_signed": [], "rawtx": "0100000001108b0b4c09bb1c38e148093c1d8c9fca89bec3c8316195bf68032264adbd2ec90000000000ffffffff02809698000000000017a91489f9946f390dd5f166bdba2f17f964caa6f575ee87a32226000000000017a9147bc904052c20db7e8882ab01a121f152843bfd9c8700000000", "external_id": null, "id": 25, "change_address": { "address": "2N4XjuhGRgT7U8mYypEK4nJEZKc3hm4Ai2j" } }, "success": true }, "code": 200, "isError": false } At this point this proposal is ready to be signed. If you don't have both a private key and API token with permissions for it, it's not possible to send signed proposals to mnsig. * Signing a proposal The signing of a proposal always happen on the client side, in this case it happens locally using the underlying mnsig-client library that the expressjs server running on localhost:7711 calls into. curl -X POST http://localhost:7711/transaction/proposal/sign \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": ...., "xpriv": <your xpriv key>}' The content for the proposal field is the contents of data.proposal returned in the request above. It's not possible to sign with the same key twice, if it happens by mistake you'll get an error message indicating that. The return data is the same as above but with an updated raw transaction and an indication of who signed it. At this point the proposal state is either "partial" or "complete". If it's "partial" then this wallet requires more signatures. If it's complete then it can be broadcasted. * Broadcasting a proposal After a proposal gets to the "complete" state it must be broadcasted to the network so the recipient can see it. curl -X POST http://localhost:7711/transaction/broadcast \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": ....}' The content for the proposal key must be a proposal that's in the "complete" state, otherwise it will be rejected. * Getting a specific proposal curl -X GET http://localhost:7711/transaction/<wallet name>/proposal/<id> \ -H 'Instance: <your instance>' -H 'Token: <your token>' The returned data is almost the same as that returned when starting a proposal, the differences are: -> There's no fee_rate field. In this case, if the proposal is still marked as "unprepared" it might be required to get a fee rate estimation before proceeding (there's an API call for that, described above) if you're using some custom signing method. -> It includes a rawtx field. This will be null initially and a string after it moves away from the unprepared state. * Cancel a proposal Proposals can be canceled at every state except "broadcasted". curl -X DELETE http://localhost:7711/transaction/<wallet name>/proposal/<id> \ -H 'Instance: <your instance>' -H 'Token: <your token>' * Expected response format: { "data": { "state": "canceled", "id": <id>, "success": true }, "code": 200, "isError": false } * Starting a proposal to many recipients (not supported for Ethereum) curl -X POST http://localhost:7711/transaction/proposal/many \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": {"wallet": <your wallet>, "recipients": {<address 1>: <amount 1>, ...}}' This call also takes an unique external_id if your service has an use for it (it's recommended to use it, see notes above regarding this). From this point on everything else is the same as a proposal for a single recipient. ** Checking for errors ** All the examples above contained responses with `"isError": false` and `"code": 200`. If a response has values different than either of those it must be considered as an error. The general format for a response error is: { isError: true, code: <error code>, data: {<error info>} } ** Advanced management ** Except for Ethereum wallets, from time to time it's necessary to coalesce the receive deposits. For providers with a significant volume it's common to have many small deposits, which gets expensive to move. The first step for handling this is checking the list of UTXOs a wallet has: curl -X GET http://localhost:7711/wallet/<wallet name>/utxos \ -H 'Instance: <your instance>' -H 'Token: <your token>' * Expected response format: { "data": { "data": [ { "redeemScript": "00204d744b0ced5e458eb6da39db18c360a1b14e53283b1e6a8dc2bcfc14752aa8e9", "vout": 0, "safe": true, "txid": "c92ebdad64220368bf956131c8c3be89ca9f8c1d3c0948e1381cbb094c0b8b10", "amount": 0.125, "confirmations": 327, "address": "2MsVp2rH9yjs2QnBRa9CsNo8rKPPSmrXk3t", "solvable": true, "scriptPubKey": "a91402c23dff18a54522d2d5f657b16a096209405f9c87" } ], "success": true }, "code": 200, "isError": false } Pseudo code for coalescing small deposits: threshold = 0.1 // 0.1 BTC in this example is the threshold for small deposits max_utxo = 150 // In this example it will coalesce at most 150 deposits min_utxo = 15 // In this example if there are fewer than 10 small deposits, no proposal is created. dest_address = <address to send funds to> utxo_list = [ .. data.data from response above .. ] total_amount = 0 dust = [] for each item in tuxo_list do if entry.amount < threshold then total_amount = total_amount + entry.amount dust.push(entry) if length(dust) == max_utxo then break if length(dust) < min_utxo: exit // Leave some amount for the initial fee estimation. // This can be further tweaked when preparing a proposal by setting the "manual_fee" key // with a specific fee (note that this is not a fee rate, it's the final transaction fee). total_amount = total_amount - 0.1 if total_amount <= 0: exit // Create a proposal with the specified utxos, sending them to some address. curl -X POST http://localhost:7711/transaction/proposal \ -H 'Instance: <your instance>' -H 'Token: <your token>' -H 'Content-Type: application/json' \ -d '{"proposal": {"wallet": <your wallet>, "dest": dest_address, "amount": total_amount, "utxo_list": utxo_list}}'
  • mnsig webhooks - /gpg/mnsig-webhooks
    Last edited 6 years ago
    Information about new deposits are sent as POST requests to a configured url. The mnsig backend will retry it in case your server does not respond with status 200 or takes too long to handle it. If your system does a lot of custom processing with the received data, it's recommended to queue that and quickly send back any response with status 200. mnsig encrypts the data transmitted using libsodium. To decrypt the data received you need the mnsig webhook public key for your instance. In order to get mnsig to setup webhooks for you, the first step is to create a keypair used specifically for this (example in Python): # If it's the first time using this, install the libsodium wrapper: # pip install PyNaCl import binascii from nacl.public import PrivateKey # Keep this secret on your server, it's necessary when attempting to read the webhooks sent to you. secret = PrivateKey.generate() secret_hex = binascii.hexlify(secret.encode()) # Store this result somewhere in order to reuse it. # Share the corresponding public key public_key_hex = binascii.hexlify(secret.public_key.encode()) print public_key_hex When receiving webhooks you need to: import json import binascii from nacl.public import PrivateKey, PublicKey, Box server_public_key_hex = YOUR_MNSIG_INSTANCE_WEBHOOK_KEY server_public_key = PublicKey(binascii.unhexlify(test_server_hex)) my_secret_hex = SECRET_HEX_GENERATED_ABOVE my_secret = PrivateKey(binascii.unhexlify(my_secret_hex)) box = Box(my_secret, server_publickey) raw = box.decrypt(binascii.unhexlify(encrypted_data)) data = json.loads(raw) # This is the data you were looking for. Example of a webhook POST: { "raw": "0aa489b61443d02006f92071b032f3b7ddde69d85bdf4b496993d8bfdf245dc948db089bc0799c4c1b5e43518f0770f03bb6f166a5c52240e896e738a7a89a5e44cca6fdd563b5d2b2e4716b37f726603dca323d74387f4a91d197cfd56fb0501b47cfb551becf364058094094efbcc2d4bb12929eec0a158a99286d2122aa280291ad0e511a4b9b8415d1a2a5c1e5fbe37bea5aee792633f861c4452a766506e9c98b64f9afcd98060b424b257415ce942cb618067a5a403405617524f4fb063ae669f61bf56330f08500916ca013b8bb447336be64fd85fa9b84054fdbb070c0cf207f2118e57adafe8ad1126bad911ca86d138e8323dcfe35b3f19fe80d8aa7e9f1f19180ccaeb24c366b9a27b854bb961e4816ca555ed3ad3bf65f362404c22c26973eafd5b0f9568e6269b6bd23260febcb695f68f0f2a9e98d5e18a325d0d24eec0ca79fc59fceba7f51d40352c95ad0fe4b8eb4df20b67e909bdb4a5a0b01a3468396824976d3f0cf235a2636b2eea57182fa40406f0550069a909c577463184e542278a890d9faa0376117de25c2395f212fb84d946ded2626d578e5a86311eb1a17475fbf4d3805bd6a7c107c972967a89b668e14d79f79d12e79" } Decrypted data: { "proposal_id": null, "complete_date": "2018-01-16T20:01:40.319434", "bestblock_height": 1259257, "txid": "220da9744251251dfa3b7b7eadadaff8922c2c7a8ebaab0adb91f73096901d6e", "note": null, "amount": 130000000, "asset": "testbitcoin_segwit", "tx_out": [], "blockheight": null, "tx_in": [ { "amount": 130000000, "description": { "vout": 0 }, "address": "2NBhooKjSNibfn3CV7M7jLRXHa1Ytyvoh9z" } ], "external_id": null } mnsig will send at least another webhook POST after the transaction gets confirmed, in which case the blockheight key won't be null. For deposits, the keys proposal_id, note, and external_id are always null and tx_out is always an empty list. The asset key the wallet type for this deposit. Note that the entries in the tx_in key always use amounts as integers, in this example the address 2NBhooKjSNibfn3CV7M7jLRXHa1Ytyvoh9z received 1.3 testnet BTC. Example using PHP: * Install this extension https://github.com/jedisct1/libsodium-php before continuing <?php if (!extension_loaded("sodium")) print "install and enable php libsodium extension"; $server_public_key_hex = '<server public key in hex>'; $server_public_key = sodium_hex2bin($server_public_key_hex); $my_secret_hex = '<your secret key in hex>'; $my_secret = sodium_hex2bin($my_secret_hex); $box = sodium_crypto_box_keypair_from_secretkey_and_publickey($my_secret, $server_public_key); $encrypted_data = sodium_hex2bin( '0aa489b61443d02006f92071b032f3b7ddde69d85bdf4b496993d8bfdf245dc948db089bc0799c4c1b5e43518f0770f03bb6f166a5c52240e896e738a7a89a5e44cca6fdd563b5d2b2e4716b37f726603dca323d74387f4a91d197cfd56fb0501b47cfb551becf364058094094efbcc2d4bb12929eec0a158a99286d2122aa280291ad0e511a4b9b8415d1a2a5c1e5fbe37bea5aee792633f861c4452a766506e9c98b64f9afcd98060b424b257415ce942cb618067a5a403405617524f4fb063ae669f61bf56330f08500916ca013b8bb447336be64fd85fa9b84054fdbb070c0cf207f2118e57adafe8ad1126bad911ca86d138e8323dcfe35b3f19fe80d8aa7e9f1f19180ccaeb24c366b9a27b854bb961e4816ca555ed3ad3bf65f362404c22c26973eafd5b0f9568e6269b6bd23260febcb695f68f0f2a9e98d5e18a325d0d24eec0ca79fc59fceba7f51d40352c95ad0fe4b8eb4df20b67e909bdb4a5a0b01a3468396824976d3f0cf235a2636b2eea57182fa40406f0550069a909c577463184e542278a890d9faa0376117de25c2395f212fb84d946ded2626d578e5a86311eb1a17475fbf4d3805bd6a7c107c972967a89b668e14d79f79d12e79' ); $nonce = mb_substr($encrypted_data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($encrypted_data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $raw = sodium_crypto_box_open($ciphertext, $nonce, $box); var_dump($raw); // Decode the JSON string: //$data = json_decode($raw); //var_dump($data); ?>
  • mnsig 1 - /gpg/mnsig-1
    Last edited 6 years ago
    // source code: https://gitlab.com/mnsig/mnsig-client-js // npm install mnsig-client const mnsig = require("mnsig-client") const cli = new mnsig.MNSigClient("mt-test1") cli.token = "mytoken" cli
  • mnsig - cosign all pending proposals - /gpg/mnsig---cosign-all-pending-proposals
    Last edited 7 years ago
    var mnsig = require('mnsig-js-client'); // Adjust all these to match your data. var credentials = { instance: 'instance name', apiToken: 'abctoken', }; var wallet = 'wallet name'; var localPriv = 'xprv...'; var tokenLimit = 5*1e8; // 5 BTC var client = new mnsig.Client(); client.token = credentials.apiToken; client.instance = credentials.instance; function signPending() { client.getProposalsByState({wallet: wallet, state: 'partial'}, function(err, json) { if (err) { console.log('getProposalsByState failed:', err); return; } var proposals = json.proposals; var done = 0; if (!proposals.length) { return setTimeout(signPending, 60 * 1000); } proposals.forEach(function(proposal) { if (proposal.satoshis >= tokenLimit) { // Do not try signing something that is not allowed for this token. done++; if (done == proposals.length) { setTimeout(signPending, 60 * 1000); } return; } console.log('signing proposal', proposal.id); client.localSign(proposal, localPriv, function(err, json) { done++; if (done == proposals.length) { setTimeout(signPending, 60 * 1000); } if (err) { console.log('localSign failed:', err, 'proposal', proposal.id); return; } console.log('signed proposal', proposal.id); }); }); }); } signPending();
  • mnsig - transaction cosign - /gpg/mnsig-transaction-cosign
    Last edited 7 years ago
    var mnsig = require('mnsig-js-client'); // Adjust all these to match your data. var credentials = { instance: 'instance name', apiToken: 'abctoken', }; var wallet = 'wallet name'; var localPriv = 'xprv...'; // In this example a specific proposal id will be cosigned, // change the next value so it matches the one you're interested. var proposalId = 5; var client = new mnsig.Client(); client.token = credentials.apiToken; client.instance = credentials.instance; client.getProposal({wallet: wallet, id: proposalId}, function(err, json) { if (err) { console.log('getProposal failed:', err); return; } var proposal = json; client.localSign(proposal, localPriv, function(err, json) { if (err) { console.log('localSign failed:', err); return; } console.log('signed proposal', json); }); }); console.log('hi');
  • Create mnsig key - /gpg/create-mnsig-key
    Last edited 7 years ago
    var mnsig = require('mnsig-js-client'); // password is not used to generate the key, only to encrypt the output var password = 'verysafe'; // change the following from 'testnet' to 'bitcoin' when using a mainnet wallet var result = mnsig.generateKey(password, 'bip44', 'testnet'); var xpub = result.xpub; var secret = result.xprivEnc; console.log("private key: " + mnsig.decryptKey(password, secret, 'bip44')) console.log("share with mnsig: " + xpub)
  • Image size distribution - /gpg/im-size-distrib
    Last edited 7 years ago
    var Plot = require('plotly-notebook-js');
  • crop it - /gpg/im-crop
    Last edited 7 years ago
    var sc = require('smartcrop'); var fs = require('fs'); var Canvas = require('canvas'), Image = Canvas.Image; var request = require('request'); var url = 'https://scontent-mia1-1.cdninstagram.com/t51.2885-15/e35/891340_341840109273329_991012808_n.jpg?ig_cache_key=MTIxMTc1NjA2Njg2MDY5MzMxNQ%3D%3D.2'; var img = new Image; var cfg = { canvasFactory: function(w, h){ return new Canvas(w, h); }, width: 800, height: 800 }; request.get({url: url, encoding: null}, function(err, response, body) { img.onload = function() { var side = Math.min(img.width, img.height); cfg.width = side; cfg.height = side; sc.crop(img, cfg, function(result) { var canvas = new Canvas(cfg.width, cfg.height); var ctx = canvas.getContext('2d'); var crop = result.topCrop; ctx.patternQuality = 'best'; ctx.filter = 'best'; ctx.drawImage(img, crop.x, crop.y, crop.width, crop.height, 0, 0, canvas.width, canvas.height); console.log(canvas.toBuffer()); }); }; img.onerror = function(err) { console.log(err); }; img.src = body; });