Skip to content

Commit

Permalink
Use Albatross WASM library to create and sign transactions
Browse files Browse the repository at this point in the history
Only regular transactions and staking. Swap txs are not yet converted.
  • Loading branch information
sisou committed Jul 12, 2023
1 parent 9e72d25 commit 0f3039d
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 54 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"env": {
"browser": true
"browser": true
},
"globals": {
"BigInt": true
},
"extends": "airbnb-base",
"plugins": [
Expand Down
5 changes: 4 additions & 1 deletion client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,12 @@ export type DerivedAddress = {
export type KeyResult = SingleKeyResult[];
export type ListResult = KeyInfoObject[];
export type ListLegacyResult = LegacyKeyInfoObject[];
export type SignTransactionResult = SignatureResult;
export type SignTransactionResult = SignatureResult & {
serializedTx: Uint8Array,
};
export type SignStakingResult = SignatureResult & {
data: Uint8Array,
serializedTx: Uint8Array,
};
export type SimpleResult = { success: boolean };
export type SignedBitcoinTransaction = {
Expand Down
8 changes: 4 additions & 4 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ module.exports = function (/** @type {any} */ config) {
// list of files / patterns to load in the browser
files: [
'node_modules/@nimiq/core-web/web-offline.js',
{'pattern': 'node_modules/@nimiq/core-web/worker-wasm.wasm', included: false},
{'pattern': 'node_modules/@nimiq/core-web/worker-wasm.js', included: false},
{'pattern': 'node_modules/@nimiq/core-web/worker.js', included: false},
{pattern: 'node_modules/@nimiq/core-web/worker-wasm.wasm', included: false},
{pattern: 'node_modules/@nimiq/core-web/worker-wasm.js', included: false},
{pattern: 'node_modules/@nimiq/core-web/worker.js', included: false},
'src/lib/*.js', // Force load of lib files before components and common.js
'src/request/TopLevelApi.js', // Force load of TopLevelApi before BitcoinEnabledTopLevelApi
'src/lib/bitcoin/*.js',
'node_modules/ethers/dist/ethers.umd.js',
'src/**/*js',
'src/**/*.js',
'tests/**/*.spec.js',
],

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"homepage": "https://github.com/nimiq/keyguard#readme",
"devDependencies": {
"@nimiq/albatross-wasm": "npm:@nimiq/core-web@next",
"@nimiq/core-web": "1.5.8",
"@nimiq/rpc": "^0.3.0",
"@nimiq/style": "^0.8.3",
Expand Down
3 changes: 2 additions & 1 deletion serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var http = require("http"),
'htm' : 'text/html',
'html' : 'text/html',
'json' : 'application/json',
'js' : 'application/x-javascript',
'js' : 'text/javascript',
'txt' : 'text/plain',
'bmp' : 'image/bmp',
'rss' : 'application/rss+xml',
Expand All @@ -38,6 +38,7 @@ var http = require("http"),
'mpg' : 'video/mpeg',
'mpe' : 'video/mpeg',
'qt' : 'video/quicktime',
'mjs' : 'text/javascript',
'mov' : 'video/quicktime',
'wmv' : 'video/x-ms-wmv',
'mp2' : 'audio/mpeg',
Expand Down
23 changes: 22 additions & 1 deletion src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
/** @type {Promise<void>?} */
let __nimiqLoaded = null;

/** @type {Promise<void>?} */
let __albatrossLoaded = null;

if (navigator.serviceWorker) {
// Register service worker to strip cookie from requests.
// This is on a best-effort basis. Cookies might still be sent to the server, if the service worker is not activated
Expand Down Expand Up @@ -57,7 +60,20 @@ async function loadNimiq() {
}

/**
* @typedef {{loadNimiq: boolean, whitelist: string[]}} Options
* Singleton promise
*
* @returns {Promise<void>}
*/
async function loadAlbatross() {
// eslint-disable-next-line no-return-assign
return __albatrossLoaded || (__albatrossLoaded = new Promise(async resolve => {
await Albatross.default(); // eslint-disable-line no-undef
resolve();
}));
}

/**
* @typedef {{loadNimiq: boolean, loadAlbatross: boolean, whitelist: string[]}} Options
*/

/**
Expand All @@ -69,6 +85,7 @@ async function runKeyguard(RequestApiClass, opts) { // eslint-disable-line no-un
/** @type {Options} */
const defaultOptions = {
loadNimiq: true,
loadAlbatross: false,
whitelist: ['request'],
};

Expand All @@ -79,6 +96,10 @@ async function runKeyguard(RequestApiClass, opts) { // eslint-disable-line no-un
await loadNimiq();
}

if (options.loadAlbatross) {
await loadAlbatross();
}

// If user navigates back to loading screen, skip it
window.addEventListener('hashchange', () => {
if (window.location.hash === '') {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/AlbatrossWasm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as Albatross from '../../node_modules/@nimiq/albatross-wasm/web/index.js';
window.Albatross = Albatross;
93 changes: 54 additions & 39 deletions src/request/sign-staking/SignStaking.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global Nimiq */
/* global Albatross */
/* global Key */
/* global KeyStore */
/* global PasswordBox */
Expand Down Expand Up @@ -149,51 +150,65 @@ class SignStaking {
return;
}

const publicKey = key.derivePublicKey(request.keyPath);

const stakingTypesWithSignatureData = [
SignStakingApi.IncomingStakingType.CREATE_STAKER,
SignStakingApi.IncomingStakingType.UPDATE_STAKER,
];

if (stakingTypesWithSignatureData.includes(request.type)) {
// The tx signature of the staker is set in the data
const signature = key.sign(request.keyPath, request.transaction.serializeContent());
const proof = Nimiq.SignatureProof.singleSig(publicKey, signature);

const data = new Nimiq.SerialBuffer(request.transaction.data);
data.writePos = data.length - Nimiq.SignatureProof.SINGLE_SIG_SIZE;
data.write(proof.serialize());

// Reconstruct transaction (as transaction.data is readonly)
const tx = request.transaction;
const value = tx.value;
const flags = tx.flags;
request.transaction = new Nimiq.ExtendedTransaction(
tx.sender, tx.senderType,
tx.recipient, tx.recipientType,
/* value */ 1, tx.fee,
tx.validityStartHeight, /* flags */ 0,
data, // <= data replaced here
tx.proof, tx.networkId,
);

// The Nimiq 1.0 Transaction constructor does not allow 0 value or the signalling flag,
// so we are setting value and flags here.
// @ts-ignore Private property access
request.transaction._value = value;
// @ts-ignore Private property access
request.transaction._flags = flags;
const powPrivateKey = key.derivePrivateKey(request.keyPath);

const privateKey = Albatross.PrivateKey.unserialize(powPrivateKey.serialize());
const keyPair = Albatross.KeyPair.derive(privateKey);

/** @type {Albatross.Transaction} */
let tx;

switch (request.type) {
case SignStakingApi.IncomingStakingType.CREATE_STAKER:
tx = Albatross.TransactionBuilder.newCreateStaker(
keyPair.toAddress(),
Albatross.Address.fromString(/** @type {Nimiq.Address} */ (request.delegation).toHex()),
BigInt(request.transaction.value),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
break;
case SignStakingApi.IncomingStakingType.STAKE:
tx = Albatross.TransactionBuilder.newStake(
keyPair.toAddress(),
keyPair.toAddress(),
BigInt(request.transaction.value),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
break;
case SignStakingApi.IncomingStakingType.UPDATE_STAKER:
tx = Albatross.TransactionBuilder.newUpdateStaker(
keyPair.toAddress(),
Albatross.Address.fromString(/** @type {Nimiq.Address} */ (request.delegation).toHex()),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
break;
case SignStakingApi.IncomingStakingType.UNSTAKE:
tx = Albatross.TransactionBuilder.newUnstake(
keyPair.toAddress(),
BigInt(request.transaction.value),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
break;
default:
throw new Errors.KeyguardError('Unreachable');
}

// Regular tx signature of the sender
const signature = key.sign(request.keyPath, request.transaction.serializeContent());
tx = tx.sign(keyPair);

/** @type {KeyguardRequest.SignStakingResult} */
const result = {
publicKey: publicKey.serialize(),
signature: signature.serialize(),
publicKey: keyPair.publicKey.serialize(),
signature: tx.proof.subarray(tx.proof.length - 64),
data: request.transaction.data,
serializedTx: tx.serialize(),
};
resolve(result);
}
Expand Down
1 change: 1 addition & 0 deletions src/request/sign-staking/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<title>Nimiq Keyguard</title>

<script defer manual src="../../../node_modules/@nimiq/core-web/web-offline.js"></script>
<script defer manual type="module" src="../../lib/AlbatrossWasm.mjs"></script>
<script defer manual src="../../lib/Constants.js"></script>
<script defer manual src="../../config/config.local.js"></script>

Expand Down
4 changes: 3 additions & 1 deletion src/request/sign-staking/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* global SignStakingApi */
/* global runKeyguard */

runKeyguard(SignStakingApi);
runKeyguard(SignStakingApi, {
loadAlbatross: true,
});
38 changes: 34 additions & 4 deletions src/request/sign-transaction/SignTransaction.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global Nimiq */
/* global Albatross */
/* global Key */
/* global KeyStore */
/* global SignTransactionApi */
Expand Down Expand Up @@ -193,13 +194,42 @@ class SignTransaction {
return;
}

const publicKey = key.derivePublicKey(request.keyPath);
const signature = key.sign(request.keyPath, request.transaction.serializeContent());
const powPrivateKey = key.derivePrivateKey(request.keyPath);

const privateKey = Albatross.PrivateKey.unserialize(powPrivateKey.serialize());
const keyPair = Albatross.KeyPair.derive(privateKey);

/** @type {Albatross.Transaction} */
let tx;

if (!request.transaction.data.length) {
tx = Albatross.TransactionBuilder.newBasic(
keyPair.toAddress(),
Albatross.Address.fromString(request.transaction.recipient.toHex()),
BigInt(request.transaction.value),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
} else {
tx = Albatross.TransactionBuilder.newBasicWithData(
keyPair.toAddress(),
Albatross.Address.fromString(request.transaction.recipient.toHex()),
request.transaction.data,
BigInt(request.transaction.value),
BigInt(request.transaction.fee),
request.transaction.validityStartHeight,
request.transaction.networkId,
);
}

tx = tx.sign(keyPair);

/** @type {KeyguardRequest.SignTransactionResult} */
const result = {
publicKey: publicKey.serialize(),
signature: signature.serialize(),
publicKey: keyPair.publicKey.serialize(),
signature: tx.proof.subarray(tx.proof.length - 64),
serializedTx: tx.serialize(),
};
resolve(result);
}
Expand Down
1 change: 1 addition & 0 deletions src/request/sign-transaction/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<title>Nimiq Keyguard</title>

<script defer manual src="../../../node_modules/@nimiq/core-web/web-offline.js"></script>
<script defer manual type="module" src="../../lib/AlbatrossWasm.mjs"></script>
<script defer manual src="../../lib/Constants.js"></script>
<script defer manual src="../../config/config.local.js"></script>

Expand Down
4 changes: 3 additions & 1 deletion src/request/sign-transaction/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* global SignTransactionApi */
/* global runKeyguard */

runKeyguard(SignTransactionApi);
runKeyguard(SignTransactionApi, {
loadAlbatross: true,
});
16 changes: 16 additions & 0 deletions tools/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ rm -rf dist
output "👷 Creating folder structure"
mkdir -p dist/request
mkdir -p dist/assets/nimiq
mkdir -p dist/assets/albatross
mkdir -p dist/lib

# bundle names
Expand Down Expand Up @@ -283,6 +284,10 @@ for DIR in src/request/*/ ; do
print space[1] "<script defer src=\"/assets/nimiq/web.js\" integrity=\"sha256-'${CORE_WEB_LIB_HASH}'\"></script>"
next
}
/<script.*type="module"/ {
print
next
}
/<script/ {
# Replace first script tag with bundles, delete all others
if (!skip_script) {
Expand Down Expand Up @@ -360,6 +365,10 @@ cp -v node_modules/@nimiq/style/nimiq-style.icons.svg dist/assets/
# copy service worker (which has to be in root to work)
cp -v src/ServiceWorker.js dist

# copy Albatross loader and replace import path
cp -v src/lib/AlbatrossWasm.mjs dist/lib/
sed -i 's/\.\.\/\.\.\/node_modules\/@nimiq\/albatross-wasm/\.\.\/assets\/albatross/' dist/lib/AlbatrossWasm.mjs

# copy Nimiq files
output "‼️ Copying Nimiq files"
cp -v node_modules/@nimiq/core-web/web.js \
Expand All @@ -373,4 +382,11 @@ cp -v node_modules/@nimiq/core-web/web.js \
node_modules/@nimiq/core-web/worker.js.map \
dist/assets/nimiq

# copy Albatross files
output "‼️ Copying Albatross files"
cp -vr node_modules/@nimiq/albatross-wasm/client-proxy.js \
node_modules/@nimiq/albatross-wasm/transfer-handlers.js \
node_modules/@nimiq/albatross-wasm/web \
dist/assets/albatross

output "✔️ Finished building into ./dist"
1 change: 1 addition & 0 deletions tools/dependencyValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class2Path.set('Nimiq', 'node_modules/@nimiq/core-web/web-offline.js');
class2Path.set('Rpc', 'node_modules/@nimiq/rpc/dist/rpc.umd.js');
class2Path.set('BarcodeDetector', 'src/lib/QrScanner.js');
class2Path.set('ethers', 'node_modules/ethers/dist/ethers.umd.js');
class2Path.set('Albatross', 'src/lib/AlbatrossWasm.mjs');
class2Path.delete('index');

const requests = funcs.listDirectories('src/request');
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/* Basic Options */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["DOM"], /* Specify library files to be included in the compilation. */
"lib": ["DOM", "ESNext.BigInt"], /* Specify library files to be included in the compilation. */
"skipLibCheck": true, /* Skip checking .d.ts files. We need this currently, because types in */
/* lib.webworker.d.ts referenced in ServiceWorker.js clash with types */
/* in lib.dom.d.ts, and also because our typescript version is too old */
Expand Down Expand Up @@ -63,6 +63,7 @@
"include": [
"types/*",
"src/**/*.js",
"src/**/*.mjs",
"tests/**/*.js"
],
"exclude": [
Expand Down
8 changes: 8 additions & 0 deletions types/Albatross.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as _Albatross from '../node_modules/@nimiq/albatross-wasm/types/index';

export as namespace Albatross;
export = _Albatross;

declare global {
const Albatross: typeof _Albatross;
}
Loading

0 comments on commit 0f3039d

Please sign in to comment.