diff --git a/src/index.ts b/src/index.ts
index 33dd07c..818323f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -37,6 +37,52 @@ const checkHTML = (s: string) => {
if (s.indexOf('<') > -1 || s.indexOf('>') > -1) throw new Error('html injection');
}
+// URL STATE
+
+const clearUrlState = () => {
+ if (window.history.state !== '') {
+ window.history.pushState('', 'TON Multisig', '#');
+ }
+}
+
+const pushUrlState = (a: string, b?: bigint) => {
+ let url = a;
+ if (b !== undefined) {
+ url += '/' + b;
+ }
+ if (window.history.state !== url) {
+ window.history.pushState(url, 'TON Multisig - ' + url, '#' + url);
+ }
+}
+
+const replaceUrlState = (a: string, b?: bigint) => {
+ let url = a;
+ if (b !== undefined) {
+ url += '/' + b;
+ }
+ if (window.history.state !== url) {
+ window.history.replaceState(url, 'TON Multisig - ' + url, '#' + url);
+ }
+}
+
+// TESTNET, LANGUAGE
+
+const browserLang: string = navigator.language;
+const lang = (browserLang === 'ru-RU') || (browserLang === 'ru') || (browserLang === 'be-BY') || (browserLang === 'be') || (browserLang === 'kk-KZ') || (browserLang === 'kk') ? 'ru' : 'en';
+
+const IS_TESTNET = window.location.href.indexOf('testnet=true') > -1;
+
+if (IS_TESTNET) {
+ $('.testnet-badge').style.display = 'block';
+ document.body.classList.add('testnet-padding');
+}
+
+export const formatContractAddress = (address: Address) => {
+ return address.toString({bounceable: true, testOnly: IS_TESTNET});
+}
+
+// SCREEN
+
type ScreenType =
'startScreen'
| 'importScreen'
@@ -56,11 +102,14 @@ const showScreen = (name: ScreenType) => {
toggle($('#' + screen), screen === name);
}
+ if (currentScreen === 'startScreen') {
+ clearUrlState();
+ }
+
if (currentScreen === 'importScreen') {
($('#import_input') as HTMLInputElement).value = '';
}
-
if (currentScreen === 'newOrderScreen') {
if (newOrderTypeSelect) {
newOrderClear();
@@ -68,16 +117,6 @@ const showScreen = (name: ScreenType) => {
}
}
-const browserLang: string = navigator.language;
-const lang = (browserLang === 'ru-RU') || (browserLang === 'ru') || (browserLang === 'be-BY') || (browserLang === 'be') || (browserLang === 'kk-KZ') || (browserLang === 'kk') ? 'ru' : 'en';
-
-const IS_TESTNET = window.location.href.indexOf('testnet=true') > -1;
-
-if (IS_TESTNET) {
- $('.testnet-badge').style.display = 'block';
- document.body.classList.add('testnet-padding');
-}
-
// TONCONNECT
const tonConnectUI = new TonConnectUI({
@@ -134,10 +173,12 @@ $('#import_backButton').addEventListener('click', () => {
const MULTISIG_CODE = Cell.fromBase64("te6cckECEgEABJUAART/APSkE/S88sgLAQIBYgIDAsrQM9DTAwFxsJJfA+D6QDAi10nAAJJfA+AC0x8BIMAAkl8E4AHTPwHtRNDT/wEB0wcBAdTTBwEB9ATSAAEB0SiCEPcYUQ+64w8FREPIUAYBy/9QBAHLBxLMAQHLB/QAAQHKAMntVAQFAgEgDA0BnjgG0/8BKLOOEiCE/7qSMCSWUwW68uPw4gWkBd4B0gABAdMHAQHTLwEB1NEjkSaRKuJSMHj0Dm+h8uPvHscF8uPvIPgjvvLgbyD4I6FUbXAGApo2OCaCEHUJf126jroGghCjLFm/uo6p+CgYxwXy4GUD1NEQNBA2RlD4AH+OjSF49HxvpSCRMuMNAbPmWxA1UDSSNDbiUFQT4w1AFVAzBAoJAdT4BwODDPlBMAODCPlBMPgHUAahgSf4AaBw+DaBEgZw+DaggSvscPg2oIEdmHD4NqAipgYioIEFOSagJ6Bw+DgjpIECmCegcPg4oAOmBliggQbgUAWgUAWgQwNw+DdZoAGgHL7y4GT4KFADBwK4AXACyFjPFgEBy//JiCLIywH0APQAywDJcCH5AHTIywISygfL/8nQyIIQnHP7olgKAssfyz8mAcsHUlDMUAsByy8bzCoBygAKlRkBywcIkTDiECRwQImAGIBQ2zwRCACSjkXIWAHLBVAFzxZQA/oCVHEjI+1E7UXtR59byFADzxfJE3dQA8trzMztZ+1l7WR0f+0RmHYBy2vMAc8X7UHt8QHy/8kB+wDbBgLiNgTT/wEB0y8BAdMHAQHT/wEB1NH4KFAFAXACyFjPFgEBy//JiCLIywH0APQAywDJcAH5AHTIywISygfL/8nQG8cF8uBlJvkAGrpRk74ZsPLgZgf4I77y4G9EFFBW+AB/jo0hePR8b6UgkTLjDQGz5lsRCgH6AtdM0NMfASCCEPE4Hlu6jmqCEB0M+9O6jl5sRNMHAQHUIX9wjhdREnj0fG+lMiGZUwK68uBnAqQC3gGzEuZsISDCAPLgbiPCAPLgbVMwu/LgbQH0BCF/cI4XURJ49HxvpTIhmVMCuvLgZwKkAt4BsxLmbCEw0VUjkTDi4w0LABAw0wfUAvsA0QFDv3T/aiaGn/gIDpg4CA6mmDgID6AmkAAIDoiBqvgoD8EdDA4CAWYPEADC+AcDgwz5QTADgwj5QTD4B1AGoYEn+AGgcPg2gRIGcPg2oIEr7HD4NqCBHZhw+DagIqYGIqCBBTkmoCegcPg4I6SBApgnoHD4OKADpgZYoIEG4FAFoFAFoEMDcPg3WaABoADxsMr7UTQ0/8BAdMHAQHU0wcBAfQE0gABAdEjf3COF1ESePR8b6UyIZlTArry4GcCpALeAbMS5mwhUjC68uBsIX9wjhdREnj0fG+lMiGZUwK68uBnAqQC3gGzEuZsITAiwgDy4G4kwgDy4G1SQ7vy4G0BkjN/kQPiA4AFZsMn+CgBAXACyFjPFgEBy//JiCLIywH0APQAywDJcAH5AHTIywISygfL/8nQgEQhCAmMFqAYchWwszwXcsN9YFccUdYcFZ8q18EnjQLz1klHzYNH/nQ==");
const MULTISIG_ORDER_CODE = Cell.fromBase64('te6cckEBAQEAIwAIQgJjBagGHIVsLM8F3LDfWBXHFHWHBWfKtfBJ40C89ZJR80AoJo0=');
-let currentMultisigInfo: MultisigInfo | null = null;
+let currentMultisigAddress: string | undefined = undefined;
+let currentMultisigInfo: MultisigInfo | undefined = undefined;
-const setMultisigAddress = async (newMultisigAddress: string) => {
+const setMultisigAddress = async (newMultisigAddress: string, queuedOrderId?: bigint) => {
showScreen('loadingScreen');
+ currentMultisigAddress = newMultisigAddress;
const multisigAddress = Address.parseFriendly(newMultisigAddress);
multisigAddress.isBounceable = true;
@@ -145,13 +186,16 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
$('#mulisig_address').innerHTML = makeAddressLink(multisigAddress);
- localStorage.setItem('multisigAddress', newMultisigAddress);
+ // localStorage.setItem('multisigAddress', newMultisigAddress);
+ pushUrlState(newMultisigAddress, queuedOrderId);
toggle($('#multisig_content'), false);
toggle($('#multisig_error'), false);
try {
- currentMultisigInfo = await checkMultisig(Address.parseFriendly(newMultisigAddress), MULTISIG_CODE, IS_TESTNET, true);
+ // Load
+
+ const multisigInfo = await checkMultisig(Address.parseFriendly(newMultisigAddress), MULTISIG_CODE, IS_TESTNET, true);
const {
tonBalance,
@@ -161,12 +205,7 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
allowArbitraryOrderSeqno,
nextOderSeqno,
lastOrders
- } = currentMultisigInfo;
-
-
- $('#multisig_tonBalance').innerText = fromNano(tonBalance) + ' TON';
-
- $('#multisig_threshold').innerText = threshold + '/' + signers.length;
+ } = multisigInfo;
let signersHTML = '';
for (let i = 0; i < signers.length; i++) {
@@ -174,15 +213,27 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
const addressString = await formatAddressAndUrl(signer, IS_TESTNET)
signersHTML += (`
#${i} - ${addressString}
`);
}
+
+ let proposersHTML = '';
+ for (let i = 0; i < proposers.length; i++) {
+ const proposer = proposers[i];
+ const addressString = await formatAddressAndUrl(proposer, IS_TESTNET)
+ proposersHTML += (`#${i} - ${addressString}
`);
+ }
+
+ // Render
+
+ if (currentMultisigAddress !== newMultisigAddress) return;
+
+ currentMultisigInfo = multisigInfo;
+
+ $('#multisig_tonBalance').innerText = fromNano(tonBalance) + ' TON';
+
+ $('#multisig_threshold').innerText = threshold + '/' + signers.length;
+
$('#multisig_signersList').innerHTML = signersHTML;
if (proposers.length > 0) {
- let proposersHTML = '';
- for (let i = 0; i < proposers.length; i++) {
- const proposer = proposers[i];
- const addressString = await formatAddressAndUrl(proposer, IS_TESTNET)
- proposersHTML += (`#${i} - ${addressString}
`);
- }
$('#multisig_proposersList').innerHTML = proposersHTML;
} else {
$('#multisig_proposersList').innerHTML = 'No proposers';
@@ -194,7 +245,7 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
for (const lastOrder of lastOrders) {
if (!lastOrder.errorMessage) {
- lastOrdersHTML += `${lastOrder.type === 'new' ? 'New order' : 'Executed order'} #${lastOrder.order.id}
`
+ lastOrdersHTML += `${lastOrder.type === 'new' ? 'New order' : 'Executed order'} #${lastOrder.order.id}
`
}
}
@@ -202,8 +253,10 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
$$('.multisig_lastOrder').forEach(div => {
div.addEventListener('click', (e) => {
- const orderAddressString = (e.currentTarget as HTMLElement).attributes.getNamedItem('order-address').value;
- setOrderAddress(orderAddressString);
+ const attributes = (e.currentTarget as HTMLElement).attributes;
+ const orderAddressString = attributes.getNamedItem('order-address').value;
+ const orderId = BigInt(attributes.getNamedItem('order-id').value);
+ setOrderId(orderId, orderAddressString);
})
})
@@ -212,8 +265,10 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
} catch (e) {
console.error(e);
- showScreen('multisigScreen');
+ // Render error
+ if (currentMultisigAddress !== newMultisigAddress) return;
+ showScreen('multisigScreen');
toggle($('#multisig_error'), true);
$('#multisig_error').innerText = e.message;
}
@@ -222,6 +277,8 @@ const setMultisigAddress = async (newMultisigAddress: string) => {
$('#multisig_logoutButton').addEventListener('click', () => {
localStorage.removeItem('multisigAddress');
+ currentMultisigInfo = undefined;
+ currentMultisigAddress = undefined;
showScreen('startScreen');
});
@@ -231,10 +288,19 @@ $('#multisig_createNewOrderButton').addEventListener('click', () => {
// ORDER SCREEN
-let currentOrderInfo: MultisigOrderInfo | null = null;
+let currentOrderId: bigint | undefined = undefined;
+let currentOrderInfo: MultisigOrderInfo | undefined = undefined;
-const setOrderAddress = async (newOrderAddress: string) => {
+const setOrderId = async (newOrderId: bigint, newOrderAddress?: string) => {
+ currentOrderId = newOrderId;
showScreen('loadingScreen');
+ pushUrlState(currentMultisigAddress, newOrderId);
+
+ if (!currentMultisigInfo) throw new Error('setOrderId: no multisig info');
+
+ if (newOrderAddress === undefined) {
+ newOrderAddress = formatContractAddress(await currentMultisigInfo.multisigContract.getOrderAddress(currentMultisigInfo.provider, newOrderId));
+ }
const orderAddress = Address.parseFriendly(newOrderAddress);
orderAddress.isBounceable = true;
@@ -246,7 +312,9 @@ const setOrderAddress = async (newOrderAddress: string) => {
toggle($('#order_error'), false);
try {
- currentOrderInfo = await checkMultisigOrder(orderAddress, MULTISIG_ORDER_CODE, currentMultisigInfo, IS_TESTNET);
+ // Load
+
+ const orderInfo = await checkMultisigOrder(orderAddress, MULTISIG_ORDER_CODE, currentMultisigInfo, IS_TESTNET);
const {
tonBalance,
@@ -258,16 +326,10 @@ const setOrderAddress = async (newOrderAddress: string) => {
threshold,
signers,
expiresAt
- } = currentOrderInfo;
+ } = orderInfo;
const isExpired = (new Date()).getTime() > expiresAt.getTime();
- $('#order_id').innerText = '#' + orderId;
- $('#order_tonBalance').innerText = fromNano(tonBalance) + ' TON';
- $('#order_executed').innerText = isExecuted ? 'Yes' : 'Not yet';
- $('#order_approvals').innerText = approvalsNum + '/' + threshold;
- $('#order_expiresAt').innerText = (isExpired ? '❌ EXPIRED - ' : '') + expiresAt.toString();
-
let signersHTML = '';
for (let i = 0; i < signers.length; i++) {
const signer = signers[i];
@@ -276,6 +338,19 @@ const setOrderAddress = async (newOrderAddress: string) => {
const isSigned = approvalsMask & mask;
signersHTML += (`#${i} - ${addressString} - ${isSigned ? '✅' : '❌'}
`);
}
+
+ // Render
+
+ if (currentOrderId !== newOrderId) return;
+
+ currentOrderInfo = orderInfo;
+
+ $('#order_id').innerText = '#' + orderId;
+ $('#order_tonBalance').innerText = fromNano(tonBalance) + ' TON';
+ $('#order_executed').innerText = isExecuted ? 'Yes' : 'Not yet';
+ $('#order_approvals').innerText = approvalsNum + '/' + threshold;
+ $('#order_expiresAt').innerText = (isExpired ? '❌ EXPIRED - ' : '') + expiresAt.toString();
+
$('#order_signersList').innerHTML = signersHTML;
let actionsHTML = '';
@@ -300,18 +375,22 @@ const setOrderAddress = async (newOrderAddress: string) => {
} catch (e) {
console.error(e);
- showScreen('orderScreen');
+ // Render error
+ if (currentOrderId !== newOrderId) return;
+ showScreen('orderScreen');
toggle($('#order_error'), true);
$('#order_error').innerText = e.message;
}
}
$('#order_backButton').addEventListener('click', () => {
+ pushUrlState(currentMultisigAddress);
+ currentOrderInfo = undefined;
+ currentOrderId = undefined;
showScreen('multisigScreen');
});
-
$('#order_approveButton').addEventListener('click', async () => {
if (!myAddress) {
alert('Please connect wallet');
@@ -344,7 +423,7 @@ $('#order_approveButton').addEventListener('click', async () => {
try {
const result = await tonConnectUI.sendTransaction(transaction);
- await setOrderAddress(orderAddressString); // reload order page
+ // todo: reload order page
} catch (e) {
console.error(e);
}
@@ -914,7 +993,7 @@ $('#newOrder_createButton').addEventListener('click', async () => {
orderId
})
- const multisigAddressString = addressToString(currentMultisigInfo.address);
+ const multisigAddressString = currentMultisigAddress;
const amount = AMOUNT_TO_SEND.toString();
transactionToSent = {
@@ -1191,7 +1270,7 @@ $('#newMultisig2_createButton').addEventListener('click', async () => {
const message = Multisig.newOrderMessage(actions, expireAt, isSigner, isSigner ? mySignerIndex : myProposerIndex, newMultisigInfo.orderId!, 0n)
const messageBase64 = message.toBoc().toString('base64');
- const multisigAddressString = addressToString(currentMultisigInfo.address);
+ const multisigAddressString = currentMultisigAddress;
const amount = DEFAULT_AMOUNT.toString();
const transaction = {
@@ -1226,10 +1305,93 @@ $('#multisig_updateButton').addEventListener('click', () => {
// INIT
-let multisigAddress: string = localStorage.getItem('multisigAddress');
+const tryLoadMultisigFromLocalStorage = () => {
+ const multisigAddress: string = localStorage.getItem('multisigAddress');
-if (!multisigAddress) {
- showScreen('startScreen');
-} else {
- setMultisigAddress(multisigAddress);
+ if (!multisigAddress) {
+ showScreen('startScreen');
+ } else {
+ setMultisigAddress(multisigAddress);
+ }
+}
+
+const parseAddressFromUrl = (url: string): undefined | AddressInfo => {
+ if (!Address.isFriendly(url)) {
+ return undefined;
+ }
+ return Address.parseFriendly(url);
+}
+
+const parseBigIntFromUrl = (url: string): undefined | bigint => {
+ try {
+ const orderId = BigInt(url);
+ if (orderId < 0) return undefined;
+ return orderId;
+ } catch (e) {
+ return undefined;
+ }
+}
+
+interface ParsedUrl {
+ multisigAddress?: AddressInfo;
+ orderId?: bigint;
+}
+
+const parseUrl = (url: string): ParsedUrl => {
+ if (url.indexOf('/') > -1) {
+ const arr = url.split('/');
+ if (arr.length !== 2) {
+ return {};
+ }
+ const multisigAddress = parseAddressFromUrl(arr[0]);
+ if (multisigAddress === undefined) {
+ return {};
+ }
+
+ const orderId = parseBigIntFromUrl(arr[1]);
+ if (orderId === undefined) {
+ return {};
+ }
+
+ return {
+ multisigAddress: multisigAddress,
+ orderId: orderId
+ };
+ } else {
+ return {
+ multisigAddress: parseAddressFromUrl(url)
+ };
+ }
}
+
+const processUrl = async () => {
+ // clear();
+ currentMultisigAddress = undefined;
+ currentMultisigInfo = undefined;
+ currentOrderId = undefined;
+ currentOrderInfo = undefined;
+
+ const urlPostfix = window.location.hash.substring(1);
+
+ if (urlPostfix) {
+ const {multisigAddress, orderId} = parseUrl(urlPostfix);
+
+ console.log(multisigAddress, orderId);
+
+ if (multisigAddress === undefined) {
+ alert('Invalid URL');
+ showScreen('startScreen');
+ } else {
+ await setMultisigAddress(formatContractAddress(multisigAddress.address), orderId);
+ if (orderId !== undefined) {
+ await setOrderId(orderId, undefined);
+ }
+ }
+ } else {
+ tryLoadMultisigFromLocalStorage();
+ }
+}
+
+processUrl();
+
+window.onpopstate = () => processUrl();
\ No newline at end of file