diff --git a/src/index.ts b/src/index.ts index cd9c6b3d..83416453 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,9 @@ import { getEncodedTokenV4, getDecodedToken, deriveKeysetId, - decodePaymentRequest + decodePaymentRequest, + rawTokenToToken, + tokenToRawToken } from './utils.js'; export * from './model/types/index.js'; @@ -22,5 +24,7 @@ export { getEncodedTokenV4, decodePaymentRequest, deriveKeysetId, - setGlobalRequestOptions + setGlobalRequestOptions, + rawTokenToToken, + tokenToRawToken }; diff --git a/src/utils.ts b/src/utils.ts index 0d92b7ad..88caba06 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -228,6 +228,17 @@ export function getEncodedTokenV4(token: Token): string { if (nonHex) { throw new Error('can not encode to v4 token if proofs contain non-hex keyset id'); } + + const tokenTemplate = templateFromToken(token); + + const encodedData = encodeCBOR(tokenTemplate); + const prefix = 'cashu'; + const version = 'B'; + const base64Data = encodeUint8toBase64Url(encodedData); + return prefix + version + base64Data; +} + +function templateFromToken(token: Token): TokenV4Template { const idMap: { [id: string]: Array } = {}; const mint = token.mint; for (let i = 0; i < token.proofs.length; i++) { @@ -261,16 +272,36 @@ export function getEncodedTokenV4(token: Token): string { }) ) } as TokenV4Template; - if (token.memo) { tokenTemplate.d = token.memo; } + return tokenTemplate; +} - const encodedData = encodeCBOR(tokenTemplate); - const prefix = 'cashu'; - const version = 'B'; - const base64Data = encodeUint8toBase64Url(encodedData); - return prefix + version + base64Data; +function tokenFromTemplate(template: TokenV4Template): Token { + const proofs: Array = []; + template.t.forEach((t) => + t.p.forEach((p) => { + proofs.push({ + secret: p.s, + C: bytesToHex(p.c), + amount: p.a, + id: bytesToHex(t.i), + ...(p.d && { + dleq: { + r: bytesToHex(p.d.r), + s: bytesToHex(p.d.s), + e: bytesToHex(p.d.e) + } as SerializedDLEQ + }) + }); + }) + ); + const decodedToken: Token = { mint: template.m, proofs, unit: template.u || 'sat' }; + if (template.d) { + decodedToken.memo = template.d; + } + return decodedToken; } /** @@ -316,28 +347,7 @@ export function handleTokens(token: string): Token { } else if (version === 'B') { const uInt8Token = encodeBase64toUint8(encodedToken); const tokenData = decodeCBOR(uInt8Token) as TokenV4Template; - const proofs: Array = []; - tokenData.t.forEach((t) => - t.p.forEach((p) => { - proofs.push({ - secret: p.s, - C: bytesToHex(p.c), - amount: p.a, - id: bytesToHex(t.i), - ...(p.d && { - dleq: { - r: bytesToHex(p.d.r), - s: bytesToHex(p.d.s), - e: bytesToHex(p.d.e) - } as SerializedDLEQ - }) - }); - }) - ); - const decodedToken: Token = { mint: tokenData.m, proofs, unit: tokenData.u || 'sat' }; - if (tokenData.d) { - decodedToken.memo = tokenData.d; - } + const decodedToken = tokenFromTemplate(tokenData); return decodedToken; } throw new Error('Token version is not supported'); @@ -521,3 +531,16 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { return true; } + +export function tokenToRawToken(token: string | Token): Uint8Array { + if (typeof token === 'string') { + token = getDecodedToken(token); + } + const template = templateFromToken(token); + return encodeCBOR(template); +} + +export function rawTokenToToken(bytes: Uint8Array): Token { + const decoded = decodeCBOR(bytes) as TokenV4Template; + return tokenFromTemplate(decoded); +} diff --git a/test/utils.test.ts b/test/utils.test.ts index cafa3de0..b716ec27 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,7 +3,7 @@ import { constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; -import { Keys, Proof } from '../src/model/types/index.js'; +import { Keys, Proof, Token } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; import { createDLEQProof } from '@cashu/crypto/modules/mint/NUT12'; @@ -414,3 +414,31 @@ describe('test zero-knowledge utilities', () => { expect(exc).toEqual(new Error('undefined key for amount 1')); }); }); + +describe('test raw tokens', () => { + test('raw token from token', () => { + const token: Token = { + mint: 'https://example.com/mint', + proofs: [ + { + id: '009a1f293253e41e', + amount: 1, + secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', + C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf' + } + ], + memo: 'memo', + unit: 'sat' + }; + const tokenHex = + 'a4616d781868747470733a2f2f6578616d706c652e636f6d2f6d696e74617563736174617481a2616948009a1f293253e41e617081a36161016173784061636331323433356537623834383463336366313835303134393231386166393066373136613532626634613565643334376534386563633133663737333838616358210244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf6164646d656d6f'; + + const rawToken = utils.tokenToRawToken(token); + const rawTokenHex = bytesToHex(rawToken); + expect(rawTokenHex).toBe(tokenHex); + + const tokenBytes = hexToBytes(tokenHex); + const decodedToken = utils.rawTokenToToken(tokenBytes); + expect(decodedToken).toEqual(token); + }); +});