@memetic-block/ao-encrypted-messages
Version:
Send encrypted messages on AO. Like an encrypted voicemail.
246 lines (245 loc) • 12.4 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EncryptedMessages = void 0;
const aoconnect_1 = require("@permaweb/aoconnect");
const tweetnacl_1 = __importDefault(require("tweetnacl"));
const tweetnacl_util_1 = __importDefault(require("tweetnacl-util"));
const arweave_1 = __importDefault(require("arweave"));
const errors_1 = require("../errors");
class EncryptedMessages {
static spawn(wallet, opts) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const arweave = arweave_1.default.init({
host: ((_a = opts === null || opts === void 0 ? void 0 : opts.arweave) === null || _a === void 0 ? void 0 : _a.host) || 'arweave.net',
port: ((_b = opts === null || opts === void 0 ? void 0 : opts.arweave) === null || _b === void 0 ? void 0 : _b.port) || 443,
protocol: ((_c = opts === null || opts === void 0 ? void 0 : opts.arweave) === null || _c === void 0 ? void 0 : _c.protocol) || 'https'
});
const signer = (0, aoconnect_1.createDataItemSigner)(wallet);
const appName = (opts === null || opts === void 0 ? void 0 : opts.appName) || '@memeticblock/ao-encrypted-messages';
const luaSourceTxId = (opts === null || opts === void 0 ? void 0 : opts.luaSourceTxId)
|| EncryptedMessages.PUBLISHED_LUA_TX_ID;
console.debug(`Fetching LUA Source code from tx id ${luaSourceTxId}`);
const luaSource = yield arweave.transactions.getData(luaSourceTxId, { decode: true, string: true });
console.debug(`Spawning new AO process`);
const processId = yield (0, aoconnect_1.spawn)({
module: (opts === null || opts === void 0 ? void 0 : opts.module) || EncryptedMessages.AOS_MODULE_ID,
scheduler: (opts === null || opts === void 0 ? void 0 : opts.scheduler) || EncryptedMessages.SCHEDULER_ID,
signer,
tags: [
...((opts === null || opts === void 0 ? void 0 : opts.tags) || []),
{ name: 'App-Name', value: appName }
]
});
console.debug(`Sending Eval Action of LUA Source to process ${processId}`);
yield EncryptedMessages.sendAosMessage({
processId,
data: luaSource,
signer,
tags: [
{ name: 'Action', value: 'Eval' },
{ name: 'App-Name', value: appName },
{
name: 'Source-Code-TX-ID',
value: luaSourceTxId
}
]
});
return new EncryptedMessages(processId, wallet);
});
}
static sendAosMessage(_a) {
return __awaiter(this, arguments, void 0, function* ({ processId, data, tags, signer }, retries = 3) {
let attempts = 0;
let lastError;
while (attempts < retries) {
try {
console.debug(`Sending AO Message to process ${processId}`);
const messageId = yield (0, aoconnect_1.message)({
process: processId,
tags,
data,
signer
});
console.debug(`Fetching AO Message result ${messageId} from process ${processId}`);
const result = yield (0, aoconnect_1.result)({
message: messageId,
process: processId
});
console.debug(`Got AO Message result ${messageId} from process ${processId}`);
console.dir(result, { depth: null });
return { messageId, result };
}
catch (error) {
console.error(`Error sending AO Message to process ${processId}`, error);
if (error.message.includes('500')) {
console.debug(`Retrying sending AO message to process ${processId}`, JSON.stringify({ attempts, retries, error: error.message }, undefined, 2));
// NB: Sleep between each attempt with exponential backoff
yield new Promise(resolve => setTimeout(resolve, Math.pow(2, attempts) * 2000));
attempts++;
lastError = error;
}
else
throw error;
}
}
throw lastError;
});
}
constructor(processId, wallet, arweave) {
this.processId = processId;
this.wallet = wallet;
if (!arweave) {
this.arweave = arweave_1.default.init({
host: 'arweave.net',
port: 443,
protocol: 'https'
});
}
}
setWallet(wallet) {
this.wallet = wallet;
}
getEncryptionPublicKey(tags) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.wallet) {
throw new errors_1.WalletNotSetError();
}
const { messageId, result } = yield EncryptedMessages.sendAosMessage({
processId: this.processId,
signer: (0, aoconnect_1.createDataItemSigner)(this.wallet),
tags: [
{ name: 'Action', value: 'Get-Encryption-Public-Key' },
...(tags || [])
]
});
const publicKey = result.Messages[0].Data;
this.lastSeenEncryptionPublicKey = publicKey;
return { messageId, result, publicKey };
});
}
setEncryptionPublicKey(encryptionPublicKey, tags) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.wallet) {
throw new errors_1.WalletNotSetError();
}
let publicKey = encryptionPublicKey;
let secretKey;
if (!publicKey) {
const keyPair = tweetnacl_1.default.box.keyPair();
publicKey = tweetnacl_util_1.default.encodeBase64(keyPair.publicKey);
secretKey = tweetnacl_util_1.default.encodeBase64(keyPair.secretKey);
}
const { messageId } = yield EncryptedMessages.sendAosMessage({
processId: this.processId,
signer: (0, aoconnect_1.createDataItemSigner)(this.wallet),
tags: [
{ name: 'Action', value: 'Set-Encryption-Public-Key' },
{ name: 'EncryptionPublicKey', value: publicKey },
...(tags || [])
]
});
this.lastSeenEncryptionPublicKey = publicKey;
return { publicKey, secretKey, messageId };
});
}
sendEncryptedMessage(message, opts, tags) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.wallet) {
throw new errors_1.WalletNotSetError();
}
const { publicKey, secretKey } = (opts === null || opts === void 0 ? void 0 : opts.secretKey)
? tweetnacl_1.default.box.keyPair.fromSecretKey(
// @ts-ignore
typeof (opts === null || opts === void 0 ? void 0 : opts.secretKey) === 'string'
? Buffer.from(opts === null || opts === void 0 ? void 0 : opts.secretKey)
: opts === null || opts === void 0 ? void 0 : opts.secretKey)
: tweetnacl_1.default.box.keyPair();
const nonce = typeof (opts === null || opts === void 0 ? void 0 : opts.nonce) === 'string'
? Buffer.from(opts === null || opts === void 0 ? void 0 : opts.nonce)
: tweetnacl_1.default.randomBytes(tweetnacl_1.default.box.nonceLength);
if (!this.lastSeenEncryptionPublicKey) {
yield this.getEncryptionPublicKey();
}
if (!this.lastSeenEncryptionPublicKey) {
throw new errors_1.EncryptionPublicKeyNotSetError();
}
const encryptedMessage = tweetnacl_1.default.box(tweetnacl_util_1.default.decodeUTF8(message),
// @ts-ignore
nonce, tweetnacl_util_1.default.decodeBase64(this.lastSeenEncryptionPublicKey), secretKey);
// @ts-ignore
const nonceB64 = tweetnacl_util_1.default.encodeBase64(nonce);
const encryptedMessageJson = JSON.stringify({
message: tweetnacl_util_1.default.encodeBase64(encryptedMessage),
nonce: nonceB64,
publicKey: tweetnacl_util_1.default.encodeBase64(publicKey),
recipientPublicKey: this.lastSeenEncryptionPublicKey
});
const { messageId, result } = yield EncryptedMessages.sendAosMessage({
processId: this.processId,
signer: (0, aoconnect_1.createDataItemSigner)(this.wallet),
tags: [
{ name: 'Action', value: 'Send-Encrypted-Message' },
{ name: 'Encrypted-Message-Nonce', value: nonceB64 },
...(tags || [])
],
data: encryptedMessageJson
});
if (result.Error) {
throw new errors_1.EncryptedMessagesLuaProcessError(result.Error);
}
return { messageId, nonce: nonceB64 };
});
}
listEncryptedMessages(tags) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.wallet) {
throw new errors_1.WalletNotSetError();
}
const { messageId, result } = yield EncryptedMessages.sendAosMessage({
processId: this.processId,
signer: (0, aoconnect_1.createDataItemSigner)(this.wallet),
tags: [
{ name: 'Action', value: 'List-Encrypted-Messages' },
...(tags || [])
]
});
const messages = JSON.parse(result.Messages[0].Data);
return { messageId, messages };
});
}
getEncryptedMessage(messageId, secretKey) {
return __awaiter(this, void 0, void 0, function* () {
if (!messageId) {
throw new errors_1.MessageIdRequiredError();
}
const { data: encryptedMessage } = yield this.arweave.api.get(`/${messageId}`);
if (!encryptedMessage) {
throw new errors_1.EncryptedMessageNotFoundError(messageId);
}
if (secretKey) {
const decryptedMessage = tweetnacl_1.default.box.open(tweetnacl_util_1.default.decodeBase64(encryptedMessage.message), tweetnacl_util_1.default.decodeBase64(encryptedMessage.nonce), tweetnacl_util_1.default.decodeBase64(encryptedMessage.publicKey), tweetnacl_util_1.default.decodeBase64(secretKey));
if (decryptedMessage) {
encryptedMessage.message = Buffer.from(decryptedMessage).toString();
}
}
return encryptedMessage;
});
}
}
exports.EncryptedMessages = EncryptedMessages;
EncryptedMessages.SCHEDULER_ID = '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA';
EncryptedMessages.AOS_MODULE_ID = 'cbn0KKrBZH7hdNkNokuXLtGryrWM--PjSTBqIzw9Kkk';
EncryptedMessages.PUBLISHED_LUA_TX_ID = 'p5zkcW3sysfkGrkN9oc_DfVNQ9PkI3hsb-8CyPeZZdg';