mnsig client proxy

node v8.17.0
version: 2.2.0
endpointsharetweet
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}}'
Loading…

no comments

    sign in to comment