Skip to content

Commit

Permalink
Merge pull request #207 from cashubtc/raw-token-utils
Browse files Browse the repository at this point in the history
raw token converters
  • Loading branch information
Egge21M authored Dec 20, 2024
2 parents 0593ad1 + 3f3fb30 commit 7bb2936
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 31 deletions.
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
getEncodedTokenV4,
getDecodedToken,
deriveKeysetId,
decodePaymentRequest
decodePaymentRequest,
getDecodedTokenBinary,
getEncodedTokenBinary
} from './utils.js';

export * from './model/types/index.js';
Expand All @@ -21,7 +23,9 @@ export {
getEncodedTokenV4,
decodePaymentRequest,
deriveKeysetId,
setGlobalRequestOptions
setGlobalRequestOptions,
getDecodedTokenBinary,
getEncodedTokenBinary
};

export { injectWebSocketImpl } from './ws.js';
98 changes: 70 additions & 28 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Proof> } = {};
const mint = token.mint;
for (let i = 0; i < token.proofs.length; i++) {
Expand Down Expand Up @@ -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<Proof> = [];
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;
}

/**
Expand Down Expand Up @@ -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<Proof> = [];
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');
Expand Down Expand Up @@ -521,3 +531,35 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean {

return true;
}

function concatByteArrays(...arrays: Array<Uint8Array>): Uint8Array {
const totalLength = arrays.reduce((a, c) => a + c.length, 0);
const byteArray = new Uint8Array(totalLength);
let pointer = 0;
for (let i = 0; i < arrays.length; i++) {
byteArray.set(arrays[i], pointer);
pointer = pointer + arrays[i].length;
}
return byteArray;
}

export function getEncodedTokenBinary(token: Token): Uint8Array {
const utf8Encoder = new TextEncoder();
const template = templateFromToken(token);
const binaryTemplate = encodeCBOR(template);
const prefix = utf8Encoder.encode('craw');
const version = utf8Encoder.encode('B');
return concatByteArrays(prefix, version, binaryTemplate);
}

export function getDecodedTokenBinary(bytes: Uint8Array): Token {
const utfDecoder = new TextDecoder();
const prefix = utfDecoder.decode(bytes.slice(0, 4));
const version = utfDecoder.decode(new Uint8Array([bytes[4]]));
if (prefix !== 'craw' || version !== 'B') {
throw new Error('not a valid binary token');
}
const binaryToken = bytes.slice(5);
const decoded = decodeCBOR(binaryToken) as TokenV4Template;
return tokenFromTemplate(decoded);
}
33 changes: 32 additions & 1 deletion test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
serializeProof
} from '@cashu/crypto/modules/client';
import { test, describe, expect } from 'vitest';
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';
Expand Down Expand Up @@ -415,3 +415,34 @@ describe('test zero-knowledge utilities', () => {
expect(exc).toEqual(new Error('undefined key for amount 1'));
});
});

describe('test raw tokens', () => {
const token: Token = {
mint: 'http://localhost:3338',
proofs: [
{
id: '00ad268c4d1f5826',
amount: 1,
secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e',
C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792'
}
],
memo: 'Thank you',
unit: 'sat'
};

test('bytes to token', () => {
const exptectedBytes = hexToBytes(
'6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174'
);

const decodedToken = utils.getDecodedTokenBinary(exptectedBytes);
expect(decodedToken).toEqual(token);
});

test('token to bytes', () => {
const bytes = utils.getEncodedTokenBinary(token);
const decodedToken = utils.getDecodedTokenBinary(bytes);
expect(decodedToken).toEqual(token);
});
});

0 comments on commit 7bb2936

Please sign in to comment.