@steemit/steem-uri 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+
2
+ The MIT License (MIT)
3
+ =====================
4
+
5
+ Copyright © 2018 Steemit Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person
8
+ obtaining a copy of this software and associated documentation
9
+ files (the “Software”), to deal in the Software without
10
+ restriction, including without limitation the rights to use,
11
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the
13
+ Software is furnished to do so, subject to the following
14
+ conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+
2
+ Steem URI protocol
3
+ ==================
4
+
5
+ Protocol facilitating signing of steem transactions. Meant to be implemented by secure Steem wallet applications.
6
+
7
+ This repository contains both the specification and a zero dependency reference implementation that works in node.js and most browsers.
8
+
9
+ **Requirements:** Node.js >= 20.
10
+
11
+ Installation
12
+ ------------
13
+
14
+ Via pnpm:
15
+
16
+ ```
17
+ pnpm add @steemit/steem-uri
18
+ ```
19
+
20
+ Build from source: clone the repository, then `pnpm install` and `pnpm run build`. Output is in `lib/` (ESM: `lib/index.js`, CommonJS: `lib/index.cjs`, types: `lib/index.d.ts`).
21
+
22
+
23
+ Example usage
24
+ -------------
25
+
26
+ Encoding operations (CommonJS):
27
+
28
+ ```js
29
+ const steemuri = require('@steemit/steem-uri')
30
+
31
+ steemuri.encodeOp(['vote', {voter: 'foo', author: 'bar', permlink: 'baz', weight: 10000}])
32
+ // steem://sign/op/WyJ2b3RlIix7InZvdGVyIjoiZm9vIiwiYXV0aG9yIjoiYmFyIiwicGVybWxpbmsiOiJiYXoiLCJ3ZWlnaHQiOjEwMDAwfV0.
33
+
34
+ steemuri.encodeOps([
35
+ ['vote', {voter: 'foo', author: 'bar', permlink: 'baz', weight: 10000}],
36
+ ['transfer', {from: 'foo', to: 'bar', amount: '10.000 STEEM', memo: 'baz'}]
37
+ ], {callback: 'https://example.com/wallet?tx={{id}}'})
38
+ // steem://sign/ops/W1sidm90ZSIseyJ2b3RlciI6ImZvbyIsImF1dGhvciI6ImJhciIsInBlcm1saW5rIjoiYmF6Iiwid2VpZ2h0IjoxMDAwMH1dLFsidHJhbnNmZXIiLHsiZnJvbSI6ImZvbyIsInRvIjoiYmFyIiwiYW1vdW50IjoiMTAuMDAwIFNURUVNIiwibWVtbyI6ImJheiJ9XV0.?cb=aHR0cHM6Ly9leGFtcGxlLmNvbS93YWxsZXQ_dHg9e3tpZH19
39
+ ```
40
+
41
+ Decoding and resolving steem:// links (for wallet implementers):
42
+
43
+ ```js
44
+ const steemuri = require('@steemit/steem-uri')
45
+
46
+ // parse the steem:// link
47
+ const parsed = steemuri.decode(link)
48
+
49
+ // resolve the decoded tx and params to a signable tx
50
+ let {tx, signer} = steemuri.resolveTransaction(parsed.tx, parsed.params, {
51
+ // e.g. from a get_dynamic_global_properties call
52
+ ref_block_num: 1234,
53
+ ref_block_prefix: 5678900,
54
+ expiration: '2020-01-01T00:00:00',
55
+ // accounts we are able to sign for
56
+ signers: ['foo', 'bar'],
57
+ // selected signer if none is asked for by the params
58
+ preferred_signer: 'foo',
59
+ })
60
+
61
+ // sign broadcast the transaction to the network
62
+ let signature = signTx(tx, myKeys[signer])
63
+ let confirmation
64
+ if (!parsed.params.no_broadcast) {
65
+ tx.signatures = [signature]
66
+ confirmation = broadcastTx(tx)
67
+ }
68
+
69
+ // redirect to the callback if set
70
+ if (parsed.params.callback) {
71
+ let url = steemuri.resolveCallback(parsed.params.callback, {
72
+ sig: signature,
73
+ id: confirmation.id,
74
+ block: confirmation.block_num,
75
+ txn: confirmation.txn_num,
76
+ })
77
+ redirectTo(url)
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ Specification
84
+ =============
85
+
86
+ A protocol that allows Steem transactions and operations to be encoded into links that can be shared across applications and devices to sign transactions without implementers having to reveal their private key.
87
+
88
+
89
+ Actions
90
+ -------
91
+
92
+ * `steem://sign/tx/<base64u(JSON-encoded tx)>`
93
+ Sign an arbitrary transaction.
94
+ * `steem://sign/op/<base64u(JSON-encoded op)>`
95
+ As above but constructs a transaction around the operation before signing.
96
+ * `steem://sign/ops/<base64u(JSON-encoded op array)>`
97
+ As above but allows multiple operations as an array.
98
+ * `steem://sign/<operation_name>[/operation_params..]`
99
+ Action aliases, see the "Specialized actions" section for more info.
100
+
101
+ To facilitate re-usable signing URIs the implementation allows for a set of placeholder variables that can be used in a signing payload.
102
+
103
+ * `__signer` - Replaced with the username of the signer
104
+ * `__expiration` - Replaced with current time plus some padding to allow the transaction to be broadcast*
105
+ * `__ref_block_num` - Reference block number*
106
+ * `__ref_block_prefix` - Reference block id*
107
+
108
+ *Reasonable values are up to the implementer, suggested expiry time is 60 seconds ahead of rpc node time and the reference block set to the current head block.
109
+
110
+
111
+
112
+ Parameters
113
+ ----------
114
+
115
+ Params are global to all actions and encoded as query string params.
116
+
117
+ * `s` (signer) - Preferred signer, if the implementer has multiple signing keys available it should pre-fill the correct authority. If the implementer does not have a key for the preferred signer a warning should be shown to the user. If omitted the implementer may auto select a signer based on the transaction parameters.
118
+
119
+ * `nb` (no_broadcast) - If set the implementer should only sign the transaction and pass the signature back in the callback.
120
+
121
+ * `cb` (callback) - Base64u encoded url that will be redirected to when the transaction has been signed. The url also allows some templating, see the callback section below for more info.
122
+
123
+ Params uses short names to save space in encoded URIs.
124
+
125
+
126
+ Callbacks
127
+ ---------
128
+
129
+ Callbacks should be redirected to once the transaction has been accepted by the network. If the callback url is a web link only `https` should be allowed.
130
+
131
+ The callbacks also allow simple templating with some response parameters, the templating format is `{{<param_name>}}`, e.g. `https://myapp.com/wallet?tx={{id}}&included_in={{block}}` or `mymobileapp://signed/{{sig}}`
132
+
133
+ Callback template params:
134
+
135
+ * `sig` - Hex-encoded string containing the 65-byte transaction signature
136
+ * `id` - Hex-encoded string containing the 20-byte transaction hash*
137
+ * `block` - The block number the transaction was included in*
138
+ * `txn` - The block transaction index*
139
+
140
+ *Will not be available if the `nb` param was set for the action.
141
+
142
+
143
+
144
+ Base64u
145
+ -------
146
+
147
+ An URL-safe version base64 where `+` is replaced by `-`, `/` by `_` and the `=` padding by `.`.
148
+
149
+ ```
150
+ base64
151
+ SGn+dGhlcmUh/k5pY2X+b2b+eW91/nRv/mRlY29kZf5tZf46KQ==
152
+ base64u
153
+ SGn-dGhlcmUh_k5pY2X-b2b-eW91_nRv_mRlY29kZf5tZf46KQ..
154
+ ```
155
+
156
+ JavaScript implementation:
157
+
158
+ ```js
159
+ b64u_lookup = {'/': '_', '_': '/', '+': '-', '-': '+', '=': '.', '.': '='}
160
+ b64u_enc = (str) => btoa(str).replace(/(\+|\/|=)/g, (m) => b64u_lookup[m])
161
+ b64u_dec = (str) => atob(str.replace(/(-|_|\.)/g, (m) => b64u_lookup[m]))
162
+ ```
163
+
164
+
165
+ Specialized actions
166
+ -------------------
167
+
168
+ To keep the length of the URIs short, and the QR code size manageable, some common operations have aliases. See list below for supported aliases, params noted in operation payloads using the format `{{param_name}}` and optional (`[param_name]`) params should be filled with empty strings unless otherwise specified:
169
+
170
+ ### Transfer tokens
171
+
172
+ Action: `steem://sign/transfer/<username>/<amount>[/memo]`
173
+
174
+ Params:
175
+
176
+ * `username` - User that should be followed by `__signer`
177
+ * `amount` - Amount to transfer, e.g. `1.000 STEEM`
178
+ * `memo` - Base64u encoded memo, optional.
179
+
180
+ Operation:
181
+
182
+ ```json
183
+ ["transfer", {
184
+ "from": "__signer",
185
+ "to": "{{username}}",
186
+ "amount": "{{amount}}",
187
+ "memo": "{{memo}}"
188
+ }]
189
+ ```
190
+
191
+ ### Follow user
192
+
193
+ Action: `steem://sign/follow/<username>`
194
+
195
+ Params:
196
+
197
+ * `<username>` - User that should be followed by `__signer`.
198
+
199
+ Operation:
200
+
201
+ ```json
202
+ ["custom_json", {
203
+ "required_auths": [],
204
+ "required_posting_auths": ["__signer"],
205
+ "id": "follow",
206
+ "json": "[\"follow\",{\"follower\":\"__signer\",\"following\":\"{{username}}\",\"what\":[\"blog\"]}]"
207
+ }]
208
+ ```
209
+
210
+
211
+ Examples
212
+ --------
213
+
214
+ Example usage of the protocol along with data for every step that can be used in tests. Examples assumes a `__signer` of `foo` a `__expiration` of `1970-01-01T00:00:00` and `__ref_block_num`, `__ref_block_prefix` set to `0` unless otherwise stated.
215
+
216
+ ### Send a limit order
217
+
218
+ Might be requested from a trading app, here we don't use any templating since we never want the transaction to be reusable.
219
+
220
+ Transaction:
221
+
222
+ ```json
223
+ {
224
+ "ref_block_num": 48872,
225
+ "ref_block_prefix": 1543858519,
226
+ "expiration": "2018-05-29T13:17:39",
227
+ "extensions": [],
228
+ "operations": [
229
+ ["limit_order_create2", {
230
+ "owner": "foo",
231
+ "orderid": 1,
232
+ "amount_to_sell": "10.000 STEEM",
233
+ "fill_or_kill": false,
234
+ "exchange_rate": {"base": "1.000 STEEM", "quote": "0.420 SBD"},
235
+ "expiration": "2018-05-30T00:00:00"
236
+ }]
237
+ ]
238
+ }
239
+ ```
240
+
241
+ Parameters:
242
+
243
+ ```json
244
+ {
245
+ "signer": "foo",
246
+ "callback": "https://steem.trader/sign_callback?id={{id}}"
247
+ }
248
+ ```
249
+
250
+ Encoded:
251
+
252
+ ```
253
+ steem://sign/tx/eyJyZWZfYmxvY2tfbnVtIjo0ODg3MiwicmVmX2Jsb2NrX3ByZWZpeCI6MTU0Mzg1ODUxOSwiZXhwaXJhdGlvbiI6IjIwMTgtMDUtMjlUMTM6MTc6MzkiLCJleHRlbnNpb25zIjpbXSwib3BlcmF0aW9ucyI6W1sibGltaXRfb3JkZXJfY3JlYXRlMiIseyJvd25lciI6ImZvbyIsIm9yZGVyaWQiOjEsImFtb3VudF90b19zZWxsIjoiMTAuMDAwIFNURUVNIiwiZmlsbF9vcl9raWxsIjpmYWxzZSwiZXhjaGFuZ2VfcmF0ZSI6eyJiYXNlIjoiMS4wMDAgU1RFRU0iLCJxdW90ZSI6IjAuNDIwIFNCRCJ9LCJleHBpcmF0aW9uIjoiMjAxOC0wNS0zMFQwMDowMDowMCJ9XV19?s=foo&cb=aHR0cHM6Ly9zdGVlbS50cmFkZXIvc2lnbl9jYWxsYmFjaz9pZD17e2lkfX0.
254
+ ```
255
+
256
+ ### Witness vote
257
+
258
+ Reusable witness vote URI, e.g. for a "Vote for me!" QR code t-shirt.
259
+
260
+ Operation:
261
+
262
+ ```json
263
+ ["account_witness_vote", {
264
+ "account": "__signer",
265
+ "witness": "jesta",
266
+ "approve": true
267
+ }]
268
+ ```
269
+
270
+ Encoded:
271
+
272
+ ```
273
+ steem://sign/op/WyJhY2NvdW50X3dpdG5lc3Nfdm90ZSIseyJhY2NvdW50IjoiX19zaWduZXIiLCJ3aXRuZXNzIjoiamVzdGEiLCJhcHByb3ZlIjp0cnVlfV0.
274
+ ```
275
+
276
+
277
+ ### Multisig
278
+
279
+ To sign for an account setup with multiple authorities a central service can act as a transaction facilitator using the `nb` (no_broadcast) option.
280
+
281
+ In the following scenario the account `foo` is setup with an active authority that has three account auths belonging to `bob`, `alice` and `picard`, the weights are setup so that two of those three accounts needs to sign.
282
+
283
+ `bob` wants to transfer `150.000 STEEM` from the `foo` account to himself so he submits an operation to the signing service:
284
+
285
+ ```json
286
+ ["transfer", {
287
+ "from": "foo",
288
+ "to": "bob",
289
+ "amount": "150.000 STEEM",
290
+ "memo": "Bob's boat needs plastic padding"
291
+ }]
292
+ ```
293
+
294
+ The service then generates a signing URI with that operation and the following options:
295
+
296
+ ```json
297
+ {
298
+ "no_broadcast": true,
299
+ "callback": "https://sign.steem.vc/collect?id=123&sig={{sig}}"
300
+ }
301
+ ```
302
+
303
+ ```
304
+ steem://sign/op/WyJ0cmFuc2ZlciIseyJmcm9tIjoiZm9vIiwidG8iOiJib2IiLCJhbW91bnQiOiIxNTAuMDAwIFNURUVNIiwibWVtbyI6IkJvYidzIGJvYXQgbmVlZHMgcGxhc3RpYyBwYWRkaW5nIn1d?nb=&cb=aHR0cHM6Ly9zaWduLnN0ZWVtLnZjL2NvbGxlY3Q_aWQ9MTIzJnNpZz17e3NpZ319
305
+ ```
306
+
307
+ `bob` then signs the transaction using the URI, the service callback is pinged and the service now has his signature. Then he sends the URI to `alice` and `picard` and when one of them signs it the service has enough signatures it broadcasts the transaction.
308
+
309
+ The UX of a service like this can be excellent with the help of QR codes and collecting emails for signers so they can be notified when a signature is needed and when the transaction is broadcast.
package/lib/index.cjs ADDED
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Steem URI Signing Protocol
4
+ * @author Johan Nordberg <johan@steemit.com>
5
+ * @refector by @ety001
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.decode = decode;
9
+ exports.resolveTransaction = resolveTransaction;
10
+ exports.resolveCallback = resolveCallback;
11
+ exports.encodeTx = encodeTx;
12
+ exports.encodeOp = encodeOp;
13
+ exports.encodeOps = encodeOps;
14
+ // Assumes node.js if any of the utils needed are missing.
15
+ if (typeof URL === 'undefined') {
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- polyfill for older envs
17
+ global['URL'] = require('url').URL;
18
+ }
19
+ if (typeof URLSearchParams === 'undefined') {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- polyfill for older envs
21
+ global['URLSearchParams'] = require('url').URLSearchParams;
22
+ }
23
+ if (typeof btoa === 'undefined') {
24
+ global['btoa'] = (str) => Buffer.from(str, 'latin1').toString('base64');
25
+ }
26
+ if (typeof atob === 'undefined') {
27
+ global['atob'] = (str) => Buffer.from(str, 'base64').toString('latin1');
28
+ }
29
+ /// URL-safe Base64 encoding and decoding.
30
+ const B64U_LOOKUP = { '/': '_', '_': '/', '+': '-', '-': '+', '=': '.', '.': '=' };
31
+ const b64uEnc = (str) => btoa(str).replace(/(\+|\/|=)/g, (m) => B64U_LOOKUP[m]);
32
+ const b64uDec = (str) => atob(str.replace(/(-|_|\.)/g, (m) => B64U_LOOKUP[m]));
33
+ /**
34
+ * Parse a steem:// protocol link.
35
+ * @param steemUrl The `steem:` url to parse.
36
+ * @throws If the url can not be parsed.
37
+ * @returns The resolved transaction and parameters.
38
+ */
39
+ function decode(steemUrl) {
40
+ const url = new URL(steemUrl);
41
+ if (url.protocol !== 'steem:') {
42
+ throw new Error(`Invalid protocol, expected 'steem:' got '${url.protocol}'`);
43
+ }
44
+ if (url.host !== 'sign') {
45
+ throw new Error(`Invalid action, expected 'sign' got '${url.host}'`);
46
+ }
47
+ const [type, rawPayload] = url.pathname.split('/').slice(1);
48
+ let payload;
49
+ try {
50
+ payload = JSON.parse(b64uDec(rawPayload));
51
+ }
52
+ catch (error) {
53
+ error.message = `Invalid payload: ${error.message}`;
54
+ throw error;
55
+ }
56
+ let tx;
57
+ switch (type) {
58
+ case 'tx':
59
+ tx = payload;
60
+ break;
61
+ case 'op':
62
+ case 'ops': {
63
+ const operations = type === 'ops' ? payload : [payload];
64
+ tx = {
65
+ ref_block_num: '__ref_block_num',
66
+ ref_block_prefix: '__ref_block_prefix',
67
+ expiration: '__expiration',
68
+ extensions: [],
69
+ operations,
70
+ };
71
+ break;
72
+ }
73
+ // case 'transfer':
74
+ // case 'follow':
75
+ default:
76
+ throw new Error(`Invalid signing action '${type}'`);
77
+ }
78
+ const params = {};
79
+ if (url.searchParams.has('cb')) {
80
+ params.callback = b64uDec(url.searchParams.get('cb'));
81
+ }
82
+ if (url.searchParams.has('nb')) {
83
+ params.no_broadcast = true;
84
+ }
85
+ if (url.searchParams.has('s')) {
86
+ params.signer = url.searchParams.get('s');
87
+ }
88
+ return { tx, params };
89
+ }
90
+ const RESOLVE_PATTERN = /(__(ref_block_(num|prefix)|expiration|signer))/g;
91
+ /**
92
+ * Resolves placeholders in a transaction.
93
+ * @param utx Unresolved transaction data.
94
+ * @param params Protocol parameters.
95
+ * @param options Values to use when resolving.
96
+ * @returns The resolved transaction and signer.
97
+ */
98
+ function resolveTransaction(utx, params, options) {
99
+ const signer = params.signer || options.preferred_signer;
100
+ if (!options.signers.includes(signer)) {
101
+ throw new Error(`Signer '${signer}' not available`);
102
+ }
103
+ const ctx = {
104
+ __ref_block_num: options.ref_block_num,
105
+ __ref_block_prefix: options.ref_block_prefix,
106
+ __expiration: options.expiration,
107
+ __signer: signer,
108
+ };
109
+ const walk = (val) => {
110
+ let type = typeof val;
111
+ if (type === 'object' && Array.isArray(val)) {
112
+ type = 'array';
113
+ }
114
+ else if (val === null) {
115
+ type = 'null';
116
+ }
117
+ switch (type) {
118
+ case 'string':
119
+ return val.replace(RESOLVE_PATTERN, (m) => ctx[m]);
120
+ case 'array':
121
+ return val.map(walk);
122
+ case 'object': {
123
+ const rv = {};
124
+ for (const [k, v] of Object.entries(val)) {
125
+ rv[k] = walk(v);
126
+ }
127
+ return rv;
128
+ }
129
+ default:
130
+ return val;
131
+ }
132
+ };
133
+ const tx = walk(utx);
134
+ return { signer, tx };
135
+ }
136
+ const CALLBACK_RESOLVE_PATTERN = /({{(sig|id|block|txn)}})/g;
137
+ /**
138
+ * Resolves template vars in a callback url.
139
+ * @param url The callback url.
140
+ * @param ctx Values to use when resolving.
141
+ * @returns The resolved url.
142
+ */
143
+ function resolveCallback(url, ctx) {
144
+ return url.replace(CALLBACK_RESOLVE_PATTERN, (_1, _2, m) => ctx[m] || '');
145
+ }
146
+ /*** Internal helper to encode Parameters to a querystring. */
147
+ function encodeParameters(params) {
148
+ const out = new URLSearchParams();
149
+ if (params.no_broadcast === true) {
150
+ out.set('nb', '');
151
+ }
152
+ if (params.signer) {
153
+ out.set('s', params.signer);
154
+ }
155
+ if (params.callback) {
156
+ out.set('cb', b64uEnc(params.callback));
157
+ }
158
+ let qs = out.toString();
159
+ if (qs.length > 0) {
160
+ qs = '?' + qs;
161
+ }
162
+ return qs;
163
+ }
164
+ /** Internal helper to encode a tx or op to a b64u+json payload. */
165
+ function encodeJson(data) {
166
+ return b64uEnc(JSON.stringify(data, null, 0));
167
+ }
168
+ /** Encodes a Steem transaction to a steem: URI. */
169
+ function encodeTx(tx, params = {}) {
170
+ return `steem://sign/tx/${encodeJson(tx)}${encodeParameters(params)}`;
171
+ }
172
+ /** Encodes a Steem operation to a steem: URI. */
173
+ function encodeOp(op, params = {}) {
174
+ return `steem://sign/op/${encodeJson(op)}${encodeParameters(params)}`;
175
+ }
176
+ /** Encodes several Steem operations to a steem: URI. */
177
+ function encodeOps(ops, params = {}) {
178
+ return `steem://sign/ops/${encodeJson(ops)}${encodeParameters(params)}`;
179
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Steem URI Signing Protocol
3
+ * @author Johan Nordberg <johan@steemit.com>
4
+ * @refector by @ety001
5
+ */
6
+ import type { Operation, Transaction } from '@steemit/steem-js';
7
+ /**
8
+ * Protocol parameters.
9
+ */
10
+ export interface Parameters {
11
+ /** Requested signer. */
12
+ signer?: string;
13
+ /** Redurect uri. */
14
+ callback?: string;
15
+ /** Whether to just sign the transaction. */
16
+ no_broadcast?: boolean;
17
+ }
18
+ /**
19
+ * A transactions that may contain placeholders.
20
+ */
21
+ export interface UnresolvedTransaction extends Transaction {
22
+ ref_block_num: any;
23
+ ref_block_prefix: any;
24
+ expiration: any;
25
+ operations: any[];
26
+ }
27
+ /**
28
+ * Decoding result.
29
+ */
30
+ export interface DecodeResult {
31
+ /**
32
+ * Decoded transaction. May have placeholders, use {@link resolve} to
33
+ * resolve them into a signable transaction.
34
+ */
35
+ tx: UnresolvedTransaction;
36
+ /**
37
+ * Decoded protocol parameters.
38
+ */
39
+ params: Parameters;
40
+ }
41
+ /**
42
+ * Parse a steem:// protocol link.
43
+ * @param steemUrl The `steem:` url to parse.
44
+ * @throws If the url can not be parsed.
45
+ * @returns The resolved transaction and parameters.
46
+ */
47
+ export declare function decode(steemUrl: string): DecodeResult;
48
+ /**
49
+ * Transaction resolving options.
50
+ */
51
+ export interface ResolveOptions {
52
+ /** The ref block number used to fill in the `__ref_block_num` placeholder. */
53
+ ref_block_num: number;
54
+ /** The ref block prefix used to fill in the `__ref_block_prefix` placeholder. */
55
+ ref_block_prefix: number;
56
+ /** The date string used to fill in the `__expiration` placeholder. */
57
+ expiration: string;
58
+ /** List of account names avialable as signers. */
59
+ signers: string[];
60
+ /** Preferred signer if none is explicitly set in params. */
61
+ preferred_signer: string;
62
+ }
63
+ /**
64
+ * Transaction resolving result.
65
+ */
66
+ export interface ResolveResult {
67
+ /** The resolved transaction ready to be signed. */
68
+ tx: Transaction;
69
+ /** The account that should sign the transaction. */
70
+ signer: string;
71
+ }
72
+ /**
73
+ * Resolves placeholders in a transaction.
74
+ * @param utx Unresolved transaction data.
75
+ * @param params Protocol parameters.
76
+ * @param options Values to use when resolving.
77
+ * @returns The resolved transaction and signer.
78
+ */
79
+ export declare function resolveTransaction(utx: UnresolvedTransaction, params: Parameters, options: ResolveOptions): ResolveResult;
80
+ /**
81
+ * Transaction confirmation including signature.
82
+ */
83
+ export interface TransactionConfirmation {
84
+ /** Transaction signature. */
85
+ sig: string;
86
+ /** Transaction hash. */
87
+ id?: string;
88
+ /** Block number transaction was included in. */
89
+ block?: number;
90
+ /** Transaction index in block. */
91
+ txn?: number;
92
+ }
93
+ /**
94
+ * Resolves template vars in a callback url.
95
+ * @param url The callback url.
96
+ * @param ctx Values to use when resolving.
97
+ * @returns The resolved url.
98
+ */
99
+ export declare function resolveCallback(url: string, ctx: TransactionConfirmation): string;
100
+ /** Encodes a Steem transaction to a steem: URI. */
101
+ export declare function encodeTx(tx: Transaction, params?: Parameters): string;
102
+ /** Encodes a Steem operation to a steem: URI. */
103
+ export declare function encodeOp(op: Operation, params?: Parameters): string;
104
+ /** Encodes several Steem operations to a steem: URI. */
105
+ export declare function encodeOps(ops: Operation, params?: Parameters): string;
package/lib/index.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Steem URI Signing Protocol
3
+ * @author Johan Nordberg <johan@steemit.com>
4
+ * @refector by @ety001
5
+ */
6
+ // Assumes node.js if any of the utils needed are missing.
7
+ if (typeof URL === 'undefined') {
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- polyfill for older envs
9
+ global['URL'] = require('url').URL;
10
+ }
11
+ if (typeof URLSearchParams === 'undefined') {
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- polyfill for older envs
13
+ global['URLSearchParams'] = require('url').URLSearchParams;
14
+ }
15
+ if (typeof btoa === 'undefined') {
16
+ global['btoa'] = (str) => Buffer.from(str, 'latin1').toString('base64');
17
+ }
18
+ if (typeof atob === 'undefined') {
19
+ global['atob'] = (str) => Buffer.from(str, 'base64').toString('latin1');
20
+ }
21
+ /// URL-safe Base64 encoding and decoding.
22
+ const B64U_LOOKUP = { '/': '_', '_': '/', '+': '-', '-': '+', '=': '.', '.': '=' };
23
+ const b64uEnc = (str) => btoa(str).replace(/(\+|\/|=)/g, (m) => B64U_LOOKUP[m]);
24
+ const b64uDec = (str) => atob(str.replace(/(-|_|\.)/g, (m) => B64U_LOOKUP[m]));
25
+ /**
26
+ * Parse a steem:// protocol link.
27
+ * @param steemUrl The `steem:` url to parse.
28
+ * @throws If the url can not be parsed.
29
+ * @returns The resolved transaction and parameters.
30
+ */
31
+ export function decode(steemUrl) {
32
+ const url = new URL(steemUrl);
33
+ if (url.protocol !== 'steem:') {
34
+ throw new Error(`Invalid protocol, expected 'steem:' got '${url.protocol}'`);
35
+ }
36
+ if (url.host !== 'sign') {
37
+ throw new Error(`Invalid action, expected 'sign' got '${url.host}'`);
38
+ }
39
+ const [type, rawPayload] = url.pathname.split('/').slice(1);
40
+ let payload;
41
+ try {
42
+ payload = JSON.parse(b64uDec(rawPayload));
43
+ }
44
+ catch (error) {
45
+ error.message = `Invalid payload: ${error.message}`;
46
+ throw error;
47
+ }
48
+ let tx;
49
+ switch (type) {
50
+ case 'tx':
51
+ tx = payload;
52
+ break;
53
+ case 'op':
54
+ case 'ops': {
55
+ const operations = type === 'ops' ? payload : [payload];
56
+ tx = {
57
+ ref_block_num: '__ref_block_num',
58
+ ref_block_prefix: '__ref_block_prefix',
59
+ expiration: '__expiration',
60
+ extensions: [],
61
+ operations,
62
+ };
63
+ break;
64
+ }
65
+ // case 'transfer':
66
+ // case 'follow':
67
+ default:
68
+ throw new Error(`Invalid signing action '${type}'`);
69
+ }
70
+ const params = {};
71
+ if (url.searchParams.has('cb')) {
72
+ params.callback = b64uDec(url.searchParams.get('cb'));
73
+ }
74
+ if (url.searchParams.has('nb')) {
75
+ params.no_broadcast = true;
76
+ }
77
+ if (url.searchParams.has('s')) {
78
+ params.signer = url.searchParams.get('s');
79
+ }
80
+ return { tx, params };
81
+ }
82
+ const RESOLVE_PATTERN = /(__(ref_block_(num|prefix)|expiration|signer))/g;
83
+ /**
84
+ * Resolves placeholders in a transaction.
85
+ * @param utx Unresolved transaction data.
86
+ * @param params Protocol parameters.
87
+ * @param options Values to use when resolving.
88
+ * @returns The resolved transaction and signer.
89
+ */
90
+ export function resolveTransaction(utx, params, options) {
91
+ const signer = params.signer || options.preferred_signer;
92
+ if (!options.signers.includes(signer)) {
93
+ throw new Error(`Signer '${signer}' not available`);
94
+ }
95
+ const ctx = {
96
+ __ref_block_num: options.ref_block_num,
97
+ __ref_block_prefix: options.ref_block_prefix,
98
+ __expiration: options.expiration,
99
+ __signer: signer,
100
+ };
101
+ const walk = (val) => {
102
+ let type = typeof val;
103
+ if (type === 'object' && Array.isArray(val)) {
104
+ type = 'array';
105
+ }
106
+ else if (val === null) {
107
+ type = 'null';
108
+ }
109
+ switch (type) {
110
+ case 'string':
111
+ return val.replace(RESOLVE_PATTERN, (m) => ctx[m]);
112
+ case 'array':
113
+ return val.map(walk);
114
+ case 'object': {
115
+ const rv = {};
116
+ for (const [k, v] of Object.entries(val)) {
117
+ rv[k] = walk(v);
118
+ }
119
+ return rv;
120
+ }
121
+ default:
122
+ return val;
123
+ }
124
+ };
125
+ const tx = walk(utx);
126
+ return { signer, tx };
127
+ }
128
+ const CALLBACK_RESOLVE_PATTERN = /({{(sig|id|block|txn)}})/g;
129
+ /**
130
+ * Resolves template vars in a callback url.
131
+ * @param url The callback url.
132
+ * @param ctx Values to use when resolving.
133
+ * @returns The resolved url.
134
+ */
135
+ export function resolveCallback(url, ctx) {
136
+ return url.replace(CALLBACK_RESOLVE_PATTERN, (_1, _2, m) => ctx[m] || '');
137
+ }
138
+ /*** Internal helper to encode Parameters to a querystring. */
139
+ function encodeParameters(params) {
140
+ const out = new URLSearchParams();
141
+ if (params.no_broadcast === true) {
142
+ out.set('nb', '');
143
+ }
144
+ if (params.signer) {
145
+ out.set('s', params.signer);
146
+ }
147
+ if (params.callback) {
148
+ out.set('cb', b64uEnc(params.callback));
149
+ }
150
+ let qs = out.toString();
151
+ if (qs.length > 0) {
152
+ qs = '?' + qs;
153
+ }
154
+ return qs;
155
+ }
156
+ /** Internal helper to encode a tx or op to a b64u+json payload. */
157
+ function encodeJson(data) {
158
+ return b64uEnc(JSON.stringify(data, null, 0));
159
+ }
160
+ /** Encodes a Steem transaction to a steem: URI. */
161
+ export function encodeTx(tx, params = {}) {
162
+ return `steem://sign/tx/${encodeJson(tx)}${encodeParameters(params)}`;
163
+ }
164
+ /** Encodes a Steem operation to a steem: URI. */
165
+ export function encodeOp(op, params = {}) {
166
+ return `steem://sign/op/${encodeJson(op)}${encodeParameters(params)}`;
167
+ }
168
+ /** Encodes several Steem operations to a steem: URI. */
169
+ export function encodeOps(ops, params = {}) {
170
+ return `steem://sign/ops/${encodeJson(ops)}${encodeParameters(params)}`;
171
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@steemit/steem-uri",
3
+ "version": "0.2.0",
4
+ "description": "Steem URI parser and encoder",
5
+ "license": "MIT",
6
+ "main": "./lib/index.cjs",
7
+ "module": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./lib/index.js",
12
+ "require": "./lib/index.cjs",
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "lib/*"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.39.1",
28
+ "@steemit/steem-js": "^1.0.12",
29
+ "@types/node": "^20",
30
+ "eslint": "^9.0.0",
31
+ "typescript": "^5.7.3",
32
+ "typescript-eslint": "^8.48.0"
33
+ },
34
+ "scripts": {
35
+ "build": "pnpm run clean && tsc -p tsconfig.cjs.json && mv lib/index.js lib/index.cjs && tsc -p tsconfig.esm.json",
36
+ "prepublish": "pnpm run build",
37
+ "lint": "eslint src",
38
+ "clean": "rm -rf lib/"
39
+ }
40
+ }