Repository navigation
Expand file tree
/
Copy pathmessages.js
More file actions
100 lines (93 loc) · 5.16 KB
/
Copy pathmessages.js
File metadata and controls
100 lines (93 loc) · 5.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// messages.js — un message entre blocks, porte PAR la transaction elle-meme.
// ================================================================================================
// ⛔⛔ POURQUOI DANS LE CALLDATA, ET PAS AILLEURS. Un message garde dans le navigateur s efface ; un
// message garde sur un serveur demande un serveur, donc une confiance, donc une panne possible.
// Le calldata d une transaction, lui, est PUBLIC et DEFINITIF : n importe qui peut le relire des
// annees plus tard, sans nous, sans compte, sans API.
//
// ⛔ COMMENT : on envoie un `transfer(address,uint256)` NORMAL, et on colle le texte APRES les 68
// octets de l appel. Un decodeur ABI standard lit ses deux arguments et IGNORE la fin — le
// transfert s execute exactement comme sans message.
// ⚠️⚠️ CE QUI N EST PAS ENCORE PROUVE : qu un jeton donne accepte bien ces octets en trop EN VRAI.
// L ABI dit que oui ; je n ai signe aucune transaction pour le verifier, et je ne signerai pas.
// L ecran doit donc le presenter comme « pas encore eprouve sur la chaine », pas comme un fait.
//
// ⛔ ON NE PROMET PAS LA CONFIDENTIALITE. Un message on-chain est LISIBLE PAR TOUS, pour toujours.
// C est ecrit dans `AVERTISSEMENT`, et l ecran l affiche a cote du champ de saisie.
import { selecteur } from './pool.js';
const enc = new TextEncoder();
const dec = new TextDecoder('utf-8', { fatal: false });
const ADRESSE = /^0x[0-9a-fA-F]{40}$/;
/** ⛔ Le calldata d un transfert fait EXACTEMENT 4 + 32 + 32 = 68 octets. Tout ce qui suit est memo. */
export const OCTETS_TRANSFER = 68;
/** ⛔ Borne dure : 256 octets. Un message plus long coute du gas pour rien et ne se lit plus. */
export const MEMO_MAX_OCTETS = 256;
export const AVERTISSEMENT = 'A message written here is public and permanent: anyone can read it on '
+ 'chain, forever. It is not private, and it cannot be deleted.';
/** ⛔ Les caracteres de controle sont REFUSES : ils ne s affichent pas et servent a masquer du texte. */
const CONTROLE = /[ --]/;
/**
* Valide un message avant encodage.
* ⛔ REFUSE, NE TRONQUE PAS. Tronquer enverrait un texte que personne n a ecrit.
*/
export function validerMemo(texte) {
const t = String(texte ?? '');
if (t.trim() === '') return { etat: 'VIDE' };
if (CONTROLE.test(t)) {
return { etat: 'REFUSE', pourquoi: 'control characters are refused — they do not display and can hide text' };
}
const n = enc.encode(t).length;
if (n > MEMO_MAX_OCTETS) {
return { etat: 'REFUSE', octets: n,
pourquoi: 'the message is ' + n + ' bytes and the limit is ' + MEMO_MAX_OCTETS
+ ' (accents and emoji count for several). Nothing was cut: shorten it.' };
}
return { etat: 'OK', texte: t, octets: n };
}
/** Les octets d un texte, en hexadecimal — sans prefixe. */
export function hexDeTexte(texte) {
return [...enc.encode(String(texte))].map((b) => b.toString(16).padStart(2, '0')).join('');
}
/**
* Le calldata d un transfert AVEC message colle a la fin.
* ⛔ SANS MESSAGE, LE CALLDATA EST STRICTEMENT CELUI D UN TRANSFERT NORMAL : pas d octet en plus,
* donc aucun risque ajoute quand on n ecrit rien.
*/
export function encodeTransferAvecMemo(destinataire, montant, texte = '') {
if (!ADRESSE.test(String(destinataire || ''))) throw new Error('destination is not an address');
if (typeof montant !== 'bigint' || montant < 0n) throw new Error('amount must be a positive bigint');
const base = '0x' + selecteur('transfer(address,uint256)')
+ destinataire.slice(2).toLowerCase().padStart(64, '0')
+ montant.toString(16).padStart(64, '0');
const v = validerMemo(texte);
if (v.etat === 'VIDE') return base;
if (v.etat !== 'OK') throw new Error(v.pourquoi);
return base + hexDeTexte(v.texte);
}
/**
* Relit le message d un calldata de transfert.
* ⛔ TROIS ETATS : `AUCUN` (transfert nu), `LU`, `ILLISIBLE` (octets qui ne forment pas du texte).
* Les confondre ferait passer un transfert normal pour un message vide, et un message casse pour
* une absence de message.
*/
export function lireMemo(input) {
const s = String(input || '');
if (!/^0x[0-9a-fA-F]*$/.test(s)) return { etat: 'ILLISIBLE', pourquoi: 'not hex data' };
const corps = s.slice(2);
const attendu = selecteur('transfer(address,uint256)');
if (corps.slice(0, 8).toLowerCase() !== attendu.toLowerCase()) {
return { etat: 'AUCUN', pourquoi: 'not a transfer call' };
}
if (corps.length <= OCTETS_TRANSFER * 2) return { etat: 'AUCUN', pourquoi: null };
const extra = corps.slice(OCTETS_TRANSFER * 2);
if (extra.length % 2 !== 0) return { etat: 'ILLISIBLE', pourquoi: 'trailing data is not whole bytes' };
const octets = new Uint8Array(extra.length / 2);
for (let i = 0; i < octets.length; i++) octets[i] = parseInt(extra.slice(i * 2, i * 2 + 2), 16);
const texte = dec.decode(octets);
/* ⛔ UN REMPLACEMENT UNICODE VEUT DIRE QUE CE N ETAIT PAS DU TEXTE. On le dit au lieu d afficher
* des losanges noirs comme si quelqu un les avait ecrits. */
if (texte.includes('�') || CONTROLE.test(texte)) {
return { etat: 'ILLISIBLE', octets: octets.length, pourquoi: 'the extra bytes are not readable text' };
}
return { etat: 'LU', texte, octets: octets.length };
}