Skip to content

Commit

Permalink
feat(keystone): support sign ao transfer via api for dapps
Browse files Browse the repository at this point in the history
  • Loading branch information
soralit committed Jul 12, 2024
1 parent 4c8028d commit 8bbcd6c
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 73 deletions.
2 changes: 1 addition & 1 deletion src/api/modules/connect/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type AuthType =
| "token"
| "sign"
| "subscription"
| "signMessage"
| "signKeystone"
| "signature"
| "signDataItem";

Expand Down
2 changes: 1 addition & 1 deletion src/api/modules/sign/chunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { nanoid } from "nanoid";
*/
export interface Chunk {
collectionID: string; // unique ID for the collection, that is the parent of this chunk
type: "tag" | "data" | "start" | "end";
type: "tag" | "data" | "bytes" | "start" | "end";
index: number; // index of the chunk, to make sure it is not in the wrong order
value?: number[] | Tag; // Uint8Array converted to number array or a tag
}
Expand Down
51 changes: 47 additions & 4 deletions src/api/modules/sign/sign_auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { onMessage, sendMessage } from "@arconnect/webext-bridge";
import { deconstructTransaction } from "./transaction_builder";
import { bytesToChunks, deconstructTransaction } from "./transaction_builder";
import type Transaction from "arweave/web/lib/transaction";
import type { AuthResult } from "shim";
import authenticate from "../connect/auth";
import { nanoid } from "nanoid";

/**
* Request a manual signature for the transaction.
Expand Down Expand Up @@ -73,15 +74,57 @@ export const signAuth = (
}
);

export const signAuthMessage = (dataToSign: Uint8Array) =>
export type AuthKeystoneType = "Message" | "DataItem";

export interface AuthKeystoneData {
type: AuthKeystoneType;
data: Uint8Array;
}

export const signAuthKeystone = (dataToSign: AuthKeystoneData) =>
new Promise<AuthResult<{ id: string; signature: string } | undefined>>(
(resolve, reject) => {
// start auth
const collectionID = nanoid();
authenticate({
type: "signMessage",
data: Buffer.from(dataToSign).toString("base64")
type: "signKeystone",
keystoneSignType: dataToSign.type,
collectionID
})
.then((res) => resolve(res))
.catch((err) => reject(err));
const dataChunks = bytesToChunks(dataToSign.data, collectionID, 0);

// send tx in chunks to sign if requested
onMessage("auth_listening", async ({ sender }) => {
if (sender.context !== "web_accessible") return;

// send data chunks
for (const chunk of dataChunks) {
try {
await sendMessage(
"auth_chunk",
chunk,
`web_accessible@${sender.tabId}`
);
} catch (e) {
// chunk fail
return reject(
`Error while sending a data chunk of collection "${collectionID}": \n${e}`
);
}
}

// end chunk
await sendMessage(
"auth_chunk",
{
collectionID,
type: "end",
index: dataChunks.length
},
`web_accessible@${sender.tabId}`
);
});
}
);
48 changes: 47 additions & 1 deletion src/api/modules/sign/transaction_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ import { nanoid } from "nanoid";
*/
export type SplitTransaction = Partial<Transaction>;

/**
* Split an Uint8Array to chunks, per chunks value max size is limited to 0.5mb
*/
export const bytesToChunks = (
data: Uint8Array,
id: string,
start_index: number
): Chunk[] => {
const dataChunks: Chunk[] = [];

for (let i = 0; i < Math.ceil(data.length / CHUNK_SIZE); i++) {
const sliceFrom = i * CHUNK_SIZE;
const chunkValue = data.slice(sliceFrom, sliceFrom + CHUNK_SIZE);

dataChunks.push({
collectionID: id,
type: "bytes",
index: i + start_index,
value: Array.from(chunkValue)
});
}
return dataChunks;
};

/**
* Reconstruct bytes from chunks
*/
export const bytesFromChunks = (chunks: Chunk[]): Uint8Array => {
chunks.sort((a, b) => a.index - b.index);

const dataSize = getDataSize(chunks);
const reconstructedData = new Uint8Array(dataSize);

let previousLength = 0;

for (const chunk of chunks) {
if (chunk.type === "bytes") {
const chunkBuffer = new Uint8Array(chunk.value as number[]);

reconstructedData.set(chunkBuffer, previousLength);
previousLength += chunkBuffer.length;
}
}
return reconstructedData;
};

/**
* Split the tags and the data of a transaction in
* chunks and remove them from the transaction object
Expand Down Expand Up @@ -81,7 +127,7 @@ export function getDataSize(chunks: Chunk[]): number {
let dataSize = 0;

for (const chunk of chunks) {
if (chunk.type === "data") {
if (chunk.type === "data" || chunk.type === "bytes") {
dataSize += chunk.value?.length || 0;
}
}
Expand Down
103 changes: 66 additions & 37 deletions src/api/modules/sign_data_item/sign_data_item.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ import type { ModuleFunction } from "~api/background";
import { ArweaveSigner, createData } from "arbundles";
import Application from "~applications/application";
import { getPrice } from "../dispatch/uploader";
import { getActiveKeyfile } from "~wallets";
import { getActiveKeyfile, getActiveWallet } from "~wallets";
import browser from "webextension-polyfill";
import { signAuth } from "../sign/sign_auth";
import {
signAuth,
signAuthKeystone,
type AuthKeystoneData
} from "../sign/sign_auth";
import Arweave from "arweave";
import authenticate from "../connect/auth";
import BigNumber from "bignumber.js";
import { createDataItem } from "~utils/data_item";
import signMessage from "../sign_message";

const background: ModuleFunction<number[]> = async (
appData,
Expand Down Expand Up @@ -69,10 +75,6 @@ const background: ModuleFunction<number[]> = async (
throw new Error("No wallets added");
});

// ensure that the currently selected
// wallet is not a local wallet
isLocalWallet(decryptedWallet);

// create app
const app = new Application(appData.appURL);

Expand All @@ -83,44 +85,71 @@ const background: ModuleFunction<number[]> = async (
const { data, ...options } = dataItem;
const binaryData = new Uint8Array(data);

// create bundlr tx as a data entry
const dataSigner = new ArweaveSigner(decryptedWallet.keyfile);
const dataEntry = createData(binaryData, dataSigner, options);
if (decryptedWallet.type == "local") {
// create bundlr tx as a data entry
const dataSigner = new ArweaveSigner(decryptedWallet.keyfile);
const dataEntry = createData(binaryData, dataSigner, options);

// check allowance
// const price = await getPrice(dataEntry, await app.getBundler());
const allowance = await app.getAllowance();
// check allowance
// const price = await getPrice(dataEntry, await app.getBundler());
const allowance = await app.getAllowance();

// allowance or sign auth
try {
if (!allowance.enabled) {
// get address
const address = await arweave.wallets.jwkToAddress(
decryptedWallet.keyfile
);
// allowance or sign auth
try {
if (!allowance.enabled) {
// get address
const address = await arweave.wallets.jwkToAddress(
decryptedWallet.keyfile
);

await signAuth(
appData.appURL,
// @ts-expect-error
dataEntry.toJSON(),
address
);
await signAuth(
appData.appURL,
// @ts-expect-error
dataEntry.toJSON(),
address
);
}
} catch (e) {
freeDecryptedWallet(decryptedWallet.keyfile);
throw new Error(e?.message || e);
}
} catch (e) {
freeDecryptedWallet(decryptedWallet.keyfile);
throw new Error(e?.message || e);
}
// sign item
await dataEntry.sign(dataSigner);

// sign item
await dataEntry.sign(dataSigner);
// update allowance spent amount (in winstons)
// await updateAllowance(appData.appURL, price);

// update allowance spent amount (in winstons)
// await updateAllowance(appData.appURL, price);

// remove keyfile
freeDecryptedWallet(decryptedWallet.keyfile);
// remove keyfile
freeDecryptedWallet(decryptedWallet.keyfile);

return Array.from<number>(dataEntry.getRaw());
return Array.from<number>(dataEntry.getRaw());
} else {
// create bundlr tx as a data entry
const activeWallet = await getActiveWallet();
if (activeWallet.type != "hardware") throw new Error("Invalid Wallet Type");
const signerConfig = {
signatureType: 1,
signatureLength: 512,
ownerLength: 512,
publicKey: Buffer.from(
Arweave.utils.b64UrlToBuffer(activeWallet.publicKey)
)
};
const dataEntry = createDataItem(binaryData, signerConfig, options);
try {
const data: AuthKeystoneData = {
type: "DataItem",
data: dataEntry.getRaw()
};
const res = await signAuthKeystone(data);
dataEntry.setSignature(
Buffer.from(Arweave.utils.b64UrlToBuffer(res.data.signature))
);
} catch (e) {
throw new Error(e?.message || e);
}
return Array.from<number>(dataEntry.getRaw());
}
};

export default background;
8 changes: 6 additions & 2 deletions src/api/modules/sign_message/sign_message.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
isNumberArray,
isSignMessageOptions
} from "~utils/assertions";
import { signAuthMessage } from "../sign/sign_auth";
import { signAuthKeystone, type AuthKeystoneData } from "../sign/sign_auth";
import Arweave from "arweave";

const background: ModuleFunction<number[]> = async (
Expand Down Expand Up @@ -66,7 +66,11 @@ const background: ModuleFunction<number[]> = async (

return Array.from(new Uint8Array(signature));
} else {
const res = await signAuthMessage(dataToSign);
const data: AuthKeystoneData = {
type: "Message",
data: dataToSign
};
const res = await signAuthKeystone(data);
const sig = Arweave.utils.b64UrlToBuffer(res.data.signature);
return Array.from(sig);
}
Expand Down
Loading

0 comments on commit 8bbcd6c

Please sign in to comment.