diff --git a/.github/workflows/node-integration.yml b/.github/workflows/node-integration.yml index d254061..8fb2495 100644 --- a/.github/workflows/node-integration.yml +++ b/.github/workflows/node-integration.yml @@ -1,13 +1,13 @@ # The wallet against a real node: the wallet's real transport, reply validators and flows (the wasm -# core and engine under Node, real `fetch`) against a real `rand-node` from the fullnode v0.7.1 -# release, on a Test-profile chain the test cuts itself. See scripts/e2e-node.sh for what it covers +# core and engine under Node, real `fetch`) against a real `rand-node` from the fullnode v0.8.0 +# release (chain 21 onward: constraint set 9, which this wallet's core proves for), on a Test-profile chain the test cuts itself. See scripts/e2e-node.sh for what it covers # and web/wallet/test/node.e2e.test.mjs for the cases. # # The node binaries are the release's, not built here: that is what makes this a check of the # wallet against the node that is deployed, and it keeps the job to the wasm core's build plus about # a minute of tests with `rand-prover`'s one real bundle proof on top (a few minutes on a shared # runner). To test against another release set FULLNODE_TAG; nothing the suite asserts may depend on -# a node feature newer than v0.7.1. +# a node feature newer than v0.8.0. name: node-integration on: @@ -26,7 +26,7 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_TOOLCHAIN: 1.98.1 - FULLNODE_TAG: v0.7.1 + FULLNODE_TAG: v0.8.0 jobs: real-node: diff --git a/README.md b/README.md index f83c8c4..dfda580 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ STARK proof, keys are Poseidon2 hashes, envelopes are ML-KEM-768 + ChaCha20-Poly primitives exist only in the fullnode's Rust crates, and a wallet that re-implemented them in Swift, Java or JavaScript would have to be byte-identical to the node or every transfer is refused. So `core/` vendors the fullnode crates (`core/vendor/fullnode`, -a submodule at fullnode `main` `37c4fdd`, the v0.8.0 candidate, on chain 20: **split authorisation**, where a transaction carries a bundle proof made from the viewing key and a small authorisation proof the wallet always makes itself from the spend key; **BIND-1**, a transaction bound to its chain's genesis; program state and the `Invoke` action (RPL-2); and the chain's bridge fee) and exposes one JSON entry point, `call(method, params)`, that each +a submodule at fullnode's `v0.8.0` tag `c0878c3` (constraint set 9, the `randprotocol-msig` signer checks with the payee check a signer runs, and the verified fold in the verify cache), chain 21 onward: **split authorisation**, where a transaction carries a bundle proof made from the viewing key and a small authorisation proof the wallet always makes itself from the spend key; **BIND-1**, a transaction bound to its chain's genesis; program state and the `Invoke` action (RPL-2); and the chain's bridge fee) and exposes one JSON entry point, `call(method, params)`, that each client wraps: an XCFramework on iOS, a `.so` on Android, WebAssembly in the browser. Everything above that line — the RPC client, note store, scan and send flow, key storage and the UI — is Swift, Java and JavaScript. @@ -203,7 +203,7 @@ git checkout v0.7.3 && git submodule update --init # a release, rather than ma ``` or unpack `rand-wallet--source.tar.gz` from a release, which has the submodule in it already. -The submodule is the full node (`core/vendor/fullnode`, at its `v0.7.0` tag): the wallet's +The submodule is the full node (`core/vendor/fullnode`, at its `v0.8.0` tag): the wallet's cryptography is the node's own code. ### 3. Test the core (optional, about a minute) diff --git a/android/app/build.gradle b/android/app/build.gradle index 96f72b9..6428402 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -73,6 +73,9 @@ android { testOptions { unitTests.returnDefaultValues = true } + + // The private-multisig vectors (fullnode 2771be22), held to their SHA-256s by ui/test/msauth-fixtures.test.mjs. + sourceSets { test { resources.srcDirs += ['../../ui/test/fixtures/msauth'] } } } dependencies { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c4724a2..403c92e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -55,6 +55,12 @@ tools:replace="screenOrientation" /> + + + expectedRecipients) throws CoreException { + return object("msauth_check", msauthCheckParams(proposal, policy, expectedRecipients)); + } + + /** + * The share {@code "i:hex"}. The core re-runs every check, the payee check with {@code + * expectedRecipients} included — pass the very list the check was given. {@code seedHex} is the + * caller's; it wipes its byte[] after (the String cannot be wiped). + */ + public static String msauthSign(String proposal, String policy, String seedHex, java.util.List expectedRecipients) throws CoreException { + Object v = call("msauth_sign", msauthSignParams(proposal, policy, seedHex, expectedRecipients)); + if (v instanceof String) return (String) v; + throw new CoreException("msauth_sign returned no share"); + } + + /** Throws the core's refusal when {@code share} does not verify over this proposal. */ + public static void msauthVerifyShare(String proposal, String policy, String share) throws CoreException { + call("msauth_verify_share", msauthVerifyShareParams(proposal, policy, share)); + } + + /** A keygen file or 64 hex → {@code {seed, public_key, address, index}}; {@code policy} null leaves index null. */ + public static JSONObject msauthSignerImport(String input, String policy) throws CoreException { + return object("msauth_signer_import", msauthSignerImportParams(input, policy)); + } + + /** {@code {fingerprint, threshold, signers, address, policy_hash}}. */ + public static JSONObject msauthPolicyInfo(String policy) throws CoreException { + return object("msauth_policy_info", msauthPolicyInfoParams(policy)); + } + + static JSONObject msauthCheckParams(String proposal, String policy, java.util.List expectedRecipients) { + return p("proposal", proposal, "policy", policy, "expected_recipients", new JSONArray(expectedRecipients)); + } + + static JSONObject msauthSignParams(String proposal, String policy, String seedHex, java.util.List expectedRecipients) { + return p("proposal", proposal, "policy", policy, "seed", seedHex, "expected_recipients", new JSONArray(expectedRecipients)); + } + + static JSONObject msauthVerifyShareParams(String proposal, String policy, String share) { + return p("proposal", proposal, "policy", policy, "share", share); + } + + static JSONObject msauthSignerImportParams(String input, String policy) { + return policy == null ? p("input", input) : p("input", input, "policy", policy); + } + + static JSONObject msauthPolicyInfoParams(String policy) { + return p("policy", policy); + } + public static String formatAmount(String units) { try { return String.valueOf(call("format_amount", p("units", units))); diff --git a/android/app/src/main/java/org/randprotocol/wallet/security/KeyVault.java b/android/app/src/main/java/org/randprotocol/wallet/security/KeyVault.java index 14016b9..33bf541 100644 --- a/android/app/src/main/java/org/randprotocol/wallet/security/KeyVault.java +++ b/android/app/src/main/java/org/randprotocol/wallet/security/KeyVault.java @@ -6,15 +6,22 @@ import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.L10n; + import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.HashSet; +import java.util.Set; /** * The spend key at rest: an {@link EncryptedSharedPreferences} whose AES-GCM key lives in the * Android Keystore. Beside it, and nothing else, a paired prover's record (delegated proving: * the token AND the key and URL a job is sealed and sent to, and whether the link marked the * prover as the user's own — the one copy a send decides by) — the viewing key and the address - * are derived from the spend key on demand by the core. + * are derived from the spend key on demand by the core. And the private-multisig signer profiles + * ({@link SignerProfiles}, every name {@code msauth.…}): a policy and a seed sealed by the + * profile's own per-use Keystore key ({@link KeystoreSeedCipher}). */ public final class KeyVault { private static final String FILE = "rand_wallet_vault"; @@ -56,10 +63,48 @@ public void store(String spendKeyHex) { prefs.edit().putString(KEY_SPEND, spendKeyHex).commit(); } + /** + * Removes the wallet: first every signer profile and every profile's Keystore key, swept by + * name (also those the index no longer lists); then the spend key and the prover's record. + * When the sweep fails, nothing else is removed and IllegalStateException says so — the + * wallet is kept whole and a retry finds what is left (never a half-wipe). + */ public void erase() { + try { + SignerProfiles.eraseAll(signerStore(), KeystoreSeedCipher.keystoreAliases()); + } catch (Exception e) { + throw new IllegalStateException(L10n.t(R.string.msauth_forget_failed, + "The signer keys on this device could not all be removed, so the wallet was kept. Try again."), e); + } prefs.edit().remove(KEY_SPEND).remove(KEY_PROVER_PAIRING).remove(KEY_LEGACY_PROVER_TOKEN).commit(); } + /** The signer profiles' storage, in this same encrypted file (spec §3); a write that does not land throws. */ + public SignerProfiles.KeyValue signerStore() { + return new SignerProfiles.KeyValue() { + @Override public String get(String k) { + return prefs.getString(k, null); + } + + @Override public void put(String k, String v) { + if (!prefs.edit().putString(k, v).commit()) throw new IllegalStateException("the key vault could not be written"); + } + + @Override public void remove(String k) { + if (!prefs.edit().remove(k).commit()) throw new IllegalStateException("the key vault could not be written"); + } + + @Override public Set keys() { + return new HashSet<>(prefs.getAll().keySet()); + } + }; + } + + /** The signer profiles, removing a profile's Keystore key with it. */ + public SignerProfiles signerProfiles() { + return new SignerProfiles(signerStore(), KeystoreSeedCipher.keystoreAliases()); + } + /** * The paired prover's record — token, key, URL, fingerprint, {@code own} — or null. The token * travels only inside a job the core sealed to this record's key, which (not {@link Prefs}' diff --git a/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedCipher.java b/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedCipher.java new file mode 100644 index 0000000..367ba4d --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedCipher.java @@ -0,0 +1,160 @@ +package org.randprotocol.wallet.security; + +import android.content.Context; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import androidx.annotation.ChecksSdkIntAtLeast; +import androidx.biometric.BiometricManager; + +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.L10n; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Base64; +import java.util.Collections; +import java.util.List; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * One Keystore AES-256-GCM key per signer profile (alias {@code msauth.}), usable only right + * after the person authenticates — spec §3: every signature re-authenticates, and so does saving the + * seed. The cipher goes through BiometricPrompt's CryptoObject; nothing here prompts. + * + *

API 30+: a strong biometric or the device credential, per use (timeout 0). API 26–29: a + * per-use key with a CryptoObject exists only for biometrics (validity -1; the device credential + * cannot unlock a CryptoObject there), so on those devices a profile needs an enrolled strong + * biometric ({@link #refusal}). + */ +public final class KeystoreSeedCipher { + private KeystoreSeedCipher() {} + + private static final String STORE = "AndroidKeyStore"; + private static final String TRANSFORM = "AES/GCM/NoPadding"; + + public static String alias(String id) { + return SignerProfiles.PREFIX + id; + } + + /** Whether the device credential can authorise a per-use key on {@code sdk}. */ + @ChecksSdkIntAtLeast(parameter = 0) + public static boolean credentialPerUse(int sdk) { + return sdk >= Build.VERSION_CODES.R; + } + + /** BiometricPrompt's authenticators for a signer key's CryptoObject on {@code sdk}. */ + public static int promptAuthenticators(int sdk) { + return credentialPerUse(sdk) + ? BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL + : BiometricManager.Authenticators.BIOMETRIC_STRONG; + } + + /** + * Why this device cannot hold a signer key, or null when it can: {@code biometricReady} a strong + * biometric is enrolled and usable, {@code credentialReady} a screen lock is set. + */ + public static String refusal(int sdk, boolean biometricReady, boolean credentialReady) { + boolean ok = credentialPerUse(sdk) ? (biometricReady || credentialReady) : biometricReady; + return ok ? null : L10n.t(R.string.msauth_needs_device_auth, + "Set a screen lock (and on Android 10 or older, a fingerprint) first: a signer key is used only right after you authenticate."); + } + + /** {@link #refusal(int, boolean, boolean)} for this device. */ + public static String refusal(Context c) { + int sdk = Build.VERSION.SDK_INT; + BiometricManager m = BiometricManager.from(c); + boolean bio = m.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS; + boolean cred = credentialPerUse(sdk) && m.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS; + return refusal(sdk, bio, cred); + } + + public static void createKey(String id) throws GeneralSecurityException { + KeyGenParameterSpec.Builder b = new KeyGenParameterSpec.Builder(alias(id), KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .setUserAuthenticationRequired(true) + .setInvalidatedByBiometricEnrollment(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // credentialPerUse, inlined for lint + b.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL); + } else { + b.setUserAuthenticationValidityDurationSeconds(-1); + } + KeyGenerator g = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE); + g.init(b.build()); + g.generateKey(); + } + + private static KeyStore keystore() throws GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(STORE); + try { + ks.load(null); + } catch (IOException e) { + throw new GeneralSecurityException(e); + } + return ks; + } + + private static SecretKey key(String id) throws GeneralSecurityException { + SecretKey k = (SecretKey) keystore().getKey(alias(id), null); + if (k == null) throw new GeneralSecurityException("no Keystore key for this signer"); + return k; + } + + /** For saving the seed: authenticate it through a CryptoObject, then {@code doFinal(seed)}; the IV is {@code getIV()}. */ + public static Cipher encryptCipher(String id) throws GeneralSecurityException { + Cipher c = Cipher.getInstance(TRANSFORM); + c.init(Cipher.ENCRYPT_MODE, key(id)); + return c; + } + + /** For reading the seed: authenticate it through a CryptoObject, then {@code doFinal(ct)}. */ + public static Cipher decryptCipher(String id, byte[] iv) throws GeneralSecurityException { + Cipher c = Cipher.getInstance(TRANSFORM); + c.init(Cipher.DECRYPT_MODE, key(id), new GCMParameterSpec(128, iv)); + return c; + } + + /** Best effort, for undoing a failed add; the wallet's sweep ({@link SignerProfiles#eraseAll}) throws instead. */ + public static void deleteKey(String id) { + try { + keystore().deleteEntry(alias(id)); + } catch (Exception ignored) { + // nothing to delete, or the sweep takes it + } + } + + /** The Keystore's aliases, for {@link SignerProfiles}: listing or deleting throws on failure. */ + public static SignerProfiles.KeyAliases keystoreAliases() { + return new SignerProfiles.KeyAliases() { + @Override public List list() throws Exception { + return Collections.list(keystore().aliases()); + } + + @Override public void delete(String alias) throws Exception { + keystore().deleteEntry(alias); + } + }; + } + + public static String seal(byte[] iv, byte[] ct) { + return Base64.getEncoder().encodeToString(iv) + ":" + Base64.getEncoder().encodeToString(ct); + } + + /** {@code {iv, ct}}; IllegalArgumentException for anything but two non-empty base64 halves. */ + public static byte[][] unseal(String s) { + int i = s == null ? -1 : s.indexOf(':'); + if (i <= 0 || i == s.length() - 1) throw new IllegalArgumentException("a sealed seed is iv:ct"); + byte[] iv = Base64.getDecoder().decode(s.substring(0, i)); + byte[] ct = Base64.getDecoder().decode(s.substring(i + 1)); + if (iv.length == 0 || ct.length == 0) throw new IllegalArgumentException("a sealed seed is iv:ct"); + return new byte[][] {iv, ct}; + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedSource.java b/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedSource.java new file mode 100644 index 0000000..21cc394 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/security/KeystoreSeedSource.java @@ -0,0 +1,54 @@ +package org.randprotocol.wallet.security; + +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.L10n; +import org.randprotocol.wallet.wallet.SignFlow; + +import javax.crypto.Cipher; + +/** + * A profile's seed, read once per signature: the sealed seed from {@link SignerProfiles}, its + * profile's Keystore cipher, the system's prompt over that cipher ({@link Authenticator}: a + * BiometricPrompt with a CryptoObject), then the decrypt. The bytes returned are a fresh buffer + * the caller zeroes ({@link SignFlow#sign} does); the Keystore's and the Cipher's own copies are + * out of reach. + */ +public final class KeystoreSeedSource implements SignFlow.SeedSource { + /** The decrypt cipher for a profile ({@link KeystoreSeedCipher#decryptCipher} on a device). */ + public interface CipherFactory { + Cipher decryptCipher(String id, byte[] iv) throws Exception; + } + + /** + * Shows the prompt with {@code cipher} as its CryptoObject and blocks until it ends: the + * authenticated cipher, or a throw when the person cancels or fails. Never on the main thread. + */ + public interface Authenticator { + Cipher authenticate(Cipher cipher) throws Exception; + } + + private final SignerProfiles profiles; + private final CipherFactory ciphers; + private final Authenticator auth; + + public KeystoreSeedSource(SignerProfiles profiles, Authenticator auth) { + this(profiles, KeystoreSeedCipher::decryptCipher, auth); + } + + public KeystoreSeedSource(SignerProfiles profiles, CipherFactory ciphers, Authenticator auth) { + this.profiles = profiles; + this.ciphers = ciphers; + this.auth = auth; + } + + @Override + public byte[] seed(String id) throws Exception { + String sealed = profiles.sealedSeed(id); + if (sealed == null) { + throw new IllegalStateException(L10n.t(R.string.msauth_no_seed, "This signer profile has no key on this device: remove it and add it again.")); + } + byte[][] parts = KeystoreSeedCipher.unseal(sealed); + Cipher authed = auth.authenticate(ciphers.decryptCipher(id, parts[0])); + return authed.doFinal(parts[1]); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java b/android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java index 58a6215..d6326e4 100644 --- a/android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java +++ b/android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java @@ -8,7 +8,7 @@ public final class Prefs { private static final String FILE = "rand_wallet_prefs"; public static final String DEFAULT_RPC_URL = "https://rpc.randprotocol.org"; - public static final long DEFAULT_CHAIN_ID = 20; + public static final long DEFAULT_CHAIN_ID = 21; public static final int DEFAULT_AUTO_LOCK_MINUTES = 15; public static final String THEME_DARK = "dark"; @@ -17,8 +17,29 @@ public final class Prefs { private final SharedPreferences p; + /** The marker of the one-time chain-id migration below; set once it has run, whatever it found. */ + static final String CHAIN_MIGRATION_KEY = "migration_chain_id_20_to_21"; + public Prefs(Context c) { - p = c.getSharedPreferences(FILE, Context.MODE_PRIVATE); + this(c.getSharedPreferences(FILE, Context.MODE_PRIVATE)); + } + + Prefs(SharedPreferences prefs) { + p = prefs; + migrateChainId(p); + } + + /** + * Once, on the first launch of a build that has it: a saved chain id of 20 becomes 21 (wallet + * v0.8.0 runs chain 21 onward; a saved 20 would refuse every proposal and transfer). Any other + * saved value, or none, is left alone, and nothing else is touched (the RPC URL is not). Later + * launches see the marker and do nothing, so a user who chooses 20 again keeps it. + */ + static void migrateChainId(SharedPreferences p) { + if (p.getBoolean(CHAIN_MIGRATION_KEY, false)) return; + SharedPreferences.Editor e = p.edit(); + if (p.contains("chain_id") && p.getLong("chain_id", DEFAULT_CHAIN_ID) == 20) e.putLong("chain_id", 21); + e.putBoolean(CHAIN_MIGRATION_KEY, true).commit(); } public String rpcUrl() { diff --git a/android/app/src/main/java/org/randprotocol/wallet/security/SignerProfiles.java b/android/app/src/main/java/org/randprotocol/wallet/security/SignerProfiles.java new file mode 100644 index 0000000..5570195 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/security/SignerProfiles.java @@ -0,0 +1,214 @@ +package org.randprotocol.wallet.security; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.L10n; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Signer profiles beside the spend key (spec §3), in {@link KeyVault}'s encrypted file: {@code + * msauth.index} (the list), {@code msauth..policy} and {@code msauth..seed} (the seed sealed + * by the profile's own Keystore key, {@link KeystoreSeedCipher}, "ivB64:ctB64"). The Keystore key's + * alias is {@code msauth.}. Every name this class writes or sweeps starts with {@code msauth.}; + * the spend key ({@code spend_key}), the prover's record ({@code prover_pairing}, {@code + * prover_token}) and the file's own master key are never among them ({@link #isSignerEntry}). + */ +public final class SignerProfiles { + public static final String PREFIX = "msauth."; + static final String INDEX = "msauth.index"; + /** Names the sweep must never take, whatever a future change to {@link #PREFIX} does. */ + private static final Set PROTECTED = Set.of("spend_key", "prover_pairing", "prover_token", "_androidx_security_master_key_"); + private static final Pattern ID = Pattern.compile("[0-9a-z-]{1,64}"); + + /** The encrypted preferences; put and remove throw when the write does not land. */ + public interface KeyValue { + String get(String k); + + void put(String k, String v); + + void remove(String k); + + /** Every name in the store, for the sweep that also finds what the index does not list. */ + Set keys(); + } + + /** The Keystore's aliases ({@link KeystoreSeedCipher#keystoreAliases()}); both throw on failure. */ + public interface KeyAliases { + List list() throws Exception; + + void delete(String alias) throws Exception; + } + + public static final class Profile { + public final String id, label, policyHash; + public final int index; + + public Profile(String id, String label, int index, String policyHash) { + this.id = id; + this.label = label; + this.index = index; + this.policyHash = policyHash; + } + + JSONObject toJson() throws JSONException { + return new JSONObject().put("id", id).put("label", label).put("index", index).put("policyHash", policyHash); + } + + /** null for an entry that is not a whole profile. */ + static Profile of(Object item) { + if (!(item instanceof JSONObject)) return null; + JSONObject o = (JSONObject) item; + Object id = o.opt("id"), label = o.opt("label"), index = o.opt("index"), hash = o.opt("policyHash"); + if (!(id instanceof String) || !(label instanceof String) || !(index instanceof Integer) || !(hash instanceof String)) return null; + return new Profile((String) id, (String) label, (Integer) index, (String) hash); + } + } + + private final KeyValue kv; + private final KeyAliases aliases; + + public SignerProfiles(KeyValue kv) { + this(kv, null); + } + + /** {@code aliases} non-null: {@link #remove} also deletes the profile's Keystore key. */ + public SignerProfiles(KeyValue kv, KeyAliases aliases) { + this.kv = kv; + this.aliases = aliases; + } + + /** A fresh profile id: 32 lowercase hex characters. */ + public static String newId() { + byte[] b = new byte[16]; + new SecureRandom().nextBytes(b); + StringBuilder s = new StringBuilder(); + for (byte x : b) s.append(String.format("%02x", x & 0xff)); + return s.toString(); + } + + private static String policyKey(String id) { + return PREFIX + id + ".policy"; + } + + private static String seedKey(String id) { + return PREFIX + id + ".seed"; + } + + public List list() { + List out = new ArrayList<>(); + String raw = kv.get(INDEX); + if (raw == null) return out; + try { + JSONArray a = new JSONArray(raw); + for (int i = 0; i < a.length(); i++) { + Profile p = Profile.of(a.opt(i)); + if (p != null) out.add(p); + } + } catch (JSONException e) { + // An unreadable index lists nothing; the sweep still finds its entries. + } + return out; + } + + /** + * Saves a profile: its label (1-64 characters, counted in code points, trimmed), the policy and + * the sealed seed. Refused for a signer already saved (same policy, same position). Nothing is + * left behind when a write fails. + */ + public Profile add(Profile p, String policy, String sealedSeed) throws JSONException { + String label = p.label == null ? "" : p.label.trim(); + int n = label.codePointCount(0, label.length()); + if (n < 1 || n > 64) throw new IllegalArgumentException(L10n.t(R.string.msauth_label_length, "a signer label is 1-64 characters")); + if (p.id == null || !ID.matcher(p.id).matches()) throw new IllegalArgumentException("a signer profile id is 1-64 of [0-9a-z-]"); + List all = list(); + for (Profile q : all) { + if (q.policyHash.equals(p.policyHash) && q.index == p.index) { + throw new IllegalStateException(L10n.t(R.string.msauth_already_saved, "this signer is already saved as %1$s", q.label)); + } + } + Profile saved = new Profile(p.id, label, p.index, p.policyHash); + try { + kv.put(policyKey(p.id), policy); + kv.put(seedKey(p.id), sealedSeed); + all.add(saved); + writeIndex(all); + } catch (RuntimeException | JSONException e) { + try { + kv.remove(seedKey(p.id)); + kv.remove(policyKey(p.id)); + } catch (RuntimeException ignored) { + // the sweep at "remove this wallet" takes what is left + } + throw e; + } + return saved; + } + + /** Removes a profile: its Keystore key (when this store was given the aliases), its two entries, its row. */ + public void remove(String id) { + if (aliases != null) { + try { + aliases.delete(KeystoreSeedCipher.alias(id)); + } catch (Exception ignored) { + // A key without its sealed seed opens nothing; the wallet's sweep removes it later. + } + } + kv.remove(seedKey(id)); + kv.remove(policyKey(id)); + List all = list(); + all.removeIf(p -> p.id.equals(id)); + try { + writeIndex(all); + } catch (JSONException e) { + throw new IllegalStateException(e); + } + } + + public String policy(String id) { + return kv.get(policyKey(id)); + } + + public String sealedSeed(String id) { + return kv.get(seedKey(id)); + } + + private void writeIndex(List all) throws JSONException { + JSONArray a = new JSONArray(); + for (Profile p : all) a.put(p.toJson()); + kv.put(INDEX, a.toString()); + } + + /** A preference this class owns: {@code msauth.…}, never a protected name. */ + static boolean isSignerEntry(String name) { + return name != null && name.startsWith(PREFIX) && !PROTECTED.contains(name); + } + + /** A Keystore alias this class owns: {@code msauth.}, never a protected name. */ + static boolean isSignerAlias(String alias) { + return isSignerEntry(alias); + } + + /** + * Every profile, listed or not, and the list itself (removing the wallet). The sweep comes + * first: every Keystore key named {@code msauth.…}, then every entry named so but the index. + * The index goes last, only after the sweep succeeded: when any step throws, the index is kept + * so a retry still finds what is left, and the error is thrown — the caller then keeps the + * wallet (never a half-wipe). + */ + public static void eraseAll(KeyValue kv, KeyAliases aliases) throws Exception { + for (String a : aliases.list()) { + if (isSignerAlias(a)) aliases.delete(a); + } + for (String k : new ArrayList<>(kv.keys())) { + if (isSignerEntry(k) && !k.equals(INDEX)) kv.remove(k); + } + kv.remove(INDEX); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/CheckSession.java b/android/app/src/main/java/org/randprotocol/wallet/ui/CheckSession.java new file mode 100644 index 0000000..1c905f4 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/CheckSession.java @@ -0,0 +1,166 @@ +package org.randprotocol.wallet.ui; + +import org.json.JSONObject; +import org.randprotocol.wallet.wallet.SignFlow; + +import java.util.List; + +/** + * One sign screen's checking state, apart from the activity so its ordering is JVM-tested (iOS + * {@code CheckSession}): which text the review is of, which check is the latest, the payments, and + * the last good summary the payment rows are drawn from. Main thread only. + */ +public final class CheckSession { + /** One check as it was sent: its sequence number, the text, and the payments with their rows' snapshot. */ + public static final class Ticket { + public final int id; + public final String proposal; + public final String sentKey; + public final List payees; + + Ticket(int id, String proposal, String sentKey, List payees) { + this.id = id; + this.proposal = proposal; + this.sentKey = sentKey; + this.payees = payees; + } + } + + private PaymentsGate gate = new PaymentsGate(); + private SignReview review; + private JSONObject lastSummary; + private String typed = ""; + private String error; + private boolean inFlight; + private String reviewedProposal; + private List reviewedPayees = java.util.Collections.emptyList(); + private String sentProposal; + /** Rises with every check and every edit, and is never reset: only the latest check lands. */ + private int latest; + + public PaymentsGate gate() { + return gate; + } + + public SignReview review() { + return review; + } + + /** The last summary the core gave, kept when a later check is refused so the rows stay editable. */ + public JSONObject lastSummary() { + return lastSummary; + } + + public String typed() { + return typed; + } + + public void setTyped(String t) { + typed = t == null ? "" : t; + } + + public String error() { + return error; + } + + public boolean inFlight() { + return inFlight; + } + + /** The exact text the shown review was checked on: the only text Sign signs. */ + public String reviewedProposal() { + return reviewedProposal; + } + + /** The intended payments the shown review was checked with: the list Sign sends. */ + public List reviewedPayees() { + return reviewedPayees; + } + + /** A check starts for {@code proposal} with the payments last adopted; a new text starts the payments over. */ + public Ticket begin(String proposal) { + if (!proposal.equals(sentProposal)) { + gate.reset(); + lastSummary = null; + sentProposal = proposal; + } + latest++; + inFlight = true; + error = null; + return new Ticket(latest, proposal, gate.sentKey(), gate.payees()); + } + + /** A result lands; false when a newer check or an edit has superseded it. */ + public boolean land(Ticket t, JSONObject summary, SignFlow.Judgement j) { + if (t.id != latest) return false; + inFlight = false; + if (summary != null) lastSummary = summary; + gate.landed(t.sentKey); + review = new SignReview(summary, j); + reviewedProposal = t.proposal; + reviewedPayees = t.payees; + typed = ""; + return true; + } + + /** The check threw (the core refused, e.g. a mistyped payee): no review, but the payments stay. */ + public boolean fail(Ticket t, String message) { + if (t.id != latest) return false; + inFlight = false; + review = null; + reviewedProposal = null; + reviewedPayees = java.util.Collections.emptyList(); + typed = ""; + error = message; + return true; + } + + /** + * The proposal text was edited: the review no longer describes it, and a check still out is + * dropped. A text other than the one the payments were asked for also drops them. + */ + public void proposalEdited(String text) { + latest++; + inFlight = false; + review = null; + reviewedProposal = null; + reviewedPayees = java.util.Collections.emptyList(); + typed = ""; + error = null; + if (!text.equals(sentProposal)) { + gate.reset(); + lastSummary = null; + sentProposal = null; + } + } + + /** A signature's refusal replaces the verdict on the same summary. */ + public void refused(SignFlow.Judgement j) { + if (review != null) review = new SignReview(review.summary, j); + typed = ""; + } + + public void clearError() { + error = null; + } + + /** The screen is going: everything dropped, the sequence kept rising so a late result is ignored. */ + public void close() { + latest++; + gate = new PaymentsGate(); + review = null; + lastSummary = null; + typed = ""; + error = null; + inFlight = false; + reviewedProposal = null; + reviewedPayees = java.util.Collections.emptyList(); + sentProposal = null; + } + + /** Sign is armed: signable, the last four typed, the payments unchanged since the check, and the editor holds the checked text. */ + public boolean canSign(String editor) { + if (review == null || reviewedProposal == null || !reviewedProposal.equals(editor)) return false; + return review.canSign(typed, gate.stale()); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/CoreErrors.java b/android/app/src/main/java/org/randprotocol/wallet/ui/CoreErrors.java index 85df2a4..15e54f1 100644 --- a/android/app/src/main/java/org/randprotocol/wallet/ui/CoreErrors.java +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/CoreErrors.java @@ -93,6 +93,23 @@ private static void p(String re, int id, String english, int... inner) { e(R.string.core_err_payout_zero, "a payout of zero creates nothing; the chain refuses it"); e(R.string.core_err_cells_ascending, "cell keys must be strictly ascending"); + // ---- private multisig signing (msauth_*, fullnode randprotocol-msig; ui/engine/core-errors.js) ---- + e(R.string.core_err_msauth_tx_binding, "the transaction does not hash to the proposal's binding"); + e(R.string.core_err_msauth_signing_message, "the proposal's signing_message is not its binding's"); + e(R.string.core_err_msauth_auth_commit, "the proposal's auth_commit is not this policy's account"); + e(R.string.core_err_msauth_expected_digest, "the proposal's expected digest is not its bundle's"); + e(R.string.core_err_msauth_no_bundle, "the transaction carries no bundle"); + e(R.string.core_err_msauth_action, "the transaction carries an action; a private multisig proposal is a payment from its bundle alone"); + e(R.string.core_err_msauth_witness_unchecked, "the witness's value cannot be checked: its nk or salt is not this proposal's"); + e(R.string.core_err_msauth_not_signer, "this key is not one of the policy's signers"); + e(R.string.core_err_msauth_share_shape, "a share is index:hex"); + e(R.string.core_err_msauth_signer_key_shape, "a signer key is a rand-node keygen file or 64 hex characters"); + e(R.string.core_err_msauth_seed_shape, "a signer seed is 64 hex characters"); + e(R.string.core_err_msauth_expected_list, "expected_recipients must be a list of addresses"); + e(R.string.core_err_msauth_key_file_pk, "the key file's public_key is not the one its seed derives"); + e(R.string.core_err_msauth_key_file_address, "the key file's address is not the one its seed derives"); + e(R.string.core_err_msauth_no_payee, "no expected recipient given: the payee was NOT checked"); + // ---- composed: a prefix the core adds to another error ---- p("^the RAND fee: (.+)$", R.string.core_err_rand_fee, "the RAND fee: %1$s", 1); p("^recipient: (.+)$", R.string.core_err_recipient, "recipient: %1$s", 1); @@ -133,6 +150,18 @@ private static void p(String re, int id, String english, int... inner) { R.string.core_err_no_bundle_guest, "this build does not carry the chain's bundle guest %1$s: update the wallet"); p("^this chain's auth guest is ([0-9a-f]+); this wallet carries ([0-9a-f]+) — refusing to prove a v3 bundle it cannot authorise; update the wallet$", R.string.core_err_auth_guest, "this chain's auth guest is %1$s; this wallet carries %2$s — refusing to prove a v3 bundle it cannot authorise; update the wallet"); + // ---- private multisig signing ---- + p("^this proposal is for another policy: it names ([0-9a-f]+), this policy is ([0-9a-f]+)$", R.string.core_err_msauth_other_policy, "this proposal is for another policy: it names %1$s, this policy is %2$s"); + p("^output (\\d+) does not open as this account's: refusing to sign value it cannot see$", R.string.core_err_msauth_output_unopened, "output %1$s does not open as this account's: refusing to sign value it cannot see"); + p("^the witness's value does not match the transaction: input (\\d+) does not nullify to the bundle's nullifier$", R.string.core_err_msauth_witness_nullifier, "the witness's value does not match the transaction: input %1$s does not nullify to the bundle's nullifier"); + p("^the witness's value does not match the outputs: asset (\\d+) has (\\d+) in and (\\d+) out with the fee and burns$", R.string.core_err_msauth_witness_balance, "the witness's value does not match the outputs: asset %1$s has %2$s in and %3$s out with the fee and burns"); + p("^the proposal expires at (\\d+), outside its bundle's window \\((\\d+), (\\d+)\\]$", R.string.core_err_msauth_expiry_window, "the proposal expires at %1$s, outside its bundle's window (%2$s, %3$s]"); + p("^the transaction is for chain (\\d+), the proposal says chain (\\d+)$", R.string.core_err_msauth_tx_chain, "the transaction is for chain %1$s, the proposal says chain %2$s"); + p("^signer (\\d+)'s signature does not verify over this proposal$", R.string.core_err_msauth_share_invalid, "signer %1$s's signature does not verify over this proposal"); + p("^signer (\\d+) is not in this policy \\(it has (\\d+) signers\\)$", R.string.core_err_msauth_signer_range, "signer %1$s is not in this policy (it has %2$s signers)"); + p("^expected recipient (\\S+): (.+)$", R.string.core_err_msauth_expected_recipient, "expected recipient %1$s: %2$s", 2); + p("^the policy does not parse: (.+)$", R.string.core_err_msauth_policy_parse, "the policy does not parse: %1$s"); + p("^the proposal does not parse: (.+)$", R.string.core_err_msauth_proposal_parse, "the proposal does not parse: %1$s"); // ---- proving on this device ---- p("^proving failed: (.+)$", R.string.core_err_proving_failed, "proving failed: %1$s"); p("^proving the spend authorisation failed: (.+)$", R.string.core_err_proving_auth_failed, "proving the spend authorisation failed: %1$s"); @@ -177,6 +206,14 @@ public static String translate(String message) { return message; } + /** Whether {@code message} is one this table maps (exactly or by pattern). */ + static boolean knows(String message) { + if (message == null) return false; + if (EXACT.containsKey(message)) return true; + for (Patterned p : PATTERNED) if (p.re.matcher(message).matches()) return true; + return false; + } + /** {@code e}'s message for a screen: a core message translated, anything else as it is (its class name when it has none). */ public static String of(Throwable e) { if (e == null) return ""; diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/CryptoPrompt.java b/android/app/src/main/java/org/randprotocol/wallet/ui/CryptoPrompt.java new file mode 100644 index 0000000..ff2cd74 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/CryptoPrompt.java @@ -0,0 +1,156 @@ +package org.randprotocol.wallet.ui; + +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.security.KeystoreSeedCipher; +import org.randprotocol.wallet.security.KeystoreSeedSource; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; + +import javax.crypto.Cipher; + +/** + * The system's prompt over a signer key's cipher (a BiometricPrompt with a CryptoObject): the + * fingerprint, face or — on Android 11 and later — the screen lock, once per use. {@link + * #authenticate} is called off the main thread ({@link org.randprotocol.wallet.wallet.SignFlow#sign}), + * posts the prompt to the main thread and blocks until it ends; {@link #cancel} dismisses it and + * releases the caller. + */ +final class CryptoPrompt implements KeystoreSeedSource.Authenticator { + /** The prompt ended without a cipher: {@link #code} is BiometricPrompt's error code. */ + static final class Failed extends Exception { + final int code; + + Failed(int code, CharSequence message) { + super(String.valueOf(message)); + this.code = code; + } + + /** The person dismissed it (Cancel, back, or the app left): nothing to show. */ + boolean dismissed() { + return code == BiometricPrompt.ERROR_USER_CANCELED || code == BiometricPrompt.ERROR_NEGATIVE_BUTTON + || code == BiometricPrompt.ERROR_CANCELED; + } + } + + interface Showing { + void showing(boolean up); + } + + private final FragmentActivity activity; + private final String title; + private final Showing showing; + private final Handler main = new Handler(Looper.getMainLooper()); + private BiometricPrompt prompt; // main thread only + private CountDownLatch pending; // the call waiting, if any + /** The prompt was asked for and has not yet covered the screen (main thread only). */ + private boolean awaitingUp; + private volatile boolean cancelled; + + CryptoPrompt(FragmentActivity activity, String title, Showing showing) { + this.activity = activity; + this.title = title; + this.showing = showing; + } + + /** The prompt for {@code sdk}: BIOMETRIC_STRONG with a Cancel button below API 30, or with the device credential from API 30. */ + static BiometricPrompt.PromptInfo info(String title, String cancel) { + int sdk = Build.VERSION.SDK_INT; + BiometricPrompt.PromptInfo.Builder b = new BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setConfirmationRequired(false) + .setAllowedAuthenticators(KeystoreSeedCipher.promptAuthenticators(sdk)); + if (!KeystoreSeedCipher.credentialPerUse(sdk)) b.setNegativeButtonText(cancel); + return b.build(); + } + + @Override + public Cipher authenticate(Cipher cipher) throws Exception { + if (Looper.myLooper() == Looper.getMainLooper()) throw new IllegalStateException("the prompt blocks: never on the main thread"); + CountDownLatch done = new CountDownLatch(1); + Cipher[] result = new Cipher[1]; + Exception[] failure = new Exception[1]; + main.post(() -> { + pending = done; + if (cancelled || activity.isFinishing() || activity.isDestroyed()) { + failure[0] = new CancellationException(); + done.countDown(); + return; + } + prompt = new BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult r) { + BiometricPrompt.CryptoObject c = r.getCryptoObject(); + if (c == null || c.getCipher() == null) failure[0] = new IllegalStateException("the prompt returned no cipher"); + else result[0] = c.getCipher(); + end(done); + } + + @Override + public void onAuthenticationError(int code, @NonNull CharSequence message) { + failure[0] = new Failed(code, message); + end(done); + } + // onAuthenticationFailed (a finger not recognised): the prompt stays up and asks again. + }); + try { + prompt.authenticate(info(title, activity.getString(R.string.cancel)), new BiometricPrompt.CryptoObject(cipher)); + // "Showing" only once the prompt has actually taken the window's focus (windowFocus): + // a prompt that never appears never marks the screen as covered. + awaitingUp = true; + } catch (RuntimeException e) { + failure[0] = e; + end(done); + } + }); + try { + done.await(); + } catch (InterruptedException e) { + cancel(); + Thread.currentThread().interrupt(); + throw new CancellationException(); + } + if (failure[0] != null) throw failure[0]; + if (cancelled) throw new CancellationException(); + return result[0]; + } + + /** The activity's window focus (onWindowFocusChanged): losing it while the prompt is asked for means the prompt is up. */ + void windowFocus(boolean hasFocus) { + if (!hasFocus && awaitingUp && pending != null) { + awaitingUp = false; + showing.showing(true); + } + } + + private void end(CountDownLatch done) { + awaitingUp = false; + prompt = null; + pending = null; + showing.showing(false); + done.countDown(); + } + + /** Dismisses the prompt if it is up and releases the waiting call; later calls are refused. Any thread. */ + void cancel() { + cancelled = true; + main.post(() -> { + if (prompt != null) prompt.cancelAuthentication(); + CountDownLatch p = pending; + prompt = null; + pending = null; + awaitingUp = false; + showing.showing(false); + if (p != null) p.countDown(); + }); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/PaymentsGate.java b/android/app/src/main/java/org/randprotocol/wallet/ui/PaymentsGate.java new file mode 100644 index 0000000..d6bb50c --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/PaymentsGate.java @@ -0,0 +1,76 @@ +package org.randprotocol.wallet.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Which payment rows the last check was sent for, and whether editing them since disarmed Sign + * (iOS {@code PaymentsGate}). Editing any row sets {@link #stale}; only a check whose snapshot of + * the rows, taken when it was sent, equals the rows when its result lands clears it. The top Check + * and Retry resend the payments last adopted, with the snapshot those were built from, so they + * cannot re-arm Sign after an edit. + */ +public final class PaymentsGate { + private final List rows = new ArrayList<>(); + private List payees = Collections.emptyList(); + private String checkedKey; + private boolean stale; + + public PaymentsGate() { + reset(); + } + + /** The rows being edited; a caller that changes one calls {@link #edited}. */ + public List rows() { + return rows; + } + + /** The entries last adopted ("Check payments"): what every check sends. */ + public List payees() { + return payees; + } + + public boolean stale() { + return stale; + } + + public void edited() { + stale = true; + } + + public void addRow() { + rows.add(new SignRows.Row()); + edited(); + } + + public void removeRow(SignRows.Row r) { + rows.remove(r); + edited(); + } + + /** "Check payments": the entries built from the rows as they are now. */ + public void adopt(List entries) { + payees = Collections.unmodifiableList(new ArrayList<>(entries)); + checkedKey = SignRows.key(rows); + } + + /** A new proposal: its payments are asked again, from one empty row. */ + public void reset() { + rows.clear(); + rows.add(new SignRows.Row()); + payees = Collections.emptyList(); + checkedKey = SignRows.key(rows); + stale = false; + } + + /** The rows a check is being sent for: those the adopted payees were built from. */ + public String sentKey() { + return checkedKey; + } + + /** A check's result arrived: stale if the rows moved since its snapshot (or were edited before). */ + public void landed(String sentKey) { + stale = !sentKey.equals(SignRows.key(rows)); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/PickedFile.java b/android/app/src/main/java/org/randprotocol/wallet/ui/PickedFile.java new file mode 100644 index 0000000..d827eb8 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/PickedFile.java @@ -0,0 +1,48 @@ +package org.randprotocol.wallet.ui; + +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.L10n; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** A proposal, a policy or a key file the person picked: refused by its size before it is read, and a read failure has its own sentence. */ +public final class PickedFile { + private PickedFile() {} + + /** The JS screen's {@code MAX_FILE_BYTES}. */ + public static final int MAX_BYTES = 1 << 20; + + static String tooLarge() { + return L10n.t(R.string.msauth_file_too_large, "That file is larger than a proposal, a policy or a key file can be."); + } + + static String unreadable() { + return L10n.t(R.string.msauth_file_unreadable, "That file could not be read."); + } + + /** + * The file's text. {@code size} is the provider's size (negative when unknown): over the cap, + * the file is refused before a byte is read; the read itself stops past the cap, so a size that + * lied still cannot hand over more. IllegalArgumentException carries the sentence. + */ + public static String read(long size, InputStream in) { + if (size > MAX_BYTES) throw new IllegalArgumentException(tooLarge()); + if (in == null) throw new IllegalArgumentException(unreadable()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + try (InputStream s = in) { + int n; + while ((n = s.read(buf)) != -1) { + out.write(buf, 0, n); + if (out.size() > MAX_BYTES) throw new IllegalArgumentException(tooLarge()); + } + } catch (IOException | RuntimeException e) { + if (e instanceof IllegalArgumentException) throw (IllegalArgumentException) e; + throw new IllegalArgumentException(unreadable()); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/RemoveAuth.java b/android/app/src/main/java/org/randprotocol/wallet/ui/RemoveAuth.java new file mode 100644 index 0000000..57ccc89 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/RemoveAuth.java @@ -0,0 +1,45 @@ +package org.randprotocol.wallet.ui; + +import androidx.biometric.BiometricManager; + +/** + * How removing a signer asks for the device's authentication (no CryptoObject: it only confirms the + * person). The ask is skipped only on a device with no secure lock at all ({@code + * KeyguardManager.isDeviceSecure()} false); on any other device, whatever BiometricManager answers + * (UNSUPPORTED on API 28–29 for a biometric-or-credential prompt, no enrolment, no hardware…), the + * person is asked — by BiometricPrompt where it can, else by the system's confirm-credential screen. + */ +public final class RemoveAuth { + public enum Kind { NONE, PROMPT, CONFIRM_CREDENTIAL } + + public final Kind kind; + /** For {@link Kind#PROMPT}: the authenticators to allow. */ + public final int authenticators; + /** For {@link Kind#PROMPT}: a Cancel button is required (no device credential in the prompt). */ + public final boolean negativeButton; + + private RemoveAuth(Kind kind, int authenticators, boolean negativeButton) { + this.kind = kind; + this.authenticators = authenticators; + this.negativeButton = negativeButton; + } + + /** What BiometricPrompt is asked for on {@code sdk}: weak biometric or the credential from API 30, weak biometric alone below. */ + public static int authenticators(int sdk) { + return sdk >= 30 + ? BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL + : BiometricManager.Authenticators.BIOMETRIC_WEAK; + } + + /** + * {@code deviceSecure}: KeyguardManager.isDeviceSecure(); {@code canAuthenticate}: BiometricManager's + * answer for {@link #authenticators(int)} on this device. + */ + public static RemoveAuth choose(int sdk, boolean deviceSecure, int canAuthenticate) { + if (!deviceSecure) return new RemoveAuth(Kind.NONE, 0, false); + if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { + return new RemoveAuth(Kind.PROMPT, authenticators(sdk), sdk < 30); + } + return new RemoveAuth(Kind.CONFIRM_CREDENTIAL, 0, false); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/SettingsActivity.java b/android/app/src/main/java/org/randprotocol/wallet/ui/SettingsActivity.java index df68da4..1cfd3c1 100644 --- a/android/app/src/main/java/org/randprotocol/wallet/ui/SettingsActivity.java +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/SettingsActivity.java @@ -151,6 +151,9 @@ protected void onCreate(Bundle savedInstanceState) { } })); + // Multisig signers + b.signers.setOnClickListener(v -> startActivity(new Intent(this, SignersActivity.class))); + // Security spinner(b.autolock, R.array.autolock_labels, indexOf(getResources().getIntArray(R.array.autolock_minutes), prefs.autoLockMinutes()), i -> prefs.setAutoLockMinutes(getResources().getIntArray(R.array.autolock_minutes)[i])); @@ -170,7 +173,13 @@ protected void onCreate(Bundle savedInstanceState) { .setTitle(R.string.settings_remove) .setMessage(R.string.settings_remove_warning) .setPositiveButton(R.string.settings_remove, (d, w) -> { - wallet().removeWallet(); + try { + wallet().removeWallet(); + } catch (IllegalStateException e) { + // The signer keys could not all be removed: the wallet is kept, whole. + new AlertDialog.Builder(this).setMessage(CoreErrors.of(e)).setPositiveButton(android.R.string.ok, null).show(); + return; + } startActivity(new Intent(this, WelcomeActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)); finish(); }) diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/SignProposalActivity.java b/android/app/src/main/java/org/randprotocol/wallet/ui/SignProposalActivity.java new file mode 100644 index 0000000..bab0e40 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/SignProposalActivity.java @@ -0,0 +1,513 @@ +package org.randprotocol.wallet.ui; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; + +import org.json.JSONObject; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.databinding.ActivitySignProposalBinding; +import org.randprotocol.wallet.databinding.ItemPaymentRowBinding; +import org.randprotocol.wallet.rpc.RpcClient; +import org.randprotocol.wallet.security.KeystoreSeedSource; +import org.randprotocol.wallet.security.Prefs; +import org.randprotocol.wallet.security.SignerProfiles; +import org.randprotocol.wallet.util.L10n; +import org.randprotocol.wallet.wallet.NativeMsauthCore; +import org.randprotocol.wallet.wallet.SignFlow; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Executors; + +/** + * Sign a private-multisig proposal (spec 2026-10-11-msauth-signing-design.md §4; iOS + * SignProposalView, ui/screens/signers.js): paste or open proposal.json, enter the intended + * payments, review exactly what the core checked (its lines verbatim), type the fingerprint's last + * four, authenticate, share. The decisions are {@link CheckSession}'s (JVM-tested); a signature + * goes only through {@link SignFlow#sign}, which re-checks, prompts, re-reads the head and zeroes + * the seed. The proposal lives in this activity only and is dropped in onDestroy. + */ +public class SignProposalActivity extends BaseActivity { + public static final String EXTRA_ID = "id"; + + private ActivitySignProposalBinding b; + private final ExecutorService work = Executors.newSingleThreadExecutor(); + private final CheckSession session = new CheckSession(); + private String id; + private SignerProfiles.Profile profile; + private int threshold; + private SignFlow flow; + private ActivityResultLauncher openProposal; + + // The signature in progress. + private boolean signing; + private Thread signThread; + /** The signature in progress's own cancel flag: a new signature never un-cancels an old one. */ + private AtomicBoolean signCancel = new AtomicBoolean(); + /** The prompt of the signature in progress (a cancelled prompt stays cancelled, so each signature has its own). */ + private volatile CryptoPrompt prompt; + private boolean promptUp; + private SignFlow.Share share; + private String rowError; + /** A file's or a signature's failure, shown under the proposal until the next check or edit. */ + private String note; + private final TreeSet inHand = new TreeSet<>(); + /** The review the screen last drew, so a repaint does not rebuild it (and its typed text). */ + private SignReview painted; + /** Set while the screen writes a field itself: its watcher is not an edit. */ + private boolean writing; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + b = ActivitySignProposalBinding.inflate(getLayoutInflater()); + setContentView(b.getRoot()); + id = getIntent().getStringExtra(EXTRA_ID); + SignerProfiles profiles = wallet().vault().signerProfiles(); + for (SignerProfiles.Profile p : profiles.list()) if (p.id.equals(id)) profile = p; + if (profile == null) { + b.profileLabel.setText(L10n.t(R.string.msauth_no_profile, "No such signer profile.")); + b.checkProposal.setEnabled(false); + b.openFile.setEnabled(false); + b.checkShare.setEnabled(false); + return; + } + b.profileLabel.setText(profile.label); + String policy = profiles.policy(id); + work.execute(() -> { + JSONObject info; + try { + info = NativeMsauthCore.INSTANCE.msauthPolicyInfo(policy); + } catch (Exception e) { + info = new JSONObject(); + } + JSONObject i = info; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + threshold = i.optInt("threshold"); + b.profileFp.setText(i.optString("fingerprint")); + b.profilePosition.setText(SignersActivity.position(threshold, i.optInt("signers"), profile.index)); + }); + }); + + Prefs prefs = wallet().prefs(); + flow = new SignFlow(profiles, NativeMsauthCore.INSTANCE, SignFlow.node(new RpcClient(prefs.rpcUrl())), prefs.chainId(), + new KeystoreSeedSource(profiles, cipher -> { + CryptoPrompt p = prompt; + if (p == null) throw new CancellationException(); + return p.authenticate(cipher); + })); + + openProposal = registerForActivityResult(new ActivityResultContracts.OpenDocument(), this::picked); + b.openFile.setOnClickListener(v -> openProposal.launch(new String[] {"application/json", "text/plain"})); + b.proposal.addTextChangedListener(watcher(() -> { + // The review describes the text that was checked: editing the text drops it until a new check. + session.proposalEdited(editor()); + share = null; + note = null; + rowError = null; + paintRows(); + paint(); + })); + b.checkProposal.setOnClickListener(v -> startCheck()); + b.retry.setOnClickListener(v -> startCheck()); + b.addPayment.setOnClickListener(v -> { + if (signing) return; + session.gate().addRow(); + paintRows(); + paint(); + }); + b.checkPayments.setOnClickListener(v -> checkPayments()); + b.confirm.addTextChangedListener(watcher(() -> { + session.setTyped(String.valueOf(b.confirm.getText())); + paint(); + })); + b.sign.setOnClickListener(v -> sign()); + b.copyShare.setOnClickListener(v -> { + if (share != null) copy(getString(R.string.msauth_share_subject), share.share, getString(R.string.copied)); + }); + b.sendShare.setOnClickListener(v -> { + if (share == null) return; + Intent i = new Intent(Intent.ACTION_SEND).setType("text/plain") + .putExtra(Intent.EXTRA_TEXT, share.share) + .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.msauth_share_subject)); + startActivity(Intent.createChooser(i, getString(R.string.msauth_share))); + }); + b.checkShare.setOnClickListener(v -> verifyShare()); + paintRows(); + paint(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + CryptoPrompt p = prompt; + if (p != null) p.windowFocus(hasFocus); + } + + @Override + protected void onStop() { + super.onStop(); + // Leaving the screen stops a signature in progress. While the system's prompt is up the + // screen can be stopped by the prompt itself (the screen-lock entry on some devices): the + // prompt is then the one that ends, and a dismissed prompt stops the signature. + if (signing && (!promptUp || isFinishing())) cancelSign(); + } + + @Override + protected void onDestroy() { + cancelSign(); + work.shutdownNow(); + // The proposal carries the account's witness: it is never stored and goes with the screen. + session.close(); + share = null; + if (b != null) { + writing = true; + b.proposal.setText(""); + b.shareText.setText(""); + writing = false; + } + super.onDestroy(); + } + + private void cancelSign() { + signCancel.set(true); + CryptoPrompt p = prompt; + if (p != null) p.cancel(); + Thread t = signThread; + if (t != null) t.interrupt(); + } + + private String editor() { + return String.valueOf(b.proposal.getText()); + } + + private interface Edit { + void edited(); + } + + private TextWatcher watcher(Edit e) { + return new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int st, int c, int a) {} + + @Override public void onTextChanged(CharSequence s, int st, int bf, int c) {} + + @Override public void afterTextChanged(Editable s) { + if (!writing) e.edited(); + } + }; + } + + // ------------------------------------------------------------------ files + + private void picked(Uri uri) { + if (uri == null) return; + work.execute(() -> { + String text = null, err = null; + try (InputStream in = getContentResolver().openInputStream(uri)) { + text = PickedFile.read(sizeOf(uri), in); + } catch (IllegalArgumentException e) { + err = e.getMessage(); + } catch (Exception e) { + err = PickedFile.unreadable(); + } + String t = text, m = err; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + if (t != null && !signing) b.proposal.setText(t); + if (m != null) showError(m); + }); + }); + } + + private long sizeOf(Uri uri) { + try (Cursor c = getContentResolver().query(uri, new String[] {OpenableColumns.SIZE}, null, null, null)) { + if (c != null && c.moveToFirst() && !c.isNull(0)) return c.getLong(0); + } catch (Exception ignored) { + // unknown: the read itself stops past the cap + } + return -1; + } + + // ------------------------------------------------------------------ checking + + /** "Check payments": the entries from the rows (each with its own amount rule), then a check with them. */ + private void checkPayments() { + if (session.inFlight() || signing) return; + try { + List entries = SignRows.entries(session.gate().rows(), session.lastSummary()); + rowError = null; + session.gate().adopt(entries); + } catch (IllegalArgumentException e) { + rowError = e.getMessage(); + paint(); + return; + } + startCheck(); + } + + /** + * The top Check and Retry send the payments last adopted, with the rows' snapshot those came + * from, so edits since keep Sign off. Only the latest check's result is applied. + */ + private void startCheck() { + if (session.inFlight() || signing || flow == null || editor().isEmpty()) return; + share = null; + note = null; + CheckSession.Ticket t = session.begin(editor()); + // A new proposal text started the payments over. + paintRows(); + paint(); + work.execute(() -> { + try { + SignFlow.Check c = flow.check(id, t.proposal, t.payees); + runOnUiThread(() -> { + if (session.land(t, c.summary, c.judgement)) landed(); + }); + } catch (Exception e) { + String m = CoreErrors.of(e); + runOnUiThread(() -> { + if (session.fail(t, m)) landed(); + }); + } + }); + } + + private void landed() { + if (isFinishing() || isDestroyed()) return; + paintRows(); + paint(); + } + + // ------------------------------------------------------------------ signing + + private void sign() { + String text = session.reviewedProposal(); + if (signing || session.inFlight() || text == null || !session.canSign(editor())) return; + List payees = session.reviewedPayees(); + signing = true; + paintRows(); + AtomicBoolean cancel = new AtomicBoolean(); + signCancel = cancel; + prompt = new CryptoPrompt(this, getString(R.string.msauth_sign_reason), up -> promptUp = up); + session.clearError(); + note = null; + paint(); + signThread = new Thread(() -> { + SignFlow.Share made = null; + SignFlow.Judgement refusal = null; + String error = null; + try { + made = flow.sign(id, text, payees, cancel::get); + } catch (SignFlow.Refusal r) { + refusal = r.judgement; + } catch (CancellationException e) { + // left the screen, or the prompt was dismissed: nothing to show + } catch (CryptoPrompt.Failed f) { + if (!f.dismissed()) error = f.getMessage(); + } catch (KeyPermanentlyInvalidatedException e) { + error = L10n.t(R.string.msauth_key_invalidated, "This signer's key no longer opens: the phone's fingerprints or screen lock changed since it was saved. Remove the signer and add it again from its key file."); + } catch (Exception e) { + error = CoreErrors.of(e); + } + SignFlow.Share s = made; + SignFlow.Judgement r = refusal; + String m = error; + runOnUiThread(() -> { + signThread = null; + signing = false; + prompt = null; + if (!isDestroyed()) paintRows(); + if (isFinishing() || isDestroyed() || cancel.get()) { + paintIfAlive(); + return; + } + if (s != null) { + share = s; + writing = true; + b.confirm.setText(""); + writing = false; + session.setTyped(""); + } else if (r != null) { + session.refused(r); + } else if (m != null) { + note = m; + } + paint(); + }); + }, "msauth-sign"); + signThread.start(); + } + + private void paintIfAlive() { + if (!isFinishing() && !isDestroyed()) paint(); + } + + private void verifyShare() { + if (profile == null) return; + String proposal = session.reviewedProposal() != null ? session.reviewedProposal() : editor(); + String given = String.valueOf(b.shareIn.getText()); + work.execute(() -> { + Integer index = null; + String error = null; + try { + index = flow.verifyShare(id, proposal, given); + } catch (Exception e) { + error = CoreErrors.of(e); + } + Integer i = index; + String m = error; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + if (i == null) { + b.verifyOut.setText(m); + return; + } + inHand.add(i); + List list = new ArrayList<>(); + for (Integer n : inHand) list.add(String.valueOf(n)); + b.verifyOut.setText(L10n.t(R.string.msauth_share_verifies, "Signer %1$s's share verifies. %2$s of %3$s in hand: signers %4$s.", + String.valueOf(i), String.valueOf(inHand.size()), String.valueOf(threshold), String.join(", ", list))); + }); + }); + } + + // ------------------------------------------------------------------ painting + + private void showError(String m) { + note = m; + paint(); + } + + /** The row objects the views below were built for, and their views, in order. */ + private final List paintedRows = new ArrayList<>(); + private final List rowViews = new ArrayList<>(); + + /** + * The payment rows' fields: rebuilt only when the gate's rows are other row objects than those + * drawn (a row added or removed, the payments started over), so a landing check never + * interrupts typing; otherwise only each row's note and enabled state are refreshed. + */ + private void paintRows() { + List rows = session.gate().rows(); + boolean same = rows.size() == paintedRows.size(); + for (int k = 0; same && k < rows.size(); k++) same = rows.get(k) == paintedRows.get(k); + if (!same) { + b.paymentRows.removeAllViews(); + paintedRows.clear(); + rowViews.clear(); + for (SignRows.Row row : rows) { + ItemPaymentRowBinding r = ItemPaymentRowBinding.inflate(getLayoutInflater(), b.paymentRows, false); + r.addr.setText(row.addr); + r.amount.setText(row.amount); + r.addr.addTextChangedListener(watcher(() -> { + row.addr = String.valueOf(r.addr.getText()); + session.gate().edited(); + r.nonrandNote.setVisibility(SignRows.amountOptional(row, session.lastSummary()) ? View.VISIBLE : View.GONE); + r.mixedNote.setVisibility(SignRows.mixedPayee(row, session.lastSummary()) ? View.VISIBLE : View.GONE); + paint(); + })); + r.amount.addTextChangedListener(watcher(() -> { + row.amount = String.valueOf(r.amount.getText()); + session.gate().edited(); + paint(); + })); + r.remove.setOnClickListener(v -> { + if (signing) return; + session.gate().removeRow(row); + paintRows(); + paint(); + }); + b.paymentRows.addView(r.getRoot()); + paintedRows.add(row); + rowViews.add(r); + } + } + for (int k = 0; k < rows.size(); k++) { + ItemPaymentRowBinding r = rowViews.get(k); + r.nonrandNote.setVisibility(SignRows.amountOptional(rows.get(k), session.lastSummary()) ? View.VISIBLE : View.GONE); + r.mixedNote.setVisibility(SignRows.mixedPayee(rows.get(k), session.lastSummary()) ? View.VISIBLE : View.GONE); + r.addr.setEnabled(!signing); + r.amount.setEnabled(!signing); + r.remove.setEnabled(!signing); + } + } + + /** Every state the screen shows, from the session and the signature in progress. */ + private void paint() { + boolean busy = session.inFlight() || signing; + boolean hasProfile = profile != null; + b.proposal.setEnabled(!signing); + b.openFile.setEnabled(!signing && hasProfile); + b.checkProposal.setEnabled(!busy && hasProfile && !editor().isEmpty()); + b.checkProposal.setText(session.inFlight() ? R.string.msauth_checking : R.string.msauth_check_proposal); + SignersActivity.show(b.error, session.error() != null ? session.error() : note); + + // Payments: while the last good summary has something to pay for. + b.payments.setVisibility(SignRows.showsPayments(session.lastSummary()) ? View.VISIBLE : View.GONE); + b.checkPayments.setEnabled(!busy); + b.addPayment.setEnabled(!signing); + SignersActivity.show(b.rowError, rowError); + + SignReview r = session.review(); + b.review.setVisibility(r == null ? View.GONE : View.VISIBLE); + if (r != painted) { + painted = r; + if (r != null) paintReview(r); + } + if (r != null) { + boolean refused = r.refusal() != null; + b.refused.setVisibility(refused ? View.VISIBLE : View.GONE); + b.signable.setVisibility(refused ? View.GONE : View.VISIBLE); + b.retry.setEnabled(!busy); + b.staleNote.setVisibility(session.gate().stale() ? View.VISIBLE : View.GONE); + b.confirm.setEnabled(!signing); + b.sign.setEnabled(!busy && session.canSign(editor())); + b.sign.setText(signing ? R.string.msauth_signing : R.string.msauth_sign); + } + + b.shareBox.setVisibility(share == null ? View.GONE : View.VISIBLE); + b.shareText.setText(share == null ? "" : share.share); + } + + private void paintReview(SignReview r) { + b.summary.setVisibility(r.showsSummary() ? View.VISIBLE : View.GONE); + b.lines.removeAllViews(); + if (r.showsSummary()) { + for (String line : r.lines()) { + TextView t = new TextView(this); + t.setTextAppearance(R.style.Text_Mono); + t.setTextSize(12); + t.setTextIsSelectable(true); + t.setText(line); // verbatim + t.setPadding(0, 2, 0, 2); + b.lines.addView(t); + } + b.fee.setText(L10n.t(R.string.msauth_fee_line, "Fee: %1$s RAND · expires at height %2$s", r.fee(), r.expiresAt())); + b.matched.setText(L10n.t(R.string.msauth_matched, "%1$s of %2$s payments matched.", String.valueOf(r.payingMatched()), String.valueOf(r.payingTotal()))); + } + b.refusal.setText(r.refusal() == null ? "" : r.refusal()); + b.blocksLeft.setText(r.refusal() == null ? L10n.t(R.string.msauth_blocks_left, "%1$s blocks left to prove it.", String.valueOf(r.blocksLeft())) : ""); + writing = true; + b.confirm.setText(session.typed()); + writing = false; + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/SignReview.java b/android/app/src/main/java/org/randprotocol/wallet/ui/SignReview.java new file mode 100644 index 0000000..50a2563 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/SignReview.java @@ -0,0 +1,84 @@ +package org.randprotocol.wallet.ui; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.randprotocol.wallet.util.Amounts; +import org.randprotocol.wallet.wallet.SignFlow; + +import java.util.List; + +/** + * What the sign screen shows of one check (iOS {@code SignProposalModel}): the core's lines + * verbatim, the refusal's sentence, and whether Sign may be pressed. Every read of the summary is + * tolerant: a malformed summary shows its refusal and nothing else, never a crash. + */ +public final class SignReview { + /** The core's reply; null when it was not a JSON object. */ + public final JSONObject summary; + public final SignFlow.Judgement judgement; + + public SignReview(JSONObject summary, SignFlow.Judgement judgement) { + this.summary = summary; + this.judgement = judgement; + } + + /** fullnode's lines, verbatim (never translated). */ + public List lines() { + return SignFlow.lines(summary); + } + + /** A summary the core did not shape as expected (no list of lines, no list of outputs) is shown as its refusal alone. */ + public boolean showsSummary() { + if (summary == null || !(summary.opt("lines") instanceof JSONArray) || !(summary.opt("outputs") instanceof JSONArray)) return false; + JSONArray lines = (JSONArray) summary.opt("lines"); + for (int i = 0; i < lines.length(); i++) if (!(lines.opt(i) instanceof String)) return false; + return true; + } + + /** The refusal's sentence in the user's language; null when signable. */ + public String refusal() { + return judgement.message(); + } + + public static boolean retryable(SignFlow.Verdict v) { + return v == SignFlow.Verdict.NO_NODE; + } + + /** No node answered: Retry asks again. */ + public boolean retryable() { + return retryable(judgement.verdict); + } + + /** Signable, the payments unchanged since the check, and the fingerprint's last four typed (dashes and case ignored). */ + public boolean canSign(String typed, boolean stale) { + if (stale || !judgement.signable() || summary == null) return false; + Object fp = summary.opt("policy_fingerprint"); + return fp instanceof String && SignFlow.confirms((String) fp, typed); + } + + public long blocksLeft() { + return judgement.blocksLeft; + } + + public int payingTotal() { + return SignRows.payingTotal(summary); + } + + public int payingMatched() { + return SignRows.payingMatched(summary); + } + + /** The fee in RAND ("0.001"); "0" when the summary carries none it can read. */ + public String fee() { + Object f = summary == null ? null : summary.opt("fee"); + String units = f instanceof Number || f instanceof String ? String.valueOf(f).trim() : ""; + return units.matches("\\d+") ? Amounts.format(units) : "0"; + } + + /** The expiry height as text; "?" when the summary carries none it can read. */ + public String expiresAt() { + Object e = summary == null ? null : summary.opt("expires_at"); + String t = e instanceof Number ? String.valueOf(e) : ""; + return t.matches("\\d+") ? t : "?"; + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/SignRows.java b/android/app/src/main/java/org/randprotocol/wallet/ui/SignRows.java new file mode 100644 index 0000000..c4bbb06 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/SignRows.java @@ -0,0 +1,176 @@ +package org.randprotocol.wallet.ui; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.util.Amounts; +import org.randprotocol.wallet.util.L10n; +import org.randprotocol.wallet.wallet.SignFlow; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The pure decisions behind the sign screen's "Intended payments" (iOS {@code SignRows}, + * ui/screens/signers.js): an output that needs a payee, a row as the core's {@code addr:AMOUNT} + * entry, and whether a row may go without its amount. + */ +public final class SignRows { + private SignRows() {} + + /** One intended payment as the signer is typing it: the address and the amount in RAND text. */ + public static final class Row { + public String addr; + public String amount; + + public Row() { + this("", ""); + } + + public Row(String addr, String amount) { + this.addr = addr == null ? "" : addr; + this.amount = amount == null ? "" : amount; + } + } + + /** + * An output the signer must name a payee for: it leaves the account ({@code to_account} is not + * {@code true}) and moves something. Decided from the output itself, not from the core's own flag. + */ + public static boolean needsPayee(Object output) { + if (!(output instanceof JSONObject)) return false; + JSONObject o = (JSONObject) output; + return !Boolean.TRUE.equals(o.opt("to_account")) && !SignFlow.isZeroAmount(o.opt("amount")); + } + + public static boolean isNonRand(JSONObject o) { + Long a = SignFlow.assetId(o.opt("asset")); + return a != null && a != 0; + } + + public static boolean isMatched(JSONObject o) { + Object m = o.opt("matches"); + return m instanceof String && !SignFlow.jsTrim((String) m).isEmpty(); + } + + /** The paying outputs of a summary; none when the summary has no list of outputs. */ + static List paying(JSONObject summary) { + List out = new ArrayList<>(); + JSONArray a = summary == null ? null : summary.optJSONArray("outputs"); + for (int i = 0; a != null && i < a.length(); i++) { + Object o = a.opt(i); + if (needsPayee(o)) out.add((JSONObject) o); + } + return out; + } + + public static int payingTotal(JSONObject summary) { + return paying(summary).size(); + } + + public static int payingMatched(JSONObject summary) { + int n = 0; + for (JSONObject o : paying(summary)) if (isMatched(o)) n++; + return n; + } + + /** The payments section shows while the last good summary has something to pay for. */ + public static boolean showsPayments(JSONObject lastSummary) { + return !paying(lastSummary).isEmpty(); + } + + /** The rows as one string, to tell whether they changed since a check was sent (length-prefixed: no separator forges another list). */ + public static String key(List rows) { + StringBuilder sb = new StringBuilder(); + for (Row r : rows) { + sb.append(r.addr.length()).append(':').append(r.addr).append('|') + .append(r.amount.length()).append(':').append(r.amount).append('\n'); + } + return sb.toString(); + } + + /** + * One intended payment as the core's {@code addr:AMOUNT}. AMOUNT is RAND display text, checked + * by {@link Amounts#parse} and passed as typed (trimmed). It may be left out only when {@code + * amountOptional}. Throws IllegalArgumentException with the sentence for the signer. + */ + public static String entry(Row row, boolean amountOptional) { + String a = SignFlow.jsTrim(row.addr); + String v = SignFlow.jsTrim(row.amount); + if (a.isEmpty()) throw new IllegalArgumentException(L10n.t(R.string.msauth_row_no_address, "Enter the address of every payment.")); + if (v.isEmpty()) { + if (amountOptional) return a; + throw new IllegalArgumentException(L10n.t(R.string.msauth_row_no_amount, "Enter the amount of every payment: it is what stops a split or an inflated payment.")); + } + BigInteger units = Amounts.parse(v); + if (units == null || units.signum() <= 0) { + throw new IllegalArgumentException(L10n.t(R.string.msauth_row_bad_amount, "An amount is a number of RAND, such as 1.5.")); + } + return a + ":" + v; + } + + /** Every row as its entry, each with its own {@link #amountOptional}; the first refusal throws. */ + public static List entries(List rows, JSONObject summary) { + List out = new ArrayList<>(); + for (Row r : rows) out.add(entry(r, amountOptional(r, summary))); + return out; + } + + /** + * Whether this row may go without an amount: only when the check's {@code expected} shows the + * row's address owns a paying non-RAND output and no paying RAND output (a payee also paid RAND + * keeps its RAND amount pinned). A row the core has not fingerprinted yet (or whose address was + * edited) may be sent address-only when some paying output is non-RAND, so the next check can + * tell; the rule ({@link SignFlow#judge}) refuses it unless its fingerprint owns only such outputs. + */ + public static boolean amountOptional(Row row, JSONObject summary) { + if (summary == null) return false; + List paying = paying(summary); + boolean anyNonRand = false; + for (JSONObject o : paying) if (isNonRand(o)) anyNonRand = true; + Set owners = new HashSet<>(); + Set randOwners = new HashSet<>(); + SignFlow.payeeAssets(paying, owners, randOwners); + JSONObject known = knownEntry(row, summary); + if (known != null) { + Object fp = known.opt("fingerprint"); + return fp instanceof String && owners.contains(fp) && !randOwners.contains(fp); + } + return anyNonRand; + } + + /** + * Whether the check showed this row's payee paid both RAND and a non-RAND asset: such a payee + * needs separate proposals, one per kind. + */ + public static boolean mixedPayee(Row row, JSONObject summary) { + if (summary == null) return false; + JSONObject known = knownEntry(row, summary); + Object fp = known == null ? null : known.opt("fingerprint"); + if (!(fp instanceof String) || ((String) fp).isEmpty()) return false; + Set owners = new HashSet<>(); + Set randOwners = new HashSet<>(); + SignFlow.payeeAssets(paying(summary), owners, randOwners); + return owners.contains(fp) && randOwners.contains(fp); + } + + /** The check's {@code expected} item whose entry's address is this row's, or null. */ + private static JSONObject knownEntry(Row row, JSONObject summary) { + String a = SignFlow.jsTrim(row.addr); + JSONArray expected = summary.optJSONArray("expected"); + if (a.isEmpty() || expected == null) return null; + for (int i = 0; i < expected.length(); i++) { + Object item = expected.opt(i); + if (!(item instanceof JSONObject)) continue; + Object entry = ((JSONObject) item).opt("entry"); + if (!(entry instanceof String)) continue; + String e = (String) entry; + int colon = e.indexOf(':'); + if (SignFlow.jsTrim(colon < 0 ? e : e.substring(0, colon)).equals(a)) return (JSONObject) item; + } + return null; + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/ui/SignersActivity.java b/android/app/src/main/java/org/randprotocol/wallet/ui/SignersActivity.java new file mode 100644 index 0000000..05155f2 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/ui/SignersActivity.java @@ -0,0 +1,479 @@ +package org.randprotocol.wallet.ui; + +import android.app.KeyguardManager; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricManager; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; + +import org.json.JSONObject; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.databinding.ActivitySignersBinding; +import org.randprotocol.wallet.databinding.ItemSignerBinding; +import org.randprotocol.wallet.security.KeystoreSeedCipher; +import org.randprotocol.wallet.security.SignerProfiles; +import org.randprotocol.wallet.util.L10n; +import org.randprotocol.wallet.wallet.MsauthCore; +import org.randprotocol.wallet.wallet.NativeMsauthCore; +import org.randprotocol.wallet.wallet.SignFlow; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.crypto.Cipher; + +/** + * Settings › Multisig signers (spec 2026-10-11-msauth-signing-design.md §4, iOS SignersView): the + * saved signer profiles, each opening its sign screen, and a form that adds one from the policy and + * the signer key (files or a paste). The seed is sealed by the profile's own Keystore key, which + * opens only right after the person authenticates; saving it asks once already. + */ +public class SignersActivity extends BaseActivity { + private ActivitySignersBinding b; + private final ExecutorService work = Executors.newSingleThreadExecutor(); + private final MsauthCore core = NativeMsauthCore.INSTANCE; + private ActivityResultLauncher openPolicy, openKey; + /** What the core read from the form, for Save; null until Check, and after any edit. */ + private Preview preview; + /** One add at a time (the prompt is up, or the seed is being sealed). */ + private boolean saving; + private int listRun; + /** The profile whose removal waits on the confirm-credential screen. */ + private SignerProfiles.Profile pendingRemove; + private ActivityResultLauncher confirmCredential; + + /** What Check found: shown for comparison, and the texts it was found in (Save re-reads them). */ + private static final class Preview { + final String policy, key, fingerprint, address, policyHash; + final int threshold, signers, index; + + Preview(String policy, String key, JSONObject info, int index) { + this.policy = policy; + this.key = key; + this.fingerprint = info.optString("fingerprint"); + this.address = info.optString("address"); + this.policyHash = info.optString("policy_hash"); + this.threshold = info.optInt("threshold"); + this.signers = info.optInt("signers"); + this.index = index; + } + } + + private SignerProfiles profiles() { + return wallet().vault().signerProfiles(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + b = ActivitySignersBinding.inflate(getLayoutInflater()); + setContentView(b.getRoot()); + + openPolicy = registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> picked(uri, b.policy)); + openKey = registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> picked(uri, b.key)); + b.openPolicy.setOnClickListener(v -> openPolicy.launch(new String[] {"application/json", "text/plain"})); + confirmCredential = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), r -> { + SignerProfiles.Profile p = pendingRemove; + pendingRemove = null; + if (p != null && r.getResultCode() == RESULT_OK) remove(p); + }); + b.openKey.setOnClickListener(v -> openKey.launch(new String[] {"application/json", "text/plain"})); + + TextWatcher dropPreview = new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int st, int c, int a) {} + + @Override public void onTextChanged(CharSequence s, int st, int bf, int c) {} + + @Override public void afterTextChanged(Editable s) { + preview = null; + b.preview.setVisibility(View.GONE); + } + }; + b.label.addTextChangedListener(dropPreview); + b.policy.addTextChangedListener(dropPreview); + b.key.addTextChangedListener(dropPreview); + b.check.setOnClickListener(v -> check()); + b.save.setOnClickListener(v -> save()); + } + + @Override + protected void onResume() { + super.onResume(); + paintList(); + } + + @Override + protected void onDestroy() { + work.shutdownNow(); + // The key text is a secret: it does not outlive the screen. + if (b != null) b.key.setText(""); + preview = null; + super.onDestroy(); + } + + // ------------------------------------------------------------------ the list + + private void paintList() { + int run = ++listRun; + work.execute(() -> { + List list; + List infos = new ArrayList<>(); + String error = null; + try { + SignerProfiles store = profiles(); + list = store.list(); + for (SignerProfiles.Profile p : list) { + JSONObject info; + try { + info = core.msauthPolicyInfo(store.policy(p.id)); + } catch (Exception e) { + info = new JSONObject(); + } + infos.add(info); + } + } catch (Exception e) { + list = new ArrayList<>(); + error = CoreErrors.of(e); + } + List rows = list; + String err = error; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed() || run != listRun) return; + b.list.removeAllViews(); + b.empty.setVisibility(rows.isEmpty() && err == null ? View.VISIBLE : View.GONE); + show(b.listError, err); + LayoutInflater inf = getLayoutInflater(); + for (int i = 0; i < rows.size(); i++) { + SignerProfiles.Profile p = rows.get(i); + JSONObject info = infos.get(i); + ItemSignerBinding row = ItemSignerBinding.inflate(inf, b.list, false); + row.label.setText(p.label); + row.fingerprint.setText(info.optString("fingerprint")); + row.position.setText(position(info.optInt("threshold"), info.optInt("signers"), p.index)); + row.open.setOnClickListener(v -> startActivity(new Intent(this, SignProposalActivity.class).putExtra(SignProposalActivity.EXTRA_ID, p.id))); + row.remove.setOnClickListener(v -> confirmRemove(p)); + b.list.addView(row.getRoot()); + } + }); + }); + } + + static String position(int threshold, int signers, int index) { + return L10n.t(R.string.msauth_profile_position, "%1$s of %2$s · signer %3$s", String.valueOf(threshold), String.valueOf(signers), String.valueOf(index)); + } + + /** "Remove Treasury?" naming the label and that the key file is needed to add it back; then the device's authentication. */ + private void confirmRemove(SignerProfiles.Profile p) { + new AlertDialog.Builder(this) + .setTitle(getString(R.string.msauth_remove_title, p.label)) + .setMessage(R.string.msauth_remove_body) + .setPositiveButton(R.string.msauth_remove_signer, (d, w) -> authenticateThenRemove(p)) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + /** + * The device's authentication, then the removal ({@link RemoveAuth}): skipped only when the + * device has no secure lock at all; otherwise BiometricPrompt where it can ask, else the + * system's confirm-credential screen. Any other failure removes nothing. + */ + private void authenticateThenRemove(SignerProfiles.Profile p) { + int sdk = Build.VERSION.SDK_INT; + KeyguardManager km = getSystemService(KeyguardManager.class); + boolean secure = km != null && km.isDeviceSecure(); + RemoveAuth how = RemoveAuth.choose(sdk, secure, BiometricManager.from(this).canAuthenticate(RemoveAuth.authenticators(sdk))); + switch (how.kind) { + case NONE: + // No screen lock at all: there is nothing to ask, and its signer keys no longer open anyway. + remove(p); + return; + case CONFIRM_CREDENTIAL: + Intent confirm = km == null ? null : km.createConfirmDeviceCredentialIntent(getString(R.string.msauth_remove_reason), null); + if (confirm == null) { + show(b.listError, KeystoreSeedCipher.refusal(sdk, false, false)); + return; + } + pendingRemove = p; + confirmCredential.launch(confirm); + return; + default: + break; + } + BiometricPrompt prompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + remove(p); + } + + @Override + public void onAuthenticationError(int code, @NonNull CharSequence message) { + if (code != BiometricPrompt.ERROR_USER_CANCELED && code != BiometricPrompt.ERROR_NEGATIVE_BUTTON && code != BiometricPrompt.ERROR_CANCELED) { + show(b.listError, String.valueOf(message)); + } + } + }); + BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.msauth_remove_reason)) + .setAllowedAuthenticators(how.authenticators); + if (how.negativeButton) info.setNegativeButtonText(getString(R.string.cancel)); + prompt.authenticate(info.build()); + } + + private void remove(SignerProfiles.Profile p) { + show(b.listError, null); + if (isDestroyed() || work.isShutdown()) return; + work.execute(() -> { + String err = null; + try { + profiles().remove(p.id); + } catch (Exception e) { + err = CoreErrors.of(e); + } + String m = err; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + if (m != null) show(b.listError, m); + paintList(); + }); + }); + } + + // ------------------------------------------------------------------ adding one + + private void picked(Uri uri, EditText into) { + if (uri == null) return; + work.execute(() -> { + String text = null, err = null; + try (InputStream in = getContentResolver().openInputStream(uri)) { + text = PickedFile.read(sizeOf(uri), in); + } catch (IllegalArgumentException e) { + err = e.getMessage(); + } catch (Exception e) { + err = PickedFile.unreadable(); + } + String t = text, m = err; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + if (t != null) into.setText(t); + show(b.addError, m); + }); + }); + } + + /** The provider's size of a picked document, -1 when it does not say. */ + long sizeOf(Uri uri) { + try (Cursor c = getContentResolver().query(uri, new String[] {OpenableColumns.SIZE}, null, null, null)) { + if (c != null && c.moveToFirst() && !c.isNull(0)) return c.getLong(0); + } catch (Exception ignored) { + // unknown: the read itself stops past the cap + } + return -1; + } + + private void check() { + show(b.addError, null); + preview = null; + b.preview.setVisibility(View.GONE); + String policy = SignFlow.clean(String.valueOf(b.policy.getText())); + String key = SignFlow.clean(String.valueOf(b.key.getText())); + if (policy.isEmpty() || key.isEmpty()) return; + b.check.setEnabled(false); + work.execute(() -> { + Preview p = null; + String err = null; + try { + JSONObject info = core.msauthPolicyInfo(policy); + JSONObject signer = core.msauthSignerImport(key, policy); + Object index = signer.opt("index"); + if (!(index instanceof Number)) throw new IllegalStateException(L10n.t(R.string.core_err_msauth_not_signer, "this key is not one of the policy's signers")); + p = new Preview(policy, key, info, ((Number) index).intValue()); + // The core's reply carries the seed; it is not kept (Save reads it again after the prompt). + signer.remove("seed"); + } catch (Exception e) { + err = CoreErrors.of(e); + } + Preview found = p; + String m = err; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + b.check.setEnabled(true); + // The form changed while the core read it: that answer is not about these texts. + if (found != null && (!found.policy.equals(SignFlow.clean(String.valueOf(b.policy.getText()))) + || !found.key.equals(SignFlow.clean(String.valueOf(b.key.getText()))))) return; + show(b.addError, m); + preview = found; + if (found == null) return; + b.previewFp.setText(found.fingerprint); + b.previewAddress.setText(found.address); + b.previewPosition.setText(L10n.t(R.string.msauth_preview_position, "%1$s of %2$s · this key is signer %3$s", + String.valueOf(found.threshold), String.valueOf(found.signers), String.valueOf(found.index))); + b.preview.setVisibility(View.VISIBLE); + }); + }); + } + + /** + * Save: the device must be able to hold a per-use key ({@link KeystoreSeedCipher#refusal}); + * the label and "already saved" are checked first so the prompt is not wasted; then a fresh + * Keystore key, the prompt over its encrypt cipher, and on success the seed sealed by it. The + * key is deleted again on any failure. + */ + private void save() { + Preview p = preview; + if (p == null || saving) return; + show(b.addError, null); + String why = KeystoreSeedCipher.refusal(this); + if (why != null) { + show(b.addError, why); + return; + } + String label = String.valueOf(b.label.getText()).trim(); + int n = label.codePointCount(0, label.length()); + if (n < 1 || n > 64) { + show(b.addError, L10n.t(R.string.msauth_label_length, "a signer label is 1-64 characters")); + return; + } + saving = true; + b.save.setEnabled(false); + String id = SignerProfiles.newId(); + work.execute(() -> { + Cipher cipher = null; + String err = null; + try { + for (SignerProfiles.Profile q : profiles().list()) { + if (q.policyHash.equals(p.policyHash) && q.index == p.index) { + throw new IllegalStateException(L10n.t(R.string.msauth_already_saved, "this signer is already saved as %1$s", q.label)); + } + } + KeystoreSeedCipher.createKey(id); + cipher = KeystoreSeedCipher.encryptCipher(id); + } catch (Exception e) { + KeystoreSeedCipher.deleteKey(id); + err = CoreErrors.of(e); + } + Cipher c = cipher; + String m = err; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + KeystoreSeedCipher.deleteKey(id); + return; + } + if (c == null) { + done(m); + return; + } + promptToSeal(id, label, p, c); + }); + }); + } + + private void promptToSeal(String id, String label, Preview p, Cipher cipher) { + BiometricPrompt prompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + BiometricPrompt.CryptoObject co = result.getCryptoObject(); + Cipher authed = co == null ? null : co.getCipher(); + if (authed == null) { + KeystoreSeedCipher.deleteKey(id); + done(null); + return; + } + if (isFinishing() || isDestroyed() || work.isShutdown()) { + KeystoreSeedCipher.deleteKey(id); + return; + } + work.execute(() -> seal(id, label, p, authed)); + } + + @Override + public void onAuthenticationError(int code, @NonNull CharSequence message) { + KeystoreSeedCipher.deleteKey(id); + boolean dismissed = code == BiometricPrompt.ERROR_USER_CANCELED || code == BiometricPrompt.ERROR_NEGATIVE_BUTTON || code == BiometricPrompt.ERROR_CANCELED; + done(dismissed ? null : String.valueOf(message)); + } + }); + prompt.authenticate(CryptoPrompt.info(getString(R.string.msauth_save_reason), getString(R.string.cancel)), new BiometricPrompt.CryptoObject(cipher)); + } + + /** Off the main thread: the seed read again from the key text, sealed, saved; zeroed whatever happens. */ + private void seal(String id, String label, Preview p, Cipher cipher) { + byte[] seed = null; + String err = null; + boolean ok = false; + try { + JSONObject signer = core.msauthSignerImport(p.key, p.policy); + seed = hex(signer.optString("seed")); + signer.remove("seed"); + byte[] ct = cipher.doFinal(seed); + profiles().add(new SignerProfiles.Profile(id, label, p.index, p.policyHash), p.policy, KeystoreSeedCipher.seal(cipher.getIV(), ct)); + ok = true; + } catch (Exception e) { + err = CoreErrors.of(e); + } finally { + if (seed != null) Arrays.fill(seed, (byte) 0); + if (!ok) KeystoreSeedCipher.deleteKey(id); + } + String m = err; + boolean saved = ok; + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) return; + if (saved) { + b.label.setText(""); + b.policy.setText(""); + b.key.setText(""); + preview = null; + b.preview.setVisibility(View.GONE); + paintList(); + } + done(m); + }); + } + + private void done(String error) { + saving = false; + b.save.setEnabled(true); + show(b.addError, error); + } + + /** 64 hex characters to 32 bytes; IllegalStateException for anything else. */ + static byte[] hex(String s) { + if (s == null || s.length() != 64) throw new IllegalStateException(L10n.t(R.string.core_err_msauth_seed_shape, "a signer seed is 64 hex characters")); + byte[] out = new byte[32]; + for (int i = 0; i < 32; i++) { + int hi = Character.digit(s.charAt(2 * i), 16), lo = Character.digit(s.charAt(2 * i + 1), 16); + if (hi < 0 || lo < 0) { + Arrays.fill(out, (byte) 0); + throw new IllegalStateException(L10n.t(R.string.core_err_msauth_seed_shape, "a signer seed is 64 hex characters")); + } + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } + + static void show(TextView v, String message) { + v.setText(message == null ? "" : message); + v.setVisibility(message == null || message.isEmpty() ? View.GONE : View.VISIBLE); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/wallet/MsauthCore.java b/android/app/src/main/java/org/randprotocol/wallet/wallet/MsauthCore.java new file mode 100644 index 0000000..1179ff2 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/wallet/MsauthCore.java @@ -0,0 +1,28 @@ +package org.randprotocol.wallet.wallet; + +import org.json.JSONObject; + +import java.util.List; + +/** + * The core methods private-multisig signing uses (wallet-core msauth.rs, documented on {@code + * wallet_core::dispatch}): {@link org.randprotocol.wallet.core.Core}'s five {@code msauth*} + * wrappers. An interface so the JVM unit tests, which cannot load the native core, can stand in for + * it ({@link ProverCore} does the same); {@link NativeMsauthCore#INSTANCE} is the real one. + */ +public interface MsauthCore { + /** {@code msauth_check}: fullnode's summary plus what wallet-core adds; {@link SignFlow} judges it. */ + JSONObject msauthCheck(String proposal, String policy, List expectedRecipients) throws Exception; + + /** {@code msauth_sign}: the share {@code "i:hex"}, with the same list the check was given. */ + String msauthSign(String proposal, String policy, String seedHex, List expectedRecipients) throws Exception; + + /** {@code msauth_verify_share}: throws the core's refusal when the share does not verify. */ + void msauthVerifyShare(String proposal, String policy, String share) throws Exception; + + /** {@code msauth_signer_import}: {@code {seed, public_key, address, index}}. */ + JSONObject msauthSignerImport(String input, String policy) throws Exception; + + /** {@code msauth_policy_info}: {@code {fingerprint, threshold, signers, address, policy_hash}}. */ + JSONObject msauthPolicyInfo(String policy) throws Exception; +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/wallet/NativeMsauthCore.java b/android/app/src/main/java/org/randprotocol/wallet/wallet/NativeMsauthCore.java new file mode 100644 index 0000000..63d379f --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/wallet/NativeMsauthCore.java @@ -0,0 +1,39 @@ +package org.randprotocol.wallet.wallet; + +import org.json.JSONObject; +import org.randprotocol.wallet.core.Core; +import org.randprotocol.wallet.core.CoreException; + +import java.util.List; + +/** {@link MsauthCore} over the native core: each method is {@link Core}'s wrapper of the same name. */ +public final class NativeMsauthCore implements MsauthCore { + public static final NativeMsauthCore INSTANCE = new NativeMsauthCore(); + + private NativeMsauthCore() {} + + @Override + public JSONObject msauthCheck(String proposal, String policy, List expectedRecipients) throws CoreException { + return Core.msauthCheck(proposal, policy, expectedRecipients); + } + + @Override + public String msauthSign(String proposal, String policy, String seedHex, List expectedRecipients) throws CoreException { + return Core.msauthSign(proposal, policy, seedHex, expectedRecipients); + } + + @Override + public void msauthVerifyShare(String proposal, String policy, String share) throws CoreException { + Core.msauthVerifyShare(proposal, policy, share); + } + + @Override + public JSONObject msauthSignerImport(String input, String policy) throws CoreException { + return Core.msauthSignerImport(input, policy); + } + + @Override + public JSONObject msauthPolicyInfo(String policy) throws CoreException { + return Core.msauthPolicyInfo(policy); + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/wallet/SignFlow.java b/android/app/src/main/java/org/randprotocol/wallet/wallet/SignFlow.java new file mode 100644 index 0000000..ab51da7 --- /dev/null +++ b/android/app/src/main/java/org/randprotocol/wallet/wallet/SignFlow.java @@ -0,0 +1,541 @@ +package org.randprotocol.wallet.wallet; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.randprotocol.wallet.R; +import org.randprotocol.wallet.rpc.RpcClient; +import org.randprotocol.wallet.security.SignerProfiles; +import org.randprotocol.wallet.util.L10n; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.CancellationException; + +/** + * Private multisig signing (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md §3, §4): + * the rules every app shares ({@link #judge}, the same decisions in the same order as + * ui/engine/signers.js {@code verdict} and iOS {@code MsauthRules.verdict}, iOS's where it is the + * stricter) and the order a signature is made in ({@link #sign}, iOS {@code MsauthSigning}). + * + *

The rules are pure and JVM-tested. A field the rule reads that is missing or of the wrong JSON + * type refuses ({@link Verdict#MALFORMED_SUMMARY}): nothing here is a reason to sign. + */ +public final class SignFlow { + /** Blocks a proposal needs before its window closes: the CLI's {@code EXPIRY_MARGIN} (fullnode d5b2e652). */ + public static final long EXPIRY_MARGIN = 16; + /** JavaScript's {@code Number.MAX_SAFE_INTEGER}, the bound the web's rule applies to every number it reads. */ + public static final long MAX_SAFE_INTEGER = 9_007_199_254_740_991L; + + public enum Verdict { SIGNABLE, MALFORMED_SUMMARY, UNCHECKED_RECIPIENT, NO_NODE, WRONG_CHAIN, WRONG_GENESIS, EXPIRED } + + /** A verdict and what its sentence names. */ + public static final class Judgement { + public final Verdict verdict; + /** The node's head, null when no node answered. */ + public final Long head; + public final long expiresAt; + public final long proposalChain; + public final long walletChain; + /** The node asked ({@code ""} when none is configured). */ + public final String url; + /** {@code expires_at - head} when signable, else 0. */ + public final long blocksLeft; + + private Judgement(Verdict verdict, Long head, long expiresAt, long proposalChain, long walletChain, String url) { + this.verdict = verdict; + this.head = head; + this.expiresAt = expiresAt; + this.proposalChain = proposalChain; + this.walletChain = walletChain; + this.url = url == null ? "" : url; + this.blocksLeft = verdict == Verdict.SIGNABLE ? expiresAt - head : 0; + } + + static Judgement of(Verdict v, String url) { + return new Judgement(v, null, 0, 0, 0, url); + } + + public boolean signable() { + return verdict == Verdict.SIGNABLE; + } + + /** The refusal in the user's language (the web's English); null when signable. */ + public String message() { + switch (verdict) { + case MALFORMED_SUMMARY: + return L10n.t(R.string.msauth_refuse_malformed, "The proposal check returned an incomplete answer, so this proposal is not signed."); + case UNCHECKED_RECIPIENT: + return L10n.t(R.string.msauth_refuse_unchecked, "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed."); + case NO_NODE: + return L10n.t(R.string.msauth_refuse_no_node, "No node answered at %1$s, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.", + url.isEmpty() ? "—" : url); + case WRONG_CHAIN: + return L10n.t(R.string.msauth_refuse_wrong_chain, "This proposal is for chain %1$s; this wallet is on chain %2$s.", + String.valueOf(proposalChain), String.valueOf(walletChain)); + case WRONG_GENESIS: + return L10n.t(R.string.msauth_refuse_wrong_genesis, "This proposal is bound to another genesis than the one this wallet's node serves."); + case EXPIRED: + return L10n.t(R.string.msauth_refuse_expired, "This proposal expires at height %1$s and the chain is at %2$s: too late to sign. Ask for a new proposal.", + String.valueOf(expiresAt), String.valueOf(head)); + default: + return null; + } + } + } + + /** A proposal that may not be signed: the judgement that refused it. */ + public static final class Refusal extends Exception { + public final Judgement judgement; + + Refusal(Judgement judgement) { + super(judgement.message()); + this.judgement = judgement; + } + } + + // ------------------------------------------------------------------ the rule + + /** {@link #judge}'s verdict alone. */ + public static Verdict verdict(Object summary, Long head, long chainId, String genesis, List recipients) { + return judge(summary, head, chainId, genesis, "", recipients).verdict; + } + + /** + * Whether a checked proposal may be signed now, on this wallet's chain. {@code summary} is + * {@code msauth_check}'s reply, {@code recipients} the list the check was given ({@link + * #payees}), {@code head} null when no node answered, {@code url} the node asked. + */ + public static Judgement judge(Object summary, Long head, long chainId, String genesis, String url, List recipients) { + if (!(summary instanceof JSONObject)) return Judgement.of(Verdict.MALFORMED_SUMMARY, url); + JSONObject s = (JSONObject) summary; + Judgement unchecked = Judgement.of(Verdict.UNCHECKED_RECIPIENT, url); + Judgement malformed = Judgement.of(Verdict.MALFORMED_SUMMARY, url); + // Never sign on an unchecked payee (a coordinator could pay its own key). + if (!Boolean.TRUE.equals(strictBool(s.opt("recipients_checked")))) return unchecked; + List rec = recipients == null ? Collections.emptyList() : recipients; + for (String r : rec) if (isBlank(r)) return unchecked; + // The review shows `lines` verbatim: missing, or anything but an array of strings, is malformed. + Object linesV = s.opt("lines"); + if (!(linesV instanceof JSONArray)) return malformed; + for (int i = 0; i < ((JSONArray) linesV).length(); i++) if (!(((JSONArray) linesV).opt(i) instanceof String)) return malformed; + Object outsV = s.opt("outputs"); + if (!(outsV instanceof JSONArray)) return malformed; + JSONArray outs = (JSONArray) outsV; + List paying = new ArrayList<>(); + for (int i = 0; i < outs.length(); i++) { + Object item = outs.opt(i); + if (!(item instanceof JSONObject)) return malformed; + JSONObject o = (JSONObject) item; + Boolean toAccount = strictBool(o.opt("to_account")); + Object amount = o.opt("amount"); + if (toAccount == null || !amountOk(amount)) return malformed; + if (!toAccount && !isZeroAmount(amount)) paying.add(o); + } + // No intended payment named at all while something pays out: unchecked, whatever the core said. + if (rec.isEmpty() && !paying.isEmpty()) return unchecked; + // An amount is pinned on every entry, except an entry whose own owner fingerprint owns a + // paying non-RAND output and NO paying RAND output (the core refuses a pin on a non-RAND + // output). Per row: one small non-RAND output cannot unpin the RAND rows, nor the RAND paid + // to its own payee (1 unit of a token beside an inflated RAND output is refused). + List amountless = new ArrayList<>(); + for (String r : rec) if (isAmountless(r)) amountless.add(r); + if (!amountless.isEmpty()) { + Object expV = s.opt("expected"); + if (!(expV instanceof JSONArray)) return unchecked; + JSONArray exp = (JSONArray) expV; + Set tokenOwners = new HashSet<>(); + Set randOwners = new HashSet<>(); + payeeAssets(paying, tokenOwners, randOwners); + for (String entry : amountless) { + boolean allowed = false; + for (int i = 0; i < exp.length() && !allowed; i++) { + Object item = exp.opt(i); + if (!(item instanceof JSONObject)) continue; + JSONObject x = (JSONObject) item; + Object e = x.opt("entry"); + Object fp = x.opt("fingerprint"); + allowed = e instanceof String && e.equals(entry) && fp instanceof String && !((String) fp).isEmpty() && tokenOwners.contains(fp) && !randOwners.contains(fp); + } + if (!allowed) return unchecked; + } + } + for (JSONObject o : paying) { + Object m = o.opt("matches"); + if (!(m instanceof String) || jsTrim((String) m).isEmpty()) return unchecked; + } + Long expiresAt = uint(s.opt("expires_at")); + Long proposalChain = uint(s.opt("chain_id")); + Long domain = uint(s.opt("binding_domain")); + Object g = s.opt("genesis"); + if (expiresAt == null || proposalChain == null || domain == null || !(g instanceof String) || !isHex((String) g)) return malformed; + if (head == null || head < 0 || head > MAX_SAFE_INTEGER) return Judgement.of(Verdict.NO_NODE, url); + if (proposalChain != chainId) return new Judgement(Verdict.WRONG_CHAIN, head, expiresAt, proposalChain, chainId, url); + String ours = genesis == null ? "" : genesis.toLowerCase(Locale.ROOT); + if (domain >= 1 && !((String) g).toLowerCase(Locale.ROOT).equals(ours)) { + return new Judgement(Verdict.WRONG_GENESIS, head, expiresAt, proposalChain, chainId, url); + } + // head + 16 >= expires_at, written so it cannot overflow. + if (head >= expiresAt || expiresAt - head <= EXPIRY_MARGIN) { + return new Judgement(Verdict.EXPIRED, head, expiresAt, proposalChain, chainId, url); + } + return new Judgement(Verdict.SIGNABLE, head, expiresAt, proposalChain, chainId, url); + } + + /** + * Of the paying outputs, the owner fingerprints paid a non-RAND asset ({@code token}) and those + * paid RAND ({@code rand}). An output whose asset is missing or unreadable counts as RAND. + */ + public static void payeeAssets(List paying, Set token, Set rand) { + for (JSONObject o : paying) { + Object fp = o.opt("owner_fingerprint"); + if (!(fp instanceof String) || ((String) fp).isEmpty()) continue; + Long asset = assetId(o.opt("asset")); + if (asset != null && asset != 0) token.add((String) fp); else rand.add((String) fp); + } + } + + /** The fingerprint's last four characters, typed: case, dashes and every whitespace ignored (as JS). */ + public static boolean confirms(String fingerprint, String typed) { + String fp = fingerprint == null ? "" : fingerprint.replace("-", ""); + if (fp.length() < 4 || typed == null) return false; + String want = fp.substring(fp.length() - 4).toLowerCase(Locale.ROOT); + StringBuilder got = new StringBuilder(); + for (int i = 0; i < typed.length(); i++) { + char c = typed.charAt(i); + if (c != '-' && !jsSpace(c)) got.append(c); + } + return got.toString().toLowerCase(Locale.ROOT).equals(want); + } + + /** A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. */ + public static String clean(String s) { + String t = s == null ? "" : s; + if (t.startsWith("\uFEFF")) t = t.substring(1); + return jsTrim(t); + } + + /** The intended payments as the core is given them: each entry cleaned, none dropped. */ + public static List payees(List list) { + List out = new ArrayList<>(); + if (list != null) for (String e : list) out.add(clean(e)); + return out; + } + + /** An entry that is empty or has no address before its amount. */ + public static boolean isBlank(String entry) { + String t = clean(entry); + return t.isEmpty() || t.startsWith(":"); + } + + /** An entry without an amount: no {@code :}, or nothing after the first one. */ + public static boolean isAmountless(String entry) { + int i = entry.indexOf(':'); + return i < 0 || jsTrim(entry.substring(i + 1)).isEmpty(); + } + + /** fullnode's lines, verbatim — never translated. */ + public static List lines(JSONObject s) { + List out = new ArrayList<>(); + JSONArray a = s == null ? null : s.optJSONArray("lines"); + for (int i = 0; a != null && i < a.length(); i++) { + Object l = a.opt(i); + if (l instanceof String) out.add((String) l); + } + return out; + } + + // ---- reading the core's JSON strictly: a Boolean is a Boolean, a number a number + + private static Boolean strictBool(Object v) { + return v instanceof Boolean ? (Boolean) v : null; + } + + /** A non-negative safe integer: a JSON number (not a Boolean, not text), integral, 0..2^53-1. */ + static Long uint(Object v) { + if (!(v instanceof Number)) return null; + BigInteger b; + if (v instanceof Integer || v instanceof Long || v instanceof Short || v instanceof Byte) { + b = BigInteger.valueOf(((Number) v).longValue()); + } else if (v instanceof BigInteger) { + b = (BigInteger) v; + } else if (v instanceof BigDecimal) { + try { + b = ((BigDecimal) v).toBigIntegerExact(); + } catch (ArithmeticException e) { + return null; + } + } else if (v instanceof Double || v instanceof Float) { + double d = ((Number) v).doubleValue(); + if (Double.isNaN(d) || Double.isInfinite(d) || d != Math.rint(d) || d < 0 || d > MAX_SAFE_INTEGER) return null; + return (long) d; + } else { + return null; + } + if (b.signum() < 0 || b.compareTo(BigInteger.valueOf(MAX_SAFE_INTEGER)) > 0) return null; + return b.longValue(); + } + + private static boolean digits(String t) { + if (t.isEmpty()) return false; + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + if (c < '0' || c > '9') return false; + } + return true; + } + + /** An amount: ASCII digits (whitespace around allowed), or a non-negative safe integer. */ + static boolean amountOk(Object v) { + if (v instanceof String) return digits(jsTrim((String) v)); + return uint(v) != null; + } + + /** Zero: {@code /^0+$/} after trimming, or the number 0. */ + public static boolean isZeroAmount(Object v) { + if (v instanceof String) { + String t = jsTrim((String) v); + return digits(t) && t.chars().allMatch(c -> c == '0'); + } + Long u = uint(v); + return u != null && u == 0; + } + + /** An asset id as the web reads it ({@code Number(o.asset)}): a number or a digit string; null otherwise. */ + public static Long assetId(Object v) { + if (v instanceof String) { + String t = jsTrim((String) v); + if (!digits(t)) return null; + BigInteger b = new BigInteger(t); + return b.compareTo(BigInteger.valueOf(MAX_SAFE_INTEGER)) > 0 ? null : b.longValue(); + } + return uint(v); + } + + static boolean isHex(String s) { + if (s.isEmpty()) return false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return false; + } + return true; + } + + /** JavaScript's {@code String.prototype.trim}: white space and line terminators, the BOM included. */ + public static String jsTrim(String s) { + int a = 0, b = s.length(); + while (a < b && jsSpace(s.charAt(a))) a++; + while (b > a && jsSpace(s.charAt(b - 1))) b--; + return s.substring(a, b); + } + + private static boolean jsSpace(char c) { + if (c >= '\u001C' && c <= '\u001F') return false; // Java's whitespace, not JavaScript's + return Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\uFEFF'; + } + + // ------------------------------------------------------------------ the order a signature is made in + + /** What signing asks of the node this wallet uses ({@link #node(RpcClient)}). */ + public interface Node { + String url(); + + long chainId() throws Exception; + + long headHeight() throws Exception; + + String genesisHash() throws Exception; + } + + /** + * {@code rand_getHead}'s height, read strictly: a non-negative integer ≤ 2^53-1, never a default. + * Anything else (missing, null, text, a fraction) throws, which the flow judges as no node — a + * height read as 0 would pass both the node check and the expiry margin. + */ + public static long headHeightOf(Object head) { + Long h = head instanceof JSONObject ? uint(((JSONObject) head).opt("height")) : null; + if (h == null) throw new IllegalStateException("the node's head has no height"); + return h; + } + + /** {@code rand_chainId}'s reply, read strictly as {@link #headHeightOf} reads a height. */ + public static long chainIdOf(Object reply) { + Long c = uint(reply); + if (c == null) throw new IllegalStateException("the node's chain id is not a chain id"); + return c; + } + + public static Node node(RpcClient rpc) { + return new Node() { + @Override public String url() { + return rpc.url(); + } + + // Not rpc.chainId() / rpc.headHeight(): those truncate a fraction and read a missing height as 0. + @Override public long chainId() throws Exception { + return chainIdOf(rpc.call("rand_chainId", null)); + } + + @Override public long headHeight() throws Exception { + return headHeightOf(rpc.call("rand_getHead", null)); + } + + @Override public String genesisHash() throws Exception { + return rpc.genesisHash(); + } + }; + } + + /** + * One authenticated read of a profile's seed (the system's prompt, then the decrypt: + * {@link org.randprotocol.wallet.security.KeystoreSeedSource}). Returns a fresh buffer the flow + * zeroes; blocks while the prompt is up, so call {@link #sign} off the main thread. + */ + public interface SeedSource { + byte[] seed(String id) throws Exception; + } + + /** Whether the person left the screen: signing stops before the prompt and before the core signs. */ + public interface Cancel { + boolean cancelled(); + } + + public static final class Check { + /** null when the entries were refused before the core was asked. */ + public final JSONObject summary; + public final Judgement judgement; + + Check(JSONObject summary, Judgement judgement) { + this.summary = summary; + this.judgement = judgement; + } + } + + public static final class Share { + public final String share; + public final int index; + + Share(String share, int index) { + this.share = share; + this.index = index; + } + } + + private final SignerProfiles profiles; + private final MsauthCore core; + private final Node node; + private final long chainId; + private final SeedSource seeds; + + /** {@code node} null is "no node": a proposal is checked and shown, never signed. {@code chainId} is this wallet's (Settings). */ + public SignFlow(SignerProfiles profiles, MsauthCore core, Node node, long chainId, SeedSource seeds) { + this.profiles = profiles; + this.core = core; + this.node = node; + this.chainId = chainId; + this.seeds = seeds; + } + + private String policy(String id) { + String p = profiles.policy(id); + if (p == null) throw new IllegalArgumentException(L10n.t(R.string.msauth_no_profile, "No such signer profile.")); + return p; + } + + /** The head from a node on this wallet's chain; any failure, or a height past 2^53-1, is no node. */ + private Judgement judgeNow(JSONObject summary, List payees) { + if (node == null) return judge(summary, null, chainId, null, "", payees); + String url = ""; + try { + url = node.url(); + if (node.chainId() != chainId) return judge(summary, null, chainId, null, url, payees); + long head = node.headHeight(); + if (head < 0 || head > MAX_SAFE_INTEGER) return judge(summary, null, chainId, null, url, payees); + return judge(summary, head, chainId, node.genesisHash(), url, payees); + } catch (Exception e) { + return judge(summary, null, chainId, null, url, payees); + } + } + + private static void checkCancel(Cancel cancel) { + if ((cancel != null && cancel.cancelled()) || Thread.currentThread().isInterrupted()) throw new CancellationException(); + } + + private static boolean anyBlank(List payees) { + for (String p : payees) if (isBlank(p)) return true; + return false; + } + + /** The core's check with the intended payments, and the verdict on it now. A blank entry is refused before the core is asked. */ + public Check check(String id, String proposal, List expectedRecipients) throws Exception { + String policy = policy(id); + List payees = Collections.unmodifiableList(payees(expectedRecipients)); + if (anyBlank(payees)) return new Check(null, Judgement.of(Verdict.UNCHECKED_RECIPIENT, "")); + JSONObject summary = core.msauthCheck(clean(proposal), policy, payees); + return new Check(summary, judgeNow(summary, payees)); + } + + /** + * The share, or the refusal ({@link Refusal}, the core's error, or {@link CancellationException}). + * The order: check and judge; stop if cancelled; read the seed (the system asks for the + * fingerprint, face or screen lock); re-read the head and judge again, since the prompt can take + * as long as the person likes; stop if cancelled; sign with the same list the check used, so the + * core re-runs the payee check on exactly what was judged. The seed buffer is zeroed on every + * exit; its hex copy for the core is a String, which cannot be wiped (it is unreachable after the + * call, and the core holds its own copy in a wiped buffer). + */ + public Share sign(String id, String proposal, List expectedRecipients, Cancel cancel) throws Exception { + String policy = policy(id); + String text = clean(proposal); + List payees = Collections.unmodifiableList(payees(expectedRecipients)); + if (anyBlank(payees)) throw new Refusal(Judgement.of(Verdict.UNCHECKED_RECIPIENT, "")); + JSONObject summary = core.msauthCheck(text, policy, payees); + Judgement first = judgeNow(summary, payees); + if (!first.signable()) throw new Refusal(first); + checkCancel(cancel); + byte[] seed = seeds.seed(id); + try { + Judgement again = judgeNow(summary, payees); + if (!again.signable()) throw new Refusal(again); + checkCancel(cancel); + String share = core.msauthSign(text, policy, hex(seed), payees); + return new Share(share, shareIndex(share)); + } finally { + if (seed != null) Arrays.fill(seed, (byte) 0); + } + } + + /** Another signer's share checked over this proposal (throws the core's refusal); its position. */ + public int verifyShare(String id, String proposal, String share) throws Exception { + String s = clean(share); + core.msauthVerifyShare(clean(proposal), policy(id), s); + return shareIndex(s); + } + + private static int shareIndex(String share) { + int colon = share == null ? -1 : share.indexOf(':'); + if (colon > 0) { + String head = share.substring(0, colon).trim(); + if (digits(head) && head.length() <= 3) return Integer.parseInt(head); + } + throw new IllegalStateException("msauth_sign returned no share"); + } + + private static String hex(byte[] b) { + char[] out = new char[b.length * 2]; + final char[] digits = "0123456789abcdef".toCharArray(); + for (int i = 0; i < b.length; i++) { + out[2 * i] = digits[(b[i] >> 4) & 0xf]; + out[2 * i + 1] = digits[b[i] & 0xf]; + } + String s = new String(out); + Arrays.fill(out, '\0'); + return s; + } +} diff --git a/android/app/src/main/java/org/randprotocol/wallet/wallet/WalletService.java b/android/app/src/main/java/org/randprotocol/wallet/wallet/WalletService.java index 1c256c9..edfcf17 100644 --- a/android/app/src/main/java/org/randprotocol/wallet/wallet/WalletService.java +++ b/android/app/src/main/java/org/randprotocol/wallet/wallet/WalletService.java @@ -197,8 +197,9 @@ public JSONObject importWallet(String input) throws CoreException { return info; } + /** Throws IllegalStateException, with nothing removed, when the signer keys cannot all be removed ({@link KeyVault#erase}). */ public void removeWallet() { - vault.erase(); // the prover's token with it + vault.erase(); // the prover's token and the signer profiles with it; throws before anything else goes prefs.setProver(null); prefs.setNoProver(false); prefs.setProverNoticeFor(null); diff --git a/android/app/src/main/res/layout/activity_settings.xml b/android/app/src/main/res/layout/activity_settings.xml index 5cf8f79..75aacd0 100644 --- a/android/app/src/main/res/layout/activity_settings.xml +++ b/android/app/src/main/res/layout/activity_settings.xml @@ -51,6 +51,10 @@ + + + + diff --git a/android/app/src/main/res/layout/activity_sign_proposal.xml b/android/app/src/main/res/layout/activity_sign_proposal.xml new file mode 100644 index 0000000..fca3f59 --- /dev/null +++ b/android/app/src/main/res/layout/activity_sign_proposal.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_signers.xml b/android/app/src/main/res/layout/activity_signers.xml new file mode 100644 index 0000000..bc773f9 --- /dev/null +++ b/android/app/src/main/res/layout/activity_signers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_payment_row.xml b/android/app/src/main/res/layout/item_payment_row.xml new file mode 100644 index 0000000..5f20251 --- /dev/null +++ b/android/app/src/main/res/layout/item_payment_row.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_signer.xml b/android/app/src/main/res/layout/item_signer.xml new file mode 100644 index 0000000..0c7b56d --- /dev/null +++ b/android/app/src/main/res/layout/item_signer.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/values-ar/strings.xml b/android/app/src/main/res/values-ar/strings.xml index cb0e045..01396e3 100644 --- a/android/app/src/main/res/values-ar/strings.xml +++ b/android/app/src/main/res/values-ar/strings.xml @@ -489,4 +489,99 @@ يجري إثبات معاملة أخرى. حاول مجددًا عند انتهائها. تتعذّر قراءة هذه المبادلة. لم يُرسل شيء. %1$s: %2$s + + تجزئة المعاملة ليست ارتباط الاقتراح + قيمة signing_message في الاقتراح ليست قيمة ارتباطه + قيمة auth_commit في الاقتراح ليست حساب هذه السياسة + الملخّص (digest) المتوقع للاقتراح ليس ملخّص حزمته + لا تحمل المعاملة أي حزمة + تحمل المعاملة إجراءً؛ والاقتراح الخاص متعدد التوقيع هو دفعة من حزمته وحدها + لا يمكن التحقق من قيمة الشاهد (witness): قيمة nk أو الملح فيه ليست لهذا الاقتراح + هذا المفتاح ليس من موقّعي السياسة + حصة التوقيع بصيغة index:hex + مفتاح الموقّع هو ملف keygen من rand-node أو 64 حرفًا سداسيًا عشريًا + بذرة الموقّع هي 64 حرفًا سداسيًا عشريًا + يجب أن يكون expected_recipients قائمة عناوين + قيمة public_key في ملف المفتاح ليست التي تشتقها بذرته + عنوان ملف المفتاح ليس العنوان الذي تشتقه بذرته + لم يُحدَّد مستلم متوقع: لم يُتحقق من المستلم (NOT checked) + هذا الاقتراح لسياسة أخرى: يذكر %1$s، وهذه السياسة هي %2$s + المخرج %1$s لا يُفتح على أنه يخص هذا الحساب: رفض توقيع قيمة لا يمكن رؤيتها + قيمة الشاهد (witness) لا تطابق المعاملة: المدخل %1$s لا يُبطَل إلى nullifier الحزمة + قيمة الشاهد (witness) لا تطابق المخرجات: للأصل %1$s مدخل %2$s ومخرج %3$s مع الرسوم والحرق + ينتهي الاقتراح عند %1$s، خارج نافذة حزمته (%2$s, %3$s] + المعاملة للسلسلة %1$s، بينما يذكر الاقتراح السلسلة %2$s + توقيع الموقّع %1$s غير صالح لهذا الاقتراح + الموقّع %1$s ليس في هذه السياسة (فيها %2$s موقّعين) + المستلم المتوقع %1$s: %2$s + تعذّر تحليل السياسة: %1$s + تعذّر تحليل الاقتراح: %1$s + أعاد التحقق من الاقتراح ردًّا ناقصًا، لذا لن يُوقَّع هذا الاقتراح. + أدخل عنوان كل مستلم: لا يمكن توقيع مخرج لا يطابق العنوان الذي أدخلته أو عنوانه فارغ. + لم تردّ أي عقدة على %1$s، لذا لا يمكن التحقق من انتهاء الصلاحية ولن يُوقَّع هذا الاقتراح. أعد المحاولة عندما تصبح العقدة متاحة. + هذا الاقتراح للسلسلة %1$s؛ وهذه المحفظة على السلسلة %2$s. + هذا الاقتراح مرتبط بنشأة (genesis) مختلفة عن التي تخدمها عقدة هذه المحفظة. + ينتهي هذا الاقتراح عند الارتفاع %1$s والسلسلة عند %2$s: فات أوان التوقيع. اطلب اقتراحًا جديدًا. + لا يوجد ملف موقّع كهذا. + ملف الموقّع هذا لا مفتاح له على هذا الجهاز: أزله وأضفه من جديد. + تسمية الموقّع من 1-64 حرفًا + هذا الموقّع محفوظ مسبقًا باسم %1$s + عيّن قفل شاشة أولًا (وبصمة إصبع أيضًا على Android 10 أو أقدم): لا يُستخدم مفتاح الموقّع إلا مباشرةً بعد أن تثبت هويتك. + تعذّرت إزالة كل مفاتيح الموقّعين على هذا الجهاز، لذا أُبقيت المحفظة. حاول مرة أخرى. + موقّعو التوقيع المتعدد + إدارة موقّعي التوقيع المتعدد + لا توجد ملفات موقّعين بعد + أضف ملف السياسة ومفتاح الموقّع الخاص بك للتوقيع عن حساب متعدد التوقيع خاص. + %1$s من %2$s · الموقّع %3$s + إضافة موقّع + التسمية + ملف السياسة + ملف مفتاح الموقّع، أو بذرته من 64 حرفًا سداسيًا عشريًا + فتح ملف + تحقق + بصمة الحساب — قارنها مع كل موقّع آخر قبل أن تحفظ + %1$s من %2$s · هذا المفتاح هو الموقّع %3$s + حفظ الموقّع + يُحفظ ملف الموقّع في Android Keystore على هذا الهاتف. يطلب كل توقيع بصمتك أو وجهك أو قفل الشاشة من جديد. + إزالة هذا الموقّع + إزالة %1$s؟ + تحتاج إلى ملف مفتاح الموقّع لإضافته مرة أخرى. + إزالة موقّع متعدد التوقيع + حفظ موقّع متعدد التوقيع + لم يعد مفتاح هذا الموقّع يُفتح: تغيّرت بصمات الهاتف أو قفل الشاشة منذ حفظه. أزل الموقّع وأضفه من جديد من ملف مفتاحه. + توقيع اقتراح + الاقتراح (proposal.json) + التحقق من الاقتراح + جارٍ التحقق… + المدفوعات المقصودة + أدخل كل دفعة تنوي إجراءها: العنوان والمبلغ بعملة RAND. الدفعة إلى العنوان نفسه مرتين تُدخل مرة واحدة بالمجموع. لا يمكن توقيع مخرج لا يطابق أيًّا منها. + عنوان المستلم + المبلغ بعملة RAND + إزالة + إضافة دفعة + التحقق من المدفوعات + لا يمكن التحقق من المبالغ في المدفوعات بغير RAND: اترك المبلغ فارغًا لها. + يتلقى هذا المستفيد RAND ورمزًا معًا: مدفوعات RAND والرموز إلى مستفيد واحد تحتاج إلى مقترحات منفصلة. + أدخل عنوان كل دفعة. + أدخل مبلغ كل دفعة: فهو ما يمنع التجزئة أو تضخيم الدفعة. + المبلغ هو عدد من RAND، مثل 1.5. + ما يفعله هذا الاقتراح، مُتحقَّقًا منه مقابل سياستك + الرسوم: %1$s RAND · تنتهي عند الارتفاع %2$s + تطابقت %1$s من أصل %2$s من المدفوعات. + إعادة المحاولة + بقي %1$s كتلة لإثباته. + اكتب آخر أربعة أحرف من بصمة الحساب + تغيّرت المدفوعات: تحقق منها مجددًا قبل التوقيع. + توقيع + جارٍ التوقيع… + توقيع اقتراح متعدد التوقيع + حصة توقيعك — أرسلها إلى من يُثبت الاقتراح + نسخ + حصة التوقيع + حصة التوقيع المتعدد + التحقق من حصة توقيع + التحقق من حصة التوقيع + حصة توقيع الموقّع %1$s صحيحة. لديك %2$s من %3$s: الموقّعون %4$s. + هذا الملف أكبر مما يمكن أن يكون عليه اقتراح أو سياسة أو ملف مفتاح. + تعذّرت قراءة هذا الملف. diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index df7a465..8851737 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -457,4 +457,99 @@ Eine andere Transaktion wird gerade bewiesen. Versuche es erneut, wenn sie fertig ist. Dieser Tausch kann nicht gelesen werden. Es wurde nichts gesendet. %1$s: %2$s + + der Hash der Transaktion ist nicht die Bindung des Vorschlags + die signing_message des Vorschlags ist nicht die seiner Bindung + das auth_commit des Vorschlags ist nicht das Konto dieser Richtlinie + der erwartete Digest des Vorschlags ist nicht der seines Bundles + die Transaktion enthält kein Bundle + die Transaktion enthält eine Aktion; ein privater Multisig-Vorschlag ist allein eine Zahlung aus seinem Bundle + der Wert des Witness lässt sich nicht prüfen: sein nk oder Salt gehört nicht zu diesem Vorschlag + dieser Schlüssel gehört nicht zu den Unterzeichnern der Richtlinie + ein Signaturanteil ist index:hex + ein Unterzeichner-Schlüssel ist eine rand-node-Keygen-Datei oder 64 Hex-Zeichen + ein Unterzeichner-Seed besteht aus 64 Hex-Zeichen + expected_recipients muss eine Liste von Adressen sein + der public_key der Schlüsseldatei ist nicht der, den ihr Seed ableitet + die Adresse der Schlüsseldatei ist nicht die, die ihr Seed ableitet + kein erwarteter Empfänger angegeben: der Zahlungsempfänger wurde NICHT geprüft + dieser Vorschlag gehört zu einer anderen Richtlinie: er nennt %1$s, diese Richtlinie ist %2$s + Ausgabe %1$s lässt sich nicht als die dieses Kontos öffnen: Signatur eines Werts, den sie nicht sehen kann, wird verweigert + der Wert des Witness passt nicht zur Transaktion: Eingang %1$s ergibt nicht den Nullifier des Bundles + der Wert des Witness passt nicht zu den Ausgaben: Asset %1$s hat %2$s Eingang und %3$s Ausgang mit Gebühr und Burns + der Vorschlag läuft bei %1$s ab, außerhalb des Fensters seines Bundles (%2$s, %3$s] + die Transaktion gilt für Chain %1$s, der Vorschlag nennt Chain %2$s + die Signatur von Unterzeichner %1$s ist für diesen Vorschlag nicht gültig + Unterzeichner %1$s gehört nicht zu dieser Richtlinie (sie hat %2$s Unterzeichner) + erwarteter Empfänger %1$s: %2$s + die Richtlinie lässt sich nicht parsen: %1$s + der Vorschlag lässt sich nicht parsen: %1$s + Die Vorschlagsprüfung hat eine unvollständige Antwort geliefert, daher wird dieser Vorschlag nicht signiert. + Gib die Adresse jedes Zahlungsempfängers ein: Eine Ausgabe, die nicht zur eingegebenen Adresse passt oder deren Adresse leer ist, kann nicht signiert werden. + Unter %1$s hat kein Knoten geantwortet, daher lässt sich der Ablauf nicht prüfen und dieser Vorschlag wird nicht signiert. Versuche es erneut, wenn der Knoten erreichbar ist. + Dieser Vorschlag gilt für Chain %1$s; diese Wallet ist auf Chain %2$s. + Dieser Vorschlag ist an ein anderes Genesis gebunden als das, das der Knoten dieser Wallet bedient. + Dieser Vorschlag läuft bei Höhe %1$s ab und die Chain steht bei %2$s: zu spät zum Signieren. Fordere einen neuen Vorschlag an. + Dieses Unterzeichnerprofil gibt es nicht. + Dieses Unterzeichnerprofil hat auf diesem Gerät keinen Schlüssel: Entferne es und füge es erneut hinzu. + eine Unterzeichner-Bezeichnung hat 1-64 Zeichen + dieser Unterzeichner ist bereits als %1$s gespeichert + Richte zuerst eine Displaysperre ein (und unter Android 10 oder älter einen Fingerabdruck): Ein Unterzeichner-Schlüssel wird nur direkt nach deiner Authentifizierung verwendet. + Die Unterzeichner-Schlüssel auf diesem Gerät konnten nicht alle entfernt werden, daher wurde die Wallet behalten. Versuche es erneut. + Multisig-Unterzeichner + Multisig-Unterzeichner verwalten + Noch keine Unterzeichnerprofile + Füge die Richtliniendatei und deinen Unterzeichner-Schlüssel hinzu, um für ein privates Multisig-Konto zu signieren. + %1$s von %2$s · Unterzeichner %3$s + Unterzeichner hinzufügen + Bezeichnung + Richtliniendatei + Unterzeichner-Schlüsseldatei oder ihr Seed aus 64 Hex-Zeichen + Datei öffnen + Prüfen + Konto-Fingerabdruck — vergleiche ihn mit jedem anderen Unterzeichner, bevor du speicherst + %1$s von %2$s · dieser Schlüssel ist Unterzeichner %3$s + Unterzeichner speichern + Ein Unterzeichnerprofil wird im Android Keystore dieses Telefons gespeichert. Jede Signatur verlangt erneut deinen Fingerabdruck, dein Gesicht oder deine Displaysperre. + Diesen Unterzeichner entfernen + %1$s entfernen? + Du brauchst die Schlüsseldatei des Unterzeichners, um ihn wieder hinzuzufügen. + Einen Multisig-Unterzeichner entfernen + Einen Multisig-Unterzeichner speichern + Der Schlüssel dieses Unterzeichners lässt sich nicht mehr öffnen: Die Fingerabdrücke oder die Displaysperre des Telefons haben sich seit dem Speichern geändert. Entferne den Unterzeichner und füge ihn aus seiner Schlüsseldatei erneut hinzu. + Einen Vorschlag signieren + Vorschlag (proposal.json) + Vorschlag prüfen + Prüfe … + Vorgesehene Zahlungen + Gib jede Zahlung ein, die du leisten willst: die Adresse und den Betrag in RAND. Eine Zahlung, die zweimal an dieselbe Adresse geht, wird einmal mit der Summe eingetragen. Eine Ausgabe, die zu keiner davon passt, kann nicht signiert werden. + Adresse des Zahlungsempfängers + Betrag in RAND + Entfernen + Zahlung hinzufügen + Zahlungen prüfen + Beträge können bei Zahlungen, die nicht in RAND erfolgen, nicht geprüft werden: Lass den Betrag dort leer. + Dieser Empfänger erhält sowohl RAND als auch einen Token: RAND- und Token-Zahlungen an einen Empfänger brauchen getrennte Vorschläge. + Gib die Adresse jeder Zahlung ein. + Gib den Betrag jeder Zahlung ein: Er verhindert eine Aufteilung oder eine überhöhte Zahlung. + Ein Betrag ist eine Anzahl RAND, zum Beispiel 1.5. + Was dieser Vorschlag tut, geprüft gegen deine Richtlinie + Gebühr: %1$s RAND · läuft bei Höhe %2$s ab + %1$s von %2$s Zahlungen stimmen überein. + Erneut versuchen + Noch %1$s Blöcke, um es zu beweisen. + Gib die letzten vier Zeichen des Konto-Fingerabdrucks ein + Zahlungen geändert: Prüfe sie vor dem Signieren erneut. + Signieren + Signiere … + Einen Multisig-Vorschlag signieren + Dein Signaturanteil — schicke ihn an die Person, die den Vorschlag beweist + Kopieren + Signaturanteil + Multisig-Signaturanteil + Einen Signaturanteil prüfen + Signaturanteil prüfen + Der Signaturanteil von Unterzeichner %1$s ist gültig. %2$s von %3$s liegen vor: Unterzeichner %4$s. + Diese Datei ist größer, als ein Vorschlag, eine Richtlinie oder eine Schlüsseldatei sein kann. + Diese Datei konnte nicht gelesen werden. diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 9d41338..7b7d010 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -465,4 +465,99 @@ Se está generando la prueba de otra transacción. Inténtalo de nuevo cuando termine. No se puede leer este intercambio. No se envió nada. %1$s: %2$s + + el hash de la transacción no es la vinculación de la propuesta + el signing_message de la propuesta no es el de su vinculación + el auth_commit de la propuesta no es la cuenta de esta política + el digest esperado de la propuesta no es el de su bundle + la transacción no lleva ningún bundle + la transacción lleva una acción; una propuesta multifirma privada es un pago solo desde su bundle + no se puede comprobar el valor del witness: su nk o su salt no son los de esta propuesta + esta clave no es de ninguno de los firmantes de la política + una parte de firma es index:hex + una clave de firmante es un archivo keygen de rand-node o 64 caracteres hexadecimales + una semilla de firmante son 64 caracteres hexadecimales + expected_recipients debe ser una lista de direcciones + el public_key del archivo de clave no es el que deriva su semilla + la dirección del archivo de clave no es la que deriva su semilla + no se indicó ningún destinatario esperado: el beneficiario NO se comprobó + esta propuesta es de otra política: nombra %1$s, esta política es %2$s + la salida %1$s no se abre como de esta cuenta: se rechaza firmar un valor que no puede ver + el valor del witness no coincide con la transacción: la entrada %1$s no genera el nullifier del bundle + el valor del witness no coincide con las salidas: el activo %1$s tiene %2$s de entrada y %3$s de salida con la comisión y las quemas + la propuesta caduca en %1$s, fuera de la ventana de su bundle (%2$s, %3$s] + la transacción es para la cadena %1$s, la propuesta dice la cadena %2$s + la firma del firmante %1$s no es válida para esta propuesta + el firmante %1$s no está en esta política (tiene %2$s firmantes) + destinatario esperado %1$s: %2$s + la política no se puede interpretar: %1$s + la propuesta no se puede interpretar: %1$s + La comprobación de la propuesta devolvió una respuesta incompleta, así que esta propuesta no se firma. + Introduce la dirección de cada beneficiario: una salida que no coincide con la dirección introducida, o cuya dirección está vacía, no se puede firmar. + Ningún nodo respondió en %1$s, así que no se puede comprobar la caducidad y esta propuesta no se firma. Reintenta cuando el nodo esté accesible. + Esta propuesta es para la cadena %1$s; esta billetera está en la cadena %2$s. + Esta propuesta está ligada a un génesis distinto del que sirve el nodo de esta billetera. + Esta propuesta caduca en la altura %1$s y la cadena va por %2$s: es tarde para firmar. Pide una propuesta nueva. + No existe ese perfil de firmante. + Este perfil de firmante no tiene clave en este dispositivo: quítalo y vuelve a añadirlo. + la etiqueta de un firmante tiene 1-64 caracteres + este firmante ya está guardado como %1$s + Primero configura un bloqueo de pantalla (y, en Android 10 o anterior, una huella): la clave de firmante solo se usa justo después de que te autentiques. + No se pudieron quitar todas las claves de firmante de este dispositivo, así que se conservó la billetera. Inténtalo de nuevo. + Firmantes multifirma + Gestionar firmantes multifirma + Aún no hay perfiles de firmante + Añade el archivo de política y tu clave de firmante para firmar por una cuenta multifirma privada. + %1$s de %2$s · firmante %3$s + Añadir un firmante + Etiqueta + Archivo de política + Archivo de clave de firmante, o su semilla de 64 hex + Abrir archivo + Comprobar + Huella de la cuenta — compárala con la de cada otro firmante antes de guardar + %1$s de %2$s · esta clave es el firmante %3$s + Guardar firmante + Un perfil de firmante se guarda en el Android Keystore de este teléfono. Cada firma vuelve a pedir tu huella, tu cara o el bloqueo de pantalla. + Quitar este firmante + ¿Quitar %1$s? + Necesitas el archivo de clave del firmante para volver a añadirlo. + Quitar un firmante multifirma + Guardar un firmante multifirma + La clave de este firmante ya no se abre: las huellas o el bloqueo de pantalla del teléfono cambiaron desde que se guardó. Quita el firmante y vuelve a añadirlo desde su archivo de clave. + Firmar una propuesta + Propuesta (proposal.json) + Comprobar propuesta + Comprobando… + Pagos previstos + Introduce cada pago que piensas hacer: la dirección y el monto en RAND. Un pago a la misma dirección dos veces se introduce una sola vez, con el total. Una salida que no coincide con ninguno no se puede firmar. + Dirección del beneficiario + Monto en RAND + Eliminar + Añadir pago + Comprobar pagos + Los montos no se pueden comprobar en pagos que no son en RAND: deja el monto vacío en esos. + Este beneficiario recibe RAND y también un token: los pagos en RAND y en tokens a un mismo beneficiario necesitan propuestas separadas. + Introduce la dirección de cada pago. + Introduce el monto de cada pago: es lo que impide una división o un pago inflado. + Un monto es una cantidad de RAND, como 1.5. + Lo que hace esta propuesta, comprobado contra tu política + Comisión: %1$s RAND · caduca en la altura %2$s + %1$s de %2$s pagos coinciden. + Reintentar + Quedan %1$s bloques para probarlo. + Escribe los últimos cuatro caracteres de la huella de la cuenta + Los pagos cambiaron: compruébalos de nuevo antes de firmar. + Firmar + Firmando… + Firmar una propuesta multifirma + Tu parte de firma — envíasela a quien pruebe la propuesta + Copiar + Parte de firma + Parte de firma multifirma + Comprobar una parte de firma + Comprobar parte de firma + La parte de firma del firmante %1$s es válida. Tienes %2$s de %3$s: firmantes %4$s. + Ese archivo es mayor de lo que puede ser una propuesta, una política o un archivo de clave. + No se pudo leer ese archivo. diff --git a/android/app/src/main/res/values-fa/strings.xml b/android/app/src/main/res/values-fa/strings.xml index 82aa927..662a12b 100644 --- a/android/app/src/main/res/values-fa/strings.xml +++ b/android/app/src/main/res/values-fa/strings.xml @@ -457,4 +457,99 @@ تراکنش دیگری در حال اثبات است. پس از پایان آن دوباره امتحان کنید. این سواپ خوانا نیست. چیزی ارسال نشد. %1$s: %2$s + + هش تراکنش همان binding پیشنهاد نیست + signing_message پیشنهاد همان signing_message مربوط به binding آن نیست + auth_commit پیشنهاد حساب این سیاست نیست + digest مورد انتظار پیشنهاد همان digest مربوط به bundle آن نیست + تراکنش هیچ bundle ای ندارد + تراکنش یک اقدام دارد؛ پیشنهاد چندامضایی خصوصی فقط پرداختی از bundle خودش است + مقدار witness قابل بررسی نیست: nk یا salt آن مال این پیشنهاد نیست + این کلید یکی از امضاکنندگان سیاست نیست + سهم امضا به شکل index:hex است + کلید امضاکننده یک فایل keygen از rand-node یا ۶۴ نویسه هگز است + seed امضاکننده ۶۴ نویسه هگز است + expected_recipients باید فهرستی از آدرس‌ها باشد + public_key فایل کلید همان نیست که seed آن مشتق می‌کند + آدرس فایل کلید همان نیست که seed آن مشتق می‌کند + هیچ دریافت‌کننده مورد انتظاری داده نشد: دریافت‌کننده بررسی نشد (NOT checked) + این پیشنهاد برای سیاست دیگری است: %1$s را نام می‌برد، این سیاست %2$s است + خروجی %1$s به‌عنوان خروجی این حساب باز نمی‌شود: امضای مقداری که دیده نمی‌شود رد می‌شود + مقدار witness با تراکنش نمی‌خواند: ورودی %1$s به nullifier مربوط به bundle نمی‌رسد + مقدار witness با خروجی‌ها نمی‌خواند: دارایی %1$s ورودی %2$s و خروجی %3$s با کارمزد و سوزاندن‌ها دارد + پیشنهاد در %1$s منقضی می‌شود، بیرون از بازه bundle خود (%2$s, %3$s] + تراکنش برای شبکه %1$s است، پیشنهاد شبکه %2$s را می‌گوید + امضای امضاکننده %1$s برای این پیشنهاد معتبر نیست + امضاکننده %1$s در این سیاست نیست (این سیاست %2$s امضاکننده دارد) + دریافت‌کننده مورد انتظار %1$s: %2$s + سیاست تجزیه نمی‌شود: %1$s + پیشنهاد تجزیه نمی‌شود: %1$s + بررسی پیشنهاد پاسخ ناقصی برگرداند، پس این پیشنهاد امضا نمی‌شود. + آدرس هر دریافت‌کننده را وارد کنید: خروجی‌ای که با آدرس واردشده مطابقت ندارد یا آدرسش خالی است قابل امضا نیست. + هیچ گره‌ای در %1$s پاسخ نداد، پس انقضا قابل بررسی نیست و این پیشنهاد امضا نمی‌شود. وقتی گره در دسترس بود دوباره تلاش کنید. + این پیشنهاد برای شبکه %1$s است؛ این کیف پول روی شبکه %2$s است. + این پیشنهاد به genesis دیگری بسته شده است، نه آنچه گره این کیف پول ارائه می‌دهد. + این پیشنهاد در ارتفاع %1$s منقضی می‌شود و شبکه در %2$s است: برای امضا دیر شده است. پیشنهاد جدید بخواهید. + چنین نمایه امضاکننده‌ای وجود ندارد. + این نمایه امضاکننده روی این دستگاه کلیدی ندارد: آن را حذف کنید و دوباره اضافه کنید. + برچسب امضاکننده ۱-۶۴ نویسه است + این امضاکننده از قبل با نام %1$s ذخیره شده است + ابتدا یک قفل صفحه تنظیم کنید (و در Android 10 یا قدیمی‌تر، اثر انگشت): کلید امضاکننده فقط درست پس از احراز هویت شما به کار می‌رود. + همهٔ کلیدهای امضاکننده روی این دستگاه حذف نشدند، پس کیف پول نگه داشته شد. دوباره تلاش کنید. + امضاکنندگان چندامضایی + مدیریت امضاکنندگان چندامضایی + هنوز نمایه امضاکننده‌ای نیست + فایل سیاست و کلید امضاکننده خود را اضافه کنید تا برای یک حساب چندامضایی خصوصی امضا کنید. + %1$s از %2$s · امضاکننده %3$s + افزودن امضاکننده + برچسب + فایل سیاست + فایل کلید امضاکننده، یا seed ۶۴ نویسه‌ای هگز آن + باز کردن فایل + بررسی + اثر انگشت حساب — پیش از ذخیره، آن را با هر امضاکننده دیگر مقایسه کنید + %1$s از %2$s · این کلید امضاکننده %3$s است + ذخیره امضاکننده + نمایهٔ امضاکننده در Android Keystore این گوشی نگه داشته می‌شود. هر امضا دوباره اثر انگشت، چهره یا قفل صفحهٔ شما را می‌خواهد. + حذف این امضاکننده + %1$s حذف شود؟ + برای افزودن دوبارهٔ این امضاکننده به فایل کلید او نیاز دارید. + حذف یک امضاکننده چندامضایی + ذخیرهٔ یک امضاکننده چندامضایی + کلید این امضاکننده دیگر باز نمی‌شود: اثر انگشت‌ها یا قفل صفحهٔ گوشی پس از ذخیره تغییر کرده‌اند. امضاکننده را حذف کنید و دوباره از فایل کلیدش اضافه کنید. + امضای یک پیشنهاد + پیشنهاد (proposal.json) + بررسی پیشنهاد + در حال بررسی… + پرداخت‌های موردنظر + هر پرداختی را که قصد انجامش را دارید وارد کنید: آدرس و مقدار به RAND. دو پرداخت به یک آدرس یک‌بار و با مجموع وارد می‌شود. خروجی‌ای که با هیچ‌کدام مطابقت نداشته باشد قابل امضا نیست. + آدرس دریافت‌کننده + مقدار به RAND + حذف + افزودن پرداخت + بررسی پرداخت‌ها + مقدار پرداخت‌های غیر RAND قابل بررسی نیست: مقدار را برای آن‌ها خالی بگذارید. + این گیرنده هم RAND و هم یک توکن دریافت می‌کند: پرداخت‌های RAND و توکن به یک گیرنده به پیشنهادهای جداگانه نیاز دارند. + آدرس هر پرداخت را وارد کنید. + مقدار هر پرداخت را وارد کنید: همین است که جلوی تقسیم یا پرداخت باد‌شده را می‌گیرد. + مقدار یک عدد RAND است، مانند 1.5. + این پیشنهاد چه می‌کند، بررسی‌شده در برابر سیاست شما + کارمزد: %1$s RAND · در ارتفاع %2$s منقضی می‌شود + %1$s از %2$s پرداخت مطابقت دارد. + تلاش دوباره + %1$s بلوک مانده تا اثبات شود. + چهار نویسه آخر اثر انگشت حساب را بنویسید + پرداخت‌ها تغییر کردند: پیش از امضا دوباره بررسی‌شان کنید. + امضا + در حال امضا… + امضای یک پیشنهاد چندامضایی + سهم امضای شما — آن را برای کسی بفرستید که پیشنهاد را اثبات می‌کند + کپی + سهم امضا + سهم امضای چندامضایی + بررسی یک سهم امضا + بررسی سهم امضا + سهم امضای امضاکننده %1$s معتبر است. %2$s از %3$s در دست است: امضاکنندگان %4$s. + آن فایل از اندازه‌ای که یک پیشنهاد، سیاست یا فایل کلید می‌تواند داشته باشد بزرگ‌تر است. + این فایل خوانده نشد. diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 90bdc3b..5cc09ad 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -465,4 +465,99 @@ Une autre transaction est en cours de preuve. Réessayez quand elle sera terminée. Cet échange est illisible. Rien n\'a été envoyé. %1$s : %2$s + + le hash de la transaction n\'est pas la liaison de la proposition + le signing_message de la proposition n\'est pas celui de sa liaison + l\'auth_commit de la proposition n\'est pas le compte de cette politique + le digest attendu de la proposition n\'est pas celui de son lot + la transaction ne porte aucun lot + la transaction porte une action ; une proposition multisig privée est un paiement depuis son seul lot + la valeur du witness ne peut pas être vérifiée : son nk ou son sel n\'est pas celui de cette proposition + cette clé n\'est pas l\'un des signataires de la politique + une part de signature est index:hex + une clé de signataire est un fichier keygen de rand-node ou 64 caractères hexadécimaux + une graine de signataire compte 64 caractères hexadécimaux + expected_recipients doit être une liste d\'adresses + le public_key du fichier de clé n\'est pas celui que dérive sa graine + l\'adresse du fichier de clé n\'est pas celle que dérive sa graine + aucun destinataire attendu n\'a été indiqué : le bénéficiaire n\'a PAS été vérifié + cette proposition est pour une autre politique : elle nomme %1$s, cette politique est %2$s + la sortie %1$s ne s\'ouvre pas comme celle de ce compte : refus de signer une valeur qu\'elle ne peut pas voir + la valeur du witness ne correspond pas à la transaction : l\'entrée %1$s n\'annule pas vers le nullifier du lot + la valeur du witness ne correspond pas aux sorties : l\'actif %1$s a %2$s en entrée et %3$s en sortie avec les frais et les brûlages + la proposition expire à %1$s, hors de la fenêtre de son lot (%2$s, %3$s] + la transaction est pour la chaîne %1$s, la proposition indique la chaîne %2$s + la signature du signataire %1$s n\'est pas valide pour cette proposition + le signataire %1$s n\'est pas dans cette politique (elle compte %2$s signataires) + destinataire attendu %1$s : %2$s + la politique ne peut pas être analysée : %1$s + la proposition ne peut pas être analysée : %1$s + La vérification de la proposition a renvoyé une réponse incomplète, cette proposition n\'est donc pas signée. + Saisissez l\'adresse de chaque bénéficiaire : une sortie qui ne correspond pas à l\'adresse saisie, ou dont l\'adresse est vide, ne peut pas être signée. + Aucun nœud n\'a répondu à %1$s, l\'expiration ne peut donc pas être vérifiée et cette proposition n\'est pas signée. Réessayez quand le nœud sera joignable. + Cette proposition est pour la chaîne %1$s ; ce portefeuille est sur la chaîne %2$s. + Cette proposition est liée à un autre genesis que celui que sert le nœud de ce portefeuille. + Cette proposition expire à la hauteur %1$s et la chaîne en est à %2$s : trop tard pour signer. Demandez une nouvelle proposition. + Ce profil de signataire n\'existe pas. + Ce profil de signataire n\'a pas de clé sur cet appareil : retirez-le et ajoutez-le de nouveau. + une étiquette de signataire compte 1-64 caractères + ce signataire est déjà enregistré sous le nom %1$s + Définissez d\'abord un verrouillage de l\'écran (et, sous Android 10 ou antérieur, une empreinte) : une clé de signataire n\'est utilisée que juste après votre authentification. + Les clés de signataire de cet appareil n\'ont pas toutes pu être retirées, donc le portefeuille a été conservé. Réessayez. + Signataires multisig + Gérer les signataires multisig + Aucun profil de signataire pour l\'instant + Ajoutez le fichier de politique et votre clé de signataire pour signer pour un compte multisig privé. + %1$s sur %2$s · signataire %3$s + Ajouter un signataire + Étiquette + Fichier de politique + Fichier de clé de signataire, ou sa graine de 64 hex + Ouvrir un fichier + Vérifier + Empreinte du compte — comparez-la avec celle de chaque autre signataire avant d\'enregistrer + %1$s sur %2$s · cette clé est le signataire %3$s + Enregistrer le signataire + Un profil de signataire est conservé dans l\'Android Keystore de ce téléphone. Chaque signature redemande votre empreinte, votre visage ou le verrouillage de l\'écran. + Retirer ce signataire + Retirer %1$s ? + Il faut le fichier de clé du signataire pour le rajouter. + Retirer un signataire multisig + Enregistrer un signataire multisig + La clé de ce signataire ne s\'ouvre plus : les empreintes ou le verrouillage de l\'écran du téléphone ont changé depuis son enregistrement. Retirez le signataire et ajoutez-le de nouveau depuis son fichier de clé. + Signer une proposition + Proposition (proposal.json) + Vérifier la proposition + Vérification… + Paiements prévus + Saisissez chaque paiement que vous comptez faire : l\'adresse et le montant en RAND. Un paiement à la même adresse deux fois est saisi une seule fois, avec le total. Une sortie qui ne correspond à aucun ne peut pas être signée. + Adresse du bénéficiaire + Montant en RAND + Supprimer + Ajouter un paiement + Vérifier les paiements + Les montants ne peuvent pas être vérifiés pour les paiements hors RAND : laissez le montant vide pour ceux-là. + Ce bénéficiaire reçoit à la fois des RAND et un jeton : les paiements en RAND et en jetons à un même bénéficiaire exigent des propositions séparées. + Saisissez l\'adresse de chaque paiement. + Saisissez le montant de chaque paiement : c\'est ce qui empêche un fractionnement ou un paiement gonflé. + Un montant est un nombre de RAND, comme 1.5. + Ce que fait cette proposition, vérifié par rapport à votre politique + Frais : %1$s RAND · expire à la hauteur %2$s + %1$s paiements sur %2$s correspondent. + Réessayer + Il reste %1$s blocs pour la prouver. + Saisissez les quatre derniers caractères de l\'empreinte du compte + Les paiements ont changé : vérifiez-les de nouveau avant de signer. + Signer + Signature… + Signer une proposition multisig + Votre part de signature — envoyez-la à qui prouve la proposition + Copier + Part de signature + Part de signature multisig + Vérifier une part de signature + Vérifier la part de signature + La part de signature du signataire %1$s est valide. %2$s sur %3$s réunies : signataires %4$s. + Ce fichier est plus grand que ne peut l\'être une proposition, une politique ou un fichier de clé. + Ce fichier n\'a pas pu être lu. diff --git a/android/app/src/main/res/values-hi/strings.xml b/android/app/src/main/res/values-hi/strings.xml index e226a62..51e581e 100644 --- a/android/app/src/main/res/values-hi/strings.xml +++ b/android/app/src/main/res/values-hi/strings.xml @@ -457,4 +457,99 @@ एक और लेन-देन का प्रूफ़ बन रहा है। उसके पूरा होने पर फिर से कोशिश करें। यह स्वैप पढ़ा नहीं जा सकता। कुछ भी नहीं भेजा गया। %1$s: %2$s + + लेन-देन का हैश प्रस्ताव की बाइंडिंग का नहीं है + प्रस्ताव का signing_message उसकी बाइंडिंग का नहीं है + प्रस्ताव का auth_commit इस पॉलिसी का खाता नहीं है + प्रस्ताव का अपेक्षित डाइजेस्ट उसके बंडल का नहीं है + लेन-देन में कोई बंडल नहीं है + लेन-देन में एक एक्शन है; निजी मल्टीसिग प्रस्ताव केवल अपने बंडल से किया गया भुगतान होता है + विटनेस का मान जाँचा नहीं जा सकता: उसका nk या सॉल्ट इस प्रस्ताव का नहीं है + यह की पॉलिसी के हस्ताक्षरकर्ताओं में से नहीं है + हस्ताक्षर अंश index:hex रूप में होता है + हस्ताक्षरकर्ता की एक rand-node keygen फ़ाइल या 64 हेक्स अक्षर होती है + हस्ताक्षरकर्ता का सीड 64 हेक्स अक्षरों का होता है + expected_recipients पतों की सूची होनी चाहिए + की फ़ाइल की public_key वह नहीं है जो उसका सीड व्युत्पन्न करता है + की फ़ाइल का पता वह नहीं है जो उसका सीड व्युत्पन्न करता है + कोई अपेक्षित प्राप्तकर्ता नहीं दिया गया: प्राप्तकर्ता की जाँच नहीं हुई (NOT checked) + यह प्रस्ताव किसी और पॉलिसी का है: इसमें %1$s है, जबकि यह पॉलिसी %2$s है + आउटपुट %1$s इस खाते के रूप में नहीं खुलता: जो मान दिखता नहीं उस पर हस्ताक्षर से इनकार + विटनेस का मान लेन-देन से मेल नहीं खाता: इनपुट %1$s बंडल के नलिफ़ायर तक नहीं पहुँचता + विटनेस का मान आउटपुट से मेल नहीं खाता: एसेट %1$s में %2$s इनपुट और शुल्क व बर्न सहित %3$s आउटपुट है + प्रस्ताव %1$s पर समाप्त होता है, जो उसके बंडल की सीमा (%2$s, %3$s] से बाहर है + लेन-देन चेन %1$s के लिए है, प्रस्ताव चेन %2$s बताता है + हस्ताक्षरकर्ता %1$s का हस्ताक्षर इस प्रस्ताव के लिए मान्य नहीं है + हस्ताक्षरकर्ता %1$s इस पॉलिसी में नहीं है (इसमें %2$s हस्ताक्षरकर्ता हैं) + अपेक्षित प्राप्तकर्ता %1$s: %2$s + पॉलिसी पार्स नहीं होती: %1$s + प्रस्ताव पार्स नहीं होता: %1$s + प्रस्ताव की जाँच ने अधूरा उत्तर लौटाया, इसलिए इस प्रस्ताव पर हस्ताक्षर नहीं किए गए। + हर प्राप्तकर्ता का पता दर्ज करें: जो आउटपुट दर्ज किए पते से मेल नहीं खाता, या जिसका पता खाली है, उस पर हस्ताक्षर नहीं हो सकते। + %1$s पर किसी नोड ने जवाब नहीं दिया, इसलिए समाप्ति की जाँच नहीं हो सकती और इस प्रस्ताव पर हस्ताक्षर नहीं किए गए। नोड उपलब्ध होने पर फिर कोशिश करें। + यह प्रस्ताव चेन %1$s के लिए है; यह वॉलेट चेन %2$s पर है। + यह प्रस्ताव उस जेनेसिस से अलग जेनेसिस से बँधा है जिसे इस वॉलेट का नोड परोसता है। + यह प्रस्ताव ऊँचाई %1$s पर समाप्त होता है और चेन %2$s पर है: हस्ताक्षर के लिए बहुत देर हो चुकी। नया प्रस्ताव माँगें। + ऐसी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं है। + इस हस्ताक्षरकर्ता प्रोफ़ाइल की इस डिवाइस पर कोई की नहीं है: इसे हटाकर फिर जोड़ें। + हस्ताक्षरकर्ता का लेबल 1-64 अक्षरों का होता है + यह हस्ताक्षरकर्ता पहले से %1$s के रूप में सहेजा गया है + पहले स्क्रीन लॉक सेट करें (और Android 10 या पुराने पर एक फ़िंगरप्रिंट भी): हस्ताक्षरकर्ता की कुंजी आपके प्रमाणित करने के ठीक बाद ही इस्तेमाल होती है। + इस डिवाइस पर हस्ताक्षरकर्ता की सभी कुंजियाँ हटाई नहीं जा सकीं, इसलिए वॉलेट रखा गया। फिर से कोशिश करें। + मल्टीसिग हस्ताक्षरकर्ता + मल्टीसिग हस्ताक्षरकर्ता प्रबंधित करें + अभी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं + निजी मल्टीसिग खाते के लिए हस्ताक्षर करने हेतु पॉलिसी फ़ाइल और अपनी हस्ताक्षरकर्ता की जोड़ें। + %2$s में से %1$s · हस्ताक्षरकर्ता %3$s + हस्ताक्षरकर्ता जोड़ें + लेबल + पॉलिसी फ़ाइल + हस्ताक्षरकर्ता की फ़ाइल, या उसका 64-हेक्स सीड + फ़ाइल खोलें + जाँचें + खाते का फ़िंगरप्रिंट — सहेजने से पहले इसे हर दूसरे हस्ताक्षरकर्ता से मिलाएँ + %2$s में से %1$s · यह की हस्ताक्षरकर्ता %3$s है + हस्ताक्षरकर्ता सहेजें + हस्ताक्षरकर्ता प्रोफ़ाइल इस फ़ोन के Android Keystore में रखी जाती है। हर हस्ताक्षर पर फिर से आपका फ़िंगरप्रिंट, चेहरा या स्क्रीन लॉक माँगा जाता है। + इस हस्ताक्षरकर्ता को हटाएँ + %1$s को हटाएँ? + इस हस्ताक्षरकर्ता को दोबारा जोड़ने के लिए आपको उसकी कुंजी फ़ाइल चाहिए होगी। + मल्टीसिग हस्ताक्षरकर्ता हटाएँ + मल्टीसिग हस्ताक्षरकर्ता सहेजें + इस हस्ताक्षरकर्ता की कुंजी अब नहीं खुलती: सहेजने के बाद फ़ोन के फ़िंगरप्रिंट या स्क्रीन लॉक बदल गए। हस्ताक्षरकर्ता को हटाकर उसकी कुंजी फ़ाइल से फिर जोड़ें। + प्रस्ताव पर हस्ताक्षर करें + प्रस्ताव (proposal.json) + प्रस्ताव जाँचें + जाँच हो रही है… + इच्छित भुगतान + जो भी भुगतान आप करना चाहते हैं, हर एक दर्ज करें: पता और RAND में राशि। एक ही पते पर दो बार भुगतान कुल राशि के साथ एक बार दर्ज होता है। जो आउटपुट इनमें से किसी से मेल नहीं खाता, उस पर हस्ताक्षर नहीं हो सकते। + प्राप्तकर्ता का पता + राशि (RAND में) + हटाएँ + भुगतान जोड़ें + भुगतान जाँचें + RAND के अलावा अन्य भुगतानों की राशि जाँची नहीं जा सकती: उनके लिए राशि खाली छोड़ें। + यह प्राप्तकर्ता RAND और एक टोकन दोनों प्राप्त करता है: एक ही प्राप्तकर्ता को RAND और टोकन भुगतानों के लिए अलग-अलग प्रस्ताव चाहिए। + हर भुगतान का पता दर्ज करें। + हर भुगतान की राशि दर्ज करें: यही भुगतान को बाँटने या बढ़ाए जाने से रोकती है। + राशि RAND की एक संख्या होती है, जैसे 1.5। + यह प्रस्ताव क्या करता है, आपकी पॉलिसी के विरुद्ध जाँचा गया + शुल्क: %1$s RAND · ऊँचाई %2$s पर समाप्त + %2$s में से %1$s भुगतान मेल खाते हैं। + दोबारा कोशिश करें + इसे प्रूव करने के लिए %1$s ब्लॉक बाकी हैं। + खाते के फ़िंगरप्रिंट के अंतिम चार अक्षर लिखें + भुगतान बदल गए: हस्ताक्षर से पहले उन्हें फिर जाँचें। + हस्ताक्षर करें + हस्ताक्षर हो रहा है… + मल्टीसिग प्रस्ताव पर हस्ताक्षर करें + आपका हस्ताक्षर अंश — इसे उसे भेजें जो प्रस्ताव प्रूव करता है + कॉपी करें + हस्ताक्षर अंश + मल्टीसिग हस्ताक्षर अंश + हस्ताक्षर अंश जाँचें + हस्ताक्षर अंश जाँचें + हस्ताक्षरकर्ता %1$s का हस्ताक्षर अंश मान्य है। %3$s में से %2$s हाथ में हैं: हस्ताक्षरकर्ता %4$s। + वह फ़ाइल किसी प्रस्ताव, पॉलिसी या की फ़ाइल के संभव आकार से बड़ी है। + वह फ़ाइल पढ़ी नहीं जा सकी। diff --git a/android/app/src/main/res/values-in/strings.xml b/android/app/src/main/res/values-in/strings.xml index 91fe495..b1b1c59 100644 --- a/android/app/src/main/res/values-in/strings.xml +++ b/android/app/src/main/res/values-in/strings.xml @@ -449,4 +449,99 @@ Transaksi lain sedang dibuktikan. Coba lagi setelah selesai. Penukaran ini tidak dapat dibaca. Tidak ada yang dikirim. %1$s: %2$s + + hash transaksi bukan binding proposal + signing_message proposal bukan milik binding-nya + auth_commit proposal bukan akun kebijakan ini + digest yang diharapkan proposal bukan milik bundle-nya + transaksi tidak membawa bundle + transaksi membawa sebuah aksi; proposal multisig privat adalah pembayaran dari bundle-nya saja + nilai witness tidak dapat diperiksa: nk atau salt-nya bukan milik proposal ini + kunci ini bukan salah satu penanda tangan kebijakan + sebuah bagian tanda tangan berformat index:hex + kunci penanda tangan adalah berkas keygen rand-node atau 64 karakter hex + seed penanda tangan terdiri dari 64 karakter hex + expected_recipients harus berupa daftar alamat + public_key berkas kunci bukan yang diturunkan seed-nya + alamat berkas kunci bukan yang diturunkan seed-nya + tidak ada penerima yang diharapkan: penerima pembayaran TIDAK diperiksa + proposal ini untuk kebijakan lain: menyebut %1$s, kebijakan ini adalah %2$s + output %1$s tidak terbuka sebagai milik akun ini: menolak menandatangani nilai yang tidak dapat dilihatnya + nilai witness tidak cocok dengan transaksi: input %1$s tidak menghasilkan nullifier bundle + nilai witness tidak cocok dengan output: aset %1$s memiliki %2$s masuk dan %3$s keluar beserta biaya dan pembakaran + proposal kedaluwarsa pada %1$s, di luar jendela bundle-nya (%2$s, %3$s] + transaksi ini untuk chain %1$s, proposal menyebut chain %2$s + tanda tangan penanda tangan %1$s tidak valid untuk proposal ini + penanda tangan %1$s tidak ada dalam kebijakan ini (kebijakan ini memiliki %2$s penanda tangan) + penerima yang diharapkan %1$s: %2$s + kebijakan tidak dapat diurai: %1$s + proposal tidak dapat diurai: %1$s + Pemeriksaan proposal mengembalikan jawaban yang tidak lengkap, sehingga proposal ini tidak ditandatangani. + Masukkan alamat setiap penerima pembayaran: output yang tidak cocok dengan alamat yang Anda masukkan, atau yang alamatnya kosong, tidak dapat ditandatangani. + Tidak ada node yang menjawab di %1$s, sehingga masa berlaku tidak dapat diperiksa dan proposal ini tidak ditandatangani. Coba lagi saat node dapat dijangkau. + Proposal ini untuk chain %1$s; dompet ini berada di chain %2$s. + Proposal ini terikat pada genesis yang berbeda dari yang dilayani node dompet ini. + Proposal ini kedaluwarsa pada ketinggian %1$s dan chain berada di %2$s: terlambat untuk menandatangani. Minta proposal baru. + Profil penanda tangan itu tidak ada. + Profil penanda tangan ini tidak memiliki kunci di perangkat ini: hapus lalu tambahkan lagi. + label penanda tangan terdiri dari 1-64 karakter + penanda tangan ini sudah tersimpan sebagai %1$s + Setel kunci layar terlebih dahulu (dan, di Android 10 atau lebih lama, sidik jari): kunci penanda tangan hanya digunakan tepat setelah Anda mengautentikasi. + Tidak semua kunci penanda tangan di perangkat ini dapat dihapus, jadi dompet tetap disimpan. Coba lagi. + Penanda tangan multisig + Kelola penanda tangan multisig + Belum ada profil penanda tangan + Tambahkan berkas kebijakan dan kunci penanda tangan Anda untuk menandatangani akun multisig privat. + %1$s dari %2$s · penanda tangan %3$s + Tambah penanda tangan + Label + Berkas kebijakan + Berkas kunci penanda tangan, atau seed 64 hex-nya + Buka berkas + Periksa + Sidik jari akun — bandingkan dengan setiap penanda tangan lain sebelum menyimpan + %1$s dari %2$s · kunci ini adalah penanda tangan %3$s + Simpan penanda tangan + Profil penanda tangan disimpan di Android Keystore ponsel ini. Setiap tanda tangan meminta sidik jari, wajah, atau kunci layar Anda lagi. + Hapus penanda tangan ini + Hapus %1$s? + Anda memerlukan file kunci penanda tangan untuk menambahkannya kembali. + Hapus penanda tangan multisig + Simpan penanda tangan multisig + Kunci penanda tangan ini tidak bisa dibuka lagi: sidik jari atau kunci layar ponsel berubah sejak kunci disimpan. Hapus penanda tangan dan tambahkan lagi dari file kuncinya. + Tandatangani proposal + Proposal (proposal.json) + Periksa proposal + Memeriksa… + Pembayaran yang dimaksud + Masukkan setiap pembayaran yang akan Anda lakukan: alamat dan jumlah dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan satu kali, dengan totalnya. Output yang tidak cocok dengan satu pun tidak dapat ditandatangani. + Alamat penerima pembayaran + Jumlah dalam RAND + Hapus + Tambah pembayaran + Periksa pembayaran + Jumlah tidak dapat diperiksa untuk pembayaran non-RAND: kosongkan jumlahnya untuk pembayaran tersebut. + Penerima ini menerima RAND sekaligus token: pembayaran RAND dan token ke satu penerima memerlukan proposal terpisah. + Masukkan alamat setiap pembayaran. + Masukkan jumlah setiap pembayaran: inilah yang mencegah pemecahan atau pembayaran yang digelembungkan. + Jumlah adalah bilangan RAND, misalnya 1.5. + Apa yang dilakukan proposal ini, diperiksa terhadap kebijakan Anda + Biaya: %1$s RAND · kedaluwarsa pada ketinggian %2$s + %1$s dari %2$s pembayaran cocok. + Coba lagi + Tersisa %1$s blok untuk membuktikannya. + Ketik empat karakter terakhir sidik jari akun + Pembayaran berubah: periksa lagi sebelum menandatangani. + Tandatangani + Menandatangani… + Tandatangani proposal multisig + Bagian tanda tangan Anda — kirim ke yang membuktikan proposal + Salin + Bagian tanda tangan + Bagian tanda tangan multisig + Periksa bagian tanda tangan + Periksa bagian tanda tangan + Bagian tanda tangan penanda tangan %1$s valid. %2$s dari %3$s sudah terkumpul: penanda tangan %4$s. + Berkas itu lebih besar dari yang mungkin untuk sebuah proposal, kebijakan, atau berkas kunci. + File itu tidak dapat dibaca. diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml index 4625832..e6be5fc 100644 --- a/android/app/src/main/res/values-it/strings.xml +++ b/android/app/src/main/res/values-it/strings.xml @@ -465,4 +465,99 @@ È in corso la prova di un\'altra transazione. Riprova quando è finita. Questo scambio non si può leggere. Non è stato inviato nulla. %1$s: %2$s + + la transazione non ha come hash il binding della proposta + il signing_message della proposta non è quello del suo binding + l\'auth_commit della proposta non è l\'account di questa policy + il digest atteso della proposta non è quello del suo bundle + la transazione non porta alcun bundle + la transazione porta un\'azione; una proposta multisig privata è un pagamento dal solo suo bundle + il valore del witness non si può verificare: il suo nk o il suo salt non sono quelli di questa proposta + questa chiave non è uno dei firmatari della policy + una quota di firma è index:hex + una chiave di firmatario è un file keygen di rand-node o 64 caratteri esadecimali + un seed di firmatario è di 64 caratteri esadecimali + expected_recipients deve essere un elenco di indirizzi + il public_key del file di chiave non è quello che il suo seed deriva + l\'indirizzo del file di chiave non è quello che il suo seed deriva + nessun destinatario atteso indicato: il beneficiario NON è stato verificato + questa proposta è per un\'altra policy: nomina %1$s, questa policy è %2$s + l\'output %1$s non si apre come di questo account: rifiuto di firmare un valore che non può vedere + il valore del witness non corrisponde alla transazione: l\'input %1$s non produce il nullifier del bundle + il valore del witness non corrisponde agli output: l\'asset %1$s ha %2$s in ingresso e %3$s in uscita con la commissione e i burn + la proposta scade a %1$s, fuori dalla finestra del suo bundle (%2$s, %3$s] + la transazione è per la catena %1$s, la proposta indica la catena %2$s + la firma del firmatario %1$s non è valida per questa proposta + il firmatario %1$s non è in questa policy (ha %2$s firmatari) + destinatario atteso %1$s: %2$s + la policy non si può analizzare: %1$s + la proposta non si può analizzare: %1$s + La verifica della proposta ha restituito una risposta incompleta, quindi questa proposta non viene firmata. + Inserisci l\'indirizzo di ogni beneficiario: un output che non corrisponde all\'indirizzo inserito, o il cui indirizzo è vuoto, non può essere firmato. + Nessun nodo ha risposto a %1$s, quindi la scadenza non si può verificare e questa proposta non viene firmata. Riprova quando il nodo è raggiungibile. + Questa proposta è per la catena %1$s; questo portafoglio è sulla catena %2$s. + Questa proposta è legata a un genesis diverso da quello che serve il nodo di questo portafoglio. + Questa proposta scade all\'altezza %1$s e la catena è a %2$s: troppo tardi per firmare. Chiedi una nuova proposta. + Nessun profilo di firmatario del genere. + Questo profilo di firmatario non ha una chiave su questo dispositivo: rimuovilo e aggiungilo di nuovo. + l\'etichetta di un firmatario è di 1-64 caratteri + questo firmatario è già salvato come %1$s + Imposta prima un blocco schermo (e, su Android 10 o precedenti, un\'impronta): la chiave di un firmatario si usa solo subito dopo la tua autenticazione. + Non è stato possibile rimuovere tutte le chiavi del firmatario da questo dispositivo, quindi il portafoglio è stato conservato. Riprova. + Firmatari multisig + Gestisci i firmatari multisig + Ancora nessun profilo di firmatario + Aggiungi il file della policy e la tua chiave di firmatario per firmare per un account multisig privato. + %1$s di %2$s · firmatario %3$s + Aggiungi un firmatario + Etichetta + File della policy + File della chiave del firmatario, o il suo seed di 64 hex + Apri file + Verifica + Impronta dell\'account — confrontala con quella di ogni altro firmatario prima di salvare + %1$s di %2$s · questa chiave è il firmatario %3$s + Salva firmatario + Un profilo di firmatario è conservato nell\'Android Keystore di questo telefono. Ogni firma chiede di nuovo la tua impronta, il tuo volto o il blocco schermo. + Rimuovi questo firmatario + Rimuovere %1$s? + Serve il file della chiave del firmatario per aggiungerlo di nuovo. + Rimuovi un firmatario multisig + Salva un firmatario multisig + La chiave di questo firmatario non si apre più: le impronte o il blocco schermo del telefono sono cambiati da quando è stata salvata. Rimuovi il firmatario e aggiungilo di nuovo dal suo file di chiave. + Firma una proposta + Proposta (proposal.json) + Verifica la proposta + Verifica in corso… + Pagamenti previsti + Inserisci ogni pagamento che intendi fare: l\'indirizzo e l\'importo in RAND. Un pagamento allo stesso indirizzo due volte si inserisce una volta sola, con il totale. Un output che non corrisponde a nessuno non può essere firmato. + Indirizzo del beneficiario + Importo in RAND + Rimuovi + Aggiungi pagamento + Verifica i pagamenti + Gli importi non si possono verificare per i pagamenti non in RAND: lascia vuoto l\'importo per quelli. + Questo beneficiario riceve sia RAND sia un token: i pagamenti in RAND e in token a uno stesso beneficiario richiedono proposte separate. + Inserisci l\'indirizzo di ogni pagamento. + Inserisci l\'importo di ogni pagamento: è ciò che impedisce una divisione o un pagamento gonfiato. + Un importo è un numero di RAND, come 1.5. + Cosa fa questa proposta, verificato rispetto alla tua policy + Commissione: %1$s RAND · scade all\'altezza %2$s + %1$s pagamenti su %2$s corrispondono. + Riprova + Mancano %1$s blocchi per dimostrarlo. + Digita gli ultimi quattro caratteri dell\'impronta dell\'account + I pagamenti sono cambiati: verificali di nuovo prima di firmare. + Firma + Firma in corso… + Firma una proposta multisig + La tua quota di firma — mandala a chi dimostra la proposta + Copia + Quota di firma + Quota di firma multisig + Verifica una quota di firma + Verifica la quota di firma + La quota di firma del firmatario %1$s è valida. %2$s di %3$s in mano: firmatari %4$s. + Quel file è più grande di quanto possa essere una proposta, una policy o un file di chiave. + Impossibile leggere quel file. diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml index 316f455..bc3bea7 100644 --- a/android/app/src/main/res/values-ja/strings.xml +++ b/android/app/src/main/res/values-ja/strings.xml @@ -449,4 +449,99 @@ 別のトランザクションを証明中です。完了してからもう一度お試しください。 このスワップを読み取れません。何も送信されていません。 %1$s: %2$s + + トランザクションのハッシュが提案のバインディングと一致しません + 提案の signing_message が、そのバインディングのものと一致しません + 提案の auth_commit が、このポリシーのアカウントと一致しません + 提案の想定ダイジェストが、そのバンドルのものと一致しません + トランザクションにバンドルが含まれていません + トランザクションにアクションが含まれています。プライベートなマルチシグ提案は、そのバンドルだけからの支払いです + ウィットネスの値を確認できません: その nk またはソルトがこの提案のものではありません + このキーはポリシーの署名者ではありません + 署名シェアは index:hex 形式です + 署名者キーは rand-node の keygen ファイル、または 64 桁の16進文字です + 署名者のシードは 64 桁の16進文字です + expected_recipients はアドレスのリストでなければなりません + キーファイルの public_key が、そのシードから導出されるものと一致しません + キーファイルのアドレスが、そのシードから導出されるものと一致しません + 想定される受取人が指定されていません: 受取人は確認されていません (NOT checked) + この提案は別のポリシー用です: %1$s を指していますが、このポリシーは %2$s です + 出力 %1$s はこのアカウントのものとして開けません: 見えない値への署名を拒否します + ウィットネスの値がトランザクションと一致しません: 入力 %1$s はバンドルのヌリファイアに対応しません + ウィットネスの値が出力と一致しません: アセット %1$s は、入力 %2$s に対し、手数料とバーンを含めた出力が %3$s です + 提案の期限 %1$s は、そのバンドルの範囲 (%2$s, %3$s] の外です + トランザクションはチェーン %1$s 用ですが、提案はチェーン %2$s を指しています + 署名者 %1$s の署名は、この提案に対して有効ではありません + 署名者 %1$s はこのポリシーに含まれていません (署名者は %2$s 人です) + 想定される受取人 %1$s: %2$s + ポリシーを解析できません: %1$s + 提案を解析できません: %1$s + 提案の確認が不完全な応答を返したため、この提案には署名しません。 + すべての受取人のアドレスを入力してください。入力したアドレスと一致しない出力、またはアドレスが空の出力には署名できません。 + %1$s でノードから応答がなかったため、有効期限を確認できず、この提案には署名しません。ノードに接続できるようになってから再試行してください。 + この提案はチェーン %1$s 用ですが、このウォレットはチェーン %2$s にあります。 + この提案は、このウォレットのノードが提供するものとは別のジェネシスに結び付けられています。 + この提案は高さ %1$s で期限切れとなり、チェーンは %2$s にあります: 署名には遅すぎます。新しい提案を依頼してください。 + そのような署名者プロファイルはありません。 + この署名者プロファイルには、この端末にキーがありません。削除してから追加し直してください。 + 署名者のラベルは 1-64 文字です + この署名者はすでに %1$s として保存されています + 先に画面ロックを設定してください(Android 10 以前では指紋も)。署名者の鍵は認証の直後にのみ使われます。 + この端末の署名者の鍵をすべて削除できなかったため、ウォレットは残しました。もう一度お試しください。 + マルチシグ署名者 + マルチシグ署名者を管理 + 署名者プロファイルはまだありません + プライベートなマルチシグアカウントで署名するには、ポリシーファイルと署名者キーを追加してください。 + %1$s/%2$s · 署名者 %3$s + 署名者を追加 + ラベル + ポリシーファイル + 署名者キーファイル、または 64 桁16進のシード + ファイルを開く + 確認 + アカウントのフィンガープリント — 保存する前に、他のすべての署名者と照合してください + %1$s/%2$s · このキーは署名者 %3$s です + 署名者を保存 + 署名者プロファイルはこのスマートフォンの Android Keystore に保存されます。署名のたびに指紋、顔、または画面ロックを再度求めます。 + この署名者を削除 + %1$sを削除しますか? + この署名者を再び追加するには、署名者の鍵ファイルが必要です。 + マルチシグの署名者を削除 + マルチシグの署名者を保存 + この署名者の鍵はもう開けません。保存後にスマートフォンの指紋または画面ロックが変更されました。署名者を削除し、鍵ファイルから追加し直してください。 + 提案に署名 + 提案 (proposal.json) + 提案を確認 + 確認中… + 予定している支払い + 行う予定の支払いをすべて入力してください。アドレスと RAND 建ての金額です。同じアドレスへの支払いが2回ある場合は、合計額で1件として入力します。どれにも一致しない出力には署名できません。 + 受取人のアドレス + 金額 (RAND) + 削除 + 支払いを追加 + 支払いを確認 + RAND 以外の支払いは金額を確認できません。その場合は金額を空欄にしてください。 + この受取人は RAND とトークンの両方を受け取ります。同じ受取人への RAND とトークンの支払いは、別々の提案にする必要があります。 + すべての支払いのアドレスを入力してください。 + すべての支払いの金額を入力してください。支払いの分割や水増しを防ぐのは金額です。 + 金額は RAND の数値です。例: 1.5 + この提案が行うこと (あなたのポリシーと照合済み) + 手数料: %1$s RAND · 高さ %2$s で期限切れ + %2$s 件中 %1$s 件の支払いが一致しました。 + 再試行 + 証明まであと %1$s ブロックです。 + アカウントのフィンガープリントの末尾4文字を入力してください + 支払いが変更されました。署名する前にもう一度確認してください。 + 署名 + 署名中… + マルチシグの提案に署名 + あなたの署名シェア — 提案を証明する人に送ってください + コピー + 署名シェア + マルチシグ署名シェア + 署名シェアを確認 + 署名シェアを確認 + 署名者 %1$s の署名シェアは有効です。%3$s 件中 %2$s 件が揃っています: 署名者 %4$s。 + そのファイルは、提案・ポリシー・キーファイルとして許容されるサイズを超えています。 + そのファイルを読み込めませんでした。 diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml index 624961e..3b52987 100644 --- a/android/app/src/main/res/values-ko/strings.xml +++ b/android/app/src/main/res/values-ko/strings.xml @@ -449,4 +449,99 @@ 다른 거래를 증명하는 중입니다. 끝난 뒤에 다시 시도하세요. 이 스왑을 읽을 수 없습니다. 아무것도 전송되지 않았습니다. %1$s: %2$s + + 트랜잭션의 해시가 제안의 바인딩과 다릅니다 + 제안의 signing_message가 바인딩의 것과 다릅니다 + 제안의 auth_commit이 이 정책의 계정이 아닙니다 + 제안의 예상 다이제스트가 번들의 것과 다릅니다 + 트랜잭션에 번들이 없습니다 + 트랜잭션에 액션이 포함되어 있습니다. 비공개 멀티시그 제안은 번들만으로 이루어진 결제입니다 + 증인(witness) 값을 확인할 수 없습니다: nk 또는 솔트가 이 제안의 것이 아닙니다 + 이 키는 정책의 서명자가 아닙니다 + 서명 조각은 index:hex 형식입니다 + 서명자 키는 rand-node keygen 파일 또는 64자리 16진수 문자입니다 + 서명자 시드는 64자리 16진수 문자입니다 + expected_recipients는 주소 목록이어야 합니다 + 키 파일의 public_key가 시드에서 도출된 값과 다릅니다 + 키 파일의 주소가 시드에서 도출된 주소와 다릅니다 + 예상 수취인이 지정되지 않았습니다: 수취인을 확인하지 않았습니다 (NOT checked) + 이 제안은 다른 정책용입니다: %1$s을(를) 가리키며 이 정책은 %2$s입니다 + 출력 %1$s이(가) 이 계정의 것으로 열리지 않습니다: 볼 수 없는 값에는 서명을 거부합니다 + 증인(witness) 값이 트랜잭션과 일치하지 않습니다: 입력 %1$s이(가) 번들의 널리파이어로 이어지지 않습니다 + 증인(witness) 값이 출력과 일치하지 않습니다: 자산 %1$s의 입력은 %2$s, 수수료와 소각을 포함한 출력은 %3$s입니다 + 제안의 만료 %1$s이(가) 번들의 구간 (%2$s, %3$s] 밖에 있습니다 + 트랜잭션은 체인 %1$s용이지만 제안은 체인 %2$s을(를) 가리킵니다 + 서명자 %1$s의 서명이 이 제안에 대해 유효하지 않습니다 + 서명자 %1$s은(는) 이 정책에 없습니다 (서명자는 %2$s명입니다) + 예상 수취인 %1$s: %2$s + 정책을 해석할 수 없습니다: %1$s + 제안을 해석할 수 없습니다: %1$s + 제안 확인이 불완전한 응답을 반환했으므로 이 제안에는 서명하지 않습니다. + 모든 수취인의 주소를 입력하세요. 입력한 주소와 일치하지 않거나 주소가 비어 있는 출력은 서명할 수 없습니다. + %1$s에서 응답한 노드가 없어 만료를 확인할 수 없으므로 이 제안에는 서명하지 않습니다. 노드에 연결할 수 있을 때 다시 시도하세요. + 이 제안은 체인 %1$s용이며, 이 지갑은 체인 %2$s에 있습니다. + 이 제안은 이 지갑의 노드가 제공하는 것과 다른 제네시스에 묶여 있습니다. + 이 제안은 높이 %1$s에서 만료되며 체인은 %2$s에 있습니다: 서명하기에는 너무 늦었습니다. 새 제안을 요청하세요. + 해당 서명자 프로필이 없습니다. + 이 서명자 프로필에는 이 기기에 키가 없습니다. 제거한 뒤 다시 추가하세요. + 서명자 레이블은 1-64자입니다 + 이 서명자는 이미 %1$s(으)로 저장되어 있습니다 + 먼저 화면 잠금을 설정하세요(Android 10 이하에서는 지문도). 서명자 키는 인증 직후에만 사용됩니다. + 이 기기의 서명자 키를 모두 제거하지 못해 지갑을 유지했습니다. 다시 시도하세요. + 멀티시그 서명자 + 멀티시그 서명자 관리 + 서명자 프로필이 아직 없습니다 + 비공개 멀티시그 계정으로 서명하려면 정책 파일과 서명자 키를 추가하세요. + %1$s/%2$s · 서명자 %3$s + 서명자 추가 + 레이블 + 정책 파일 + 서명자 키 파일 또는 64자리 16진수 시드 + 파일 열기 + 확인 + 계정 지문 — 저장하기 전에 다른 모든 서명자와 비교하세요 + %1$s/%2$s · 이 키는 서명자 %3$s입니다 + 서명자 저장 + 서명자 프로필은 이 휴대폰의 Android Keystore에 보관됩니다. 서명할 때마다 지문, 얼굴 또는 화면 잠금을 다시 요청합니다. + 이 서명자 제거 + %1$s 서명자를 제거할까요? + 이 서명자를 다시 추가하려면 서명자의 키 파일이 필요합니다. + 멀티시그 서명자 제거 + 멀티시그 서명자 저장 + 이 서명자의 키를 더 이상 열 수 없습니다. 저장한 뒤 휴대폰의 지문 또는 화면 잠금이 바뀌었습니다. 서명자를 제거하고 키 파일에서 다시 추가하세요. + 제안에 서명 + 제안 (proposal.json) + 제안 확인 + 확인하는 중… + 예정된 결제 + 보내려는 모든 결제를 입력하세요: 주소와 RAND 금액입니다. 같은 주소로 두 번 결제하는 경우 합계로 한 번만 입력합니다. 어느 것과도 일치하지 않는 출력은 서명할 수 없습니다. + 수취인 주소 + 금액 (RAND) + 삭제 + 결제 추가 + 결제 확인 + RAND가 아닌 결제는 금액을 확인할 수 없습니다. 이 경우 금액을 비워 두세요. + 이 수취인은 RAND와 토큰을 모두 받습니다. 한 수취인에게 보내는 RAND 결제와 토큰 결제는 별도의 제안이 필요합니다. + 모든 결제의 주소를 입력하세요. + 모든 결제의 금액을 입력하세요. 결제를 쪼개거나 부풀리는 것을 막아 주는 것이 금액입니다. + 금액은 RAND 단위의 숫자입니다. 예: 1.5 + 이 제안이 하는 일 (정책과 대조해 확인됨) + 수수료: %1$s RAND · 높이 %2$s에서 만료 + 결제 %2$s건 중 %1$s건이 일치합니다. + 다시 시도 + 증명까지 %1$s 블록 남았습니다. + 계정 지문의 마지막 네 글자를 입력하세요 + 결제가 변경되었습니다. 서명하기 전에 다시 확인하세요. + 서명 + 서명하는 중… + 멀티시그 제안에 서명 + 내 서명 조각 — 제안을 증명하는 사람에게 보내세요 + 복사 + 서명 조각 + 멀티시그 서명 조각 + 서명 조각 확인 + 서명 조각 확인 + 서명자 %1$s의 서명 조각이 유효합니다. %3$s개 중 %2$s개 확보: 서명자 %4$s. + 파일이 제안, 정책, 키 파일로 허용되는 크기보다 큽니다. + 해당 파일을 읽을 수 없습니다. diff --git a/android/app/src/main/res/values-ms/strings.xml b/android/app/src/main/res/values-ms/strings.xml index 5a0615e..b836768 100644 --- a/android/app/src/main/res/values-ms/strings.xml +++ b/android/app/src/main/res/values-ms/strings.xml @@ -457,4 +457,99 @@ Transaksi lain sedang dibuktikan. Cuba lagi apabila ia selesai. Pertukaran ini tidak dapat dibaca. Tiada apa-apa dihantar. %1$s: %2$s + + hash transaksi bukan binding cadangan + signing_message cadangan bukan milik binding-nya + auth_commit cadangan bukan akaun dasar ini + digest dijangka cadangan bukan milik bundle-nya + transaksi tidak membawa bundle + transaksi membawa satu tindakan; cadangan multisig persendirian ialah pembayaran daripada bundle-nya sahaja + nilai witness tidak dapat disemak: nk atau salt-nya bukan milik cadangan ini + kunci ini bukan salah satu penandatangan dasar + bahagian tandatangan ialah index:hex + kunci penandatangan ialah fail keygen rand-node atau 64 aksara hex + seed penandatangan ialah 64 aksara hex + expected_recipients mestilah senarai alamat + public_key fail kunci bukan yang diterbitkan oleh seed-nya + alamat fail kunci bukan yang diterbitkan oleh seed-nya + tiada penerima dijangka diberikan: penerima bayaran TIDAK disemak + cadangan ini untuk dasar lain: ia menyebut %1$s, dasar ini ialah %2$s + output %1$s tidak terbuka sebagai milik akaun ini: enggan menandatangani nilai yang tidak dapat dilihatnya + nilai witness tidak sepadan dengan transaksi: input %1$s tidak menjadi nullifier bundle + nilai witness tidak sepadan dengan output: aset %1$s mempunyai %2$s masuk dan %3$s keluar dengan yuran dan pembakaran + cadangan tamat tempoh pada %1$s, di luar tetingkap bundle-nya (%2$s, %3$s] + transaksi ini untuk rantaian %1$s, cadangan menyebut rantaian %2$s + tandatangan penandatangan %1$s tidak sah untuk cadangan ini + penandatangan %1$s tiada dalam dasar ini (ia mempunyai %2$s penandatangan) + penerima dijangka %1$s: %2$s + dasar tidak dapat dihuraikan: %1$s + cadangan tidak dapat dihuraikan: %1$s + Semakan cadangan memulangkan jawapan yang tidak lengkap, jadi cadangan ini tidak ditandatangani. + Masukkan alamat setiap penerima bayaran: output yang tidak sepadan dengan alamat yang anda masukkan, atau yang alamatnya kosong, tidak boleh ditandatangani. + Tiada nod menjawab di %1$s, jadi tempoh tamat tidak dapat disemak dan cadangan ini tidak ditandatangani. Cuba lagi apabila nod boleh dicapai. + Cadangan ini untuk rantaian %1$s; dompet ini berada pada rantaian %2$s. + Cadangan ini terikat pada genesis yang berbeza daripada yang dilayani nod dompet ini. + Cadangan ini tamat tempoh pada ketinggian %1$s dan rantaian berada di %2$s: terlambat untuk menandatangani. Minta cadangan baharu. + Tiada profil penandatangan sedemikian. + Profil penandatangan ini tiada kunci pada peranti ini: buangnya dan tambah semula. + label penandatangan ialah 1-64 aksara + penandatangan ini sudah disimpan sebagai %1$s + Tetapkan kunci skrin dahulu (dan, pada Android 10 atau lebih lama, cap jari): kunci penandatangan hanya digunakan sejurus selepas anda mengesahkan diri. + Tidak semua kunci penandatangan pada peranti ini dapat dibuang, jadi dompet dikekalkan. Cuba lagi. + Penandatangan multisig + Urus penandatangan multisig + Belum ada profil penandatangan + Tambah fail dasar dan kunci penandatangan anda untuk menandatangani bagi akaun multisig persendirian. + %1$s daripada %2$s · penandatangan %3$s + Tambah penandatangan + Label + Fail dasar + Fail kunci penandatangan, atau seed 64-hex-nya + Buka fail + Semak + Cap jari akaun — bandingkan dengan setiap penandatangan lain sebelum menyimpan + %1$s daripada %2$s · kunci ini ialah penandatangan %3$s + Simpan penandatangan + Profil penandatangan disimpan dalam Android Keystore telefon ini. Setiap tandatangan meminta cap jari, wajah atau kunci skrin anda sekali lagi. + Buang penandatangan ini + Buang %1$s? + Anda memerlukan fail kunci penandatangan untuk menambahnya semula. + Buang penandatangan multisig + Simpan penandatangan multisig + Kunci penandatangan ini tidak lagi dapat dibuka: cap jari atau kunci skrin telefon berubah sejak ia disimpan. Buang penandatangan dan tambah semula daripada fail kuncinya. + Tandatangani cadangan + Cadangan (proposal.json) + Semak cadangan + Menyemak… + Pembayaran yang dimaksudkan + Masukkan setiap pembayaran yang anda ingin buat: alamat dan amaun dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan sekali, dengan jumlahnya. Output yang tidak sepadan dengan mana-mana tidak boleh ditandatangani. + Alamat penerima bayaran + Amaun dalam RAND + Buang + Tambah pembayaran + Semak pembayaran + Amaun tidak boleh disemak untuk pembayaran bukan RAND: biarkan amaun kosong bagi pembayaran itu. + Penerima ini menerima RAND dan juga token: pembayaran RAND dan token kepada satu penerima memerlukan cadangan berasingan. + Masukkan alamat setiap pembayaran. + Masukkan amaun setiap pembayaran: ia menghalang pemecahan atau pembayaran yang dibengkakkan. + Amaun ialah bilangan RAND, seperti 1.5. + Apa yang cadangan ini lakukan, disemak terhadap dasar anda + Yuran: %1$s RAND · tamat tempoh pada ketinggian %2$s + %1$s daripada %2$s pembayaran sepadan. + Cuba lagi + Tinggal %1$s blok lagi untuk membuktikannya. + Taip empat aksara terakhir cap jari akaun + Pembayaran berubah: semak semula sebelum menandatangani. + Tandatangan + Menandatangani… + Tandatangani cadangan multisig + Bahagian tandatangan anda — hantar kepada yang membuktikan cadangan + Salin + Bahagian tandatangan + Bahagian tandatangan multisig + Semak bahagian tandatangan + Semak bahagian tandatangan + Bahagian tandatangan penandatangan %1$s sah. %2$s daripada %3$s sudah ada: penandatangan %4$s. + Fail itu lebih besar daripada yang boleh bagi sesebuah cadangan, dasar atau fail kunci. + Fail itu tidak dapat dibaca. diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml index 767849a..5e36a0f 100644 --- a/android/app/src/main/res/values-pl/strings.xml +++ b/android/app/src/main/res/values-pl/strings.xml @@ -473,4 +473,99 @@ Trwa generowanie dowodu innej transakcji. Spróbuj ponownie, gdy się zakończy. Nie można odczytać tej wymiany. Nic nie wysłano. %1$s: %2$s + + skrót transakcji nie jest powiązaniem propozycji + signing_message propozycji nie jest signing_message jej powiązania + auth_commit propozycji nie jest kontem tej polityki + oczekiwany digest propozycji nie jest digestem jej bundle + transakcja nie zawiera bundle + transakcja zawiera akcję; prywatna propozycja multisig to płatność wyłącznie z jej bundle + wartości witness nie można sprawdzić: jego nk lub salt nie pochodzi z tej propozycji + ten klucz nie należy do sygnatariuszy polityki + udział podpisu ma postać index:hex + klucz sygnatariusza to plik keygen rand-node lub 64 znaki szesnastkowe + seed sygnatariusza to 64 znaki szesnastkowe + expected_recipients musi być listą adresów + public_key z pliku klucza nie jest tym, który wyprowadza jego seed + adres z pliku klucza nie jest tym, który wyprowadza jego seed + nie podano oczekiwanego odbiorcy: odbiorca NIE został sprawdzony + ta propozycja dotyczy innej polityki: wskazuje %1$s, ta polityka to %2$s + wyjście %1$s nie otwiera się jako należące do tego konta: odmowa podpisania wartości, której nie widzi + wartość witness nie zgadza się z transakcją: wejście %1$s nie daje nullifiera bundle + wartość witness nie zgadza się z wyjściami: aktywo %1$s ma %2$s na wejściu i %3$s na wyjściu z opłatą i spaleniami + propozycja wygasa na %1$s, poza oknem swojego bundle (%2$s, %3$s] + transakcja jest dla łańcucha %1$s, propozycja wskazuje łańcuch %2$s + podpis sygnatariusza %1$s nie jest poprawny dla tej propozycji + sygnatariusza %1$s nie ma w tej polityce (ma ona %2$s sygnatariuszy) + oczekiwany odbiorca %1$s: %2$s + nie można odczytać polityki: %1$s + nie można odczytać propozycji: %1$s + Sprawdzenie propozycji zwróciło niepełną odpowiedź, więc ta propozycja nie zostaje podpisana. + Wpisz adres każdego odbiorcy: wyjścia niepasującego do wpisanego adresu lub mającego pusty adres nie można podpisać. + Żaden węzeł nie odpowiedział pod %1$s, więc nie można sprawdzić wygaśnięcia i ta propozycja nie zostaje podpisana. Spróbuj ponownie, gdy węzeł będzie osiągalny. + Ta propozycja jest dla łańcucha %1$s; ten portfel jest w łańcuchu %2$s. + Ta propozycja jest związana z innym genesis niż ten, który obsługuje węzeł tego portfela. + Ta propozycja wygasa na wysokości %1$s, a łańcuch jest na %2$s: za późno na podpis. Poproś o nową propozycję. + Nie ma takiego profilu sygnatariusza. + Ten profil sygnatariusza nie ma klucza na tym urządzeniu: usuń go i dodaj ponownie. + etykieta sygnatariusza ma 1-64 znaki + ten sygnatariusz jest już zapisany jako %1$s + Najpierw ustaw blokadę ekranu (a na Androidzie 10 lub starszym także odcisk palca): klucz sygnatariusza jest używany tylko zaraz po uwierzytelnieniu. + Nie udało się usunąć wszystkich kluczy sygnatariusza z tego urządzenia, więc portfel został zachowany. Spróbuj ponownie. + Sygnatariusze multisig + Zarządzaj sygnatariuszami multisig + Brak profili sygnatariuszy + Dodaj plik polityki i swój klucz sygnatariusza, aby podpisywać dla prywatnego konta multisig. + %1$s z %2$s · sygnatariusz %3$s + Dodaj sygnatariusza + Etykieta + Plik polityki + Plik klucza sygnatariusza lub jego seed w 64 znakach hex + Otwórz plik + Sprawdź + Odcisk konta — porównaj go z odciskiem każdego innego sygnatariusza, zanim zapiszesz + %1$s z %2$s · ten klucz to sygnatariusz %3$s + Zapisz sygnatariusza + Profil sygnatariusza jest przechowywany w Android Keystore tego telefonu. Każdy podpis ponownie wymaga odcisku palca, twarzy lub blokady ekranu. + Usuń tego sygnatariusza + Usunąć %1$s? + Aby ponownie dodać tego sygnatariusza, potrzebujesz jego pliku klucza. + Usuń sygnatariusza multisig + Zapisz sygnatariusza multisig + Klucza tego sygnatariusza nie da się już otworzyć: odciski palców lub blokada ekranu telefonu zmieniły się od jego zapisania. Usuń sygnatariusza i dodaj go ponownie z jego pliku klucza. + Podpisz propozycję + Propozycja (proposal.json) + Sprawdź propozycję + Sprawdzanie… + Zamierzone płatności + Wpisz każdą płatność, którą zamierzasz wykonać: adres i kwotę w RAND. Płatność dwa razy na ten sam adres wpisuje się raz, z sumą. Wyjścia niepasującego do żadnej z nich nie można podpisać. + Adres odbiorcy + Kwota w RAND + Usuń + Dodaj płatność + Sprawdź płatności + Kwot nie można sprawdzić dla płatności innych niż w RAND: zostaw dla nich kwotę pustą. + Ten odbiorca otrzymuje zarówno RAND, jak i token: płatności w RAND i w tokenach do jednego odbiorcy wymagają osobnych propozycji. + Wpisz adres każdej płatności. + Wpisz kwotę każdej płatności: to ona chroni przed podziałem lub zawyżoną płatnością. + Kwota to liczba RAND, na przykład 1.5. + Co robi ta propozycja, sprawdzone względem Twojej polityki + Opłata: %1$s RAND · wygasa na wysokości %2$s + Zgodnych płatności: %1$s z %2$s. + Ponów + Zostało %1$s bloków, aby to udowodnić. + Wpisz ostatnie cztery znaki odcisku konta + Płatności się zmieniły: sprawdź je ponownie przed podpisaniem. + Podpisz + Podpisywanie… + Podpisz propozycję multisig + Twój udział podpisu — wyślij go temu, kto dowodzi propozycji + Kopiuj + Udział podpisu + Udział podpisu multisig + Sprawdź udział podpisu + Sprawdź udział podpisu + Udział podpisu sygnatariusza %1$s jest poprawny. Masz %2$s z %3$s: sygnatariusze %4$s. + Ten plik jest większy, niż może być propozycja, polityka lub plik klucza. + Nie udało się odczytać tego pliku. diff --git a/android/app/src/main/res/values-ps/strings.xml b/android/app/src/main/res/values-ps/strings.xml index 01a5633..a4b4044 100644 --- a/android/app/src/main/res/values-ps/strings.xml +++ b/android/app/src/main/res/values-ps/strings.xml @@ -457,4 +457,99 @@ د بلې راکړې ورکړې ثبوت جوړېږي. کله چې بشپړه شي، بیا هڅه وکړئ. دا تبادله نشي لوستل کېدای. هېڅ ونه لېږل شو. %1$s: %2$s + + د راکړې ورکړې hash د وړاندیز binding نه دی + د وړاندیز signing_message د هغه د binding نه دی + د وړاندیز auth_commit د دې پالیسۍ حساب نه دی + د وړاندیز تمه شوی digest د هغه د bundle نه دی + راکړه ورکړه هیڅ bundle نه لري + راکړه ورکړه یو عمل لري؛ خصوصي ملټي‌سېګ وړاندیز یوازې د خپل bundle څخه تادیه ده + د witness ارزښت نشي څېړل کېدای: د هغه nk یا salt د دې وړاندیز نه دی + دا کیلي د پالیسۍ له لاسلیک کوونکو څخه نه ده + د لاسلیک برخه index:hex ده + د لاسلیک کوونکي کیلي د rand-node keygen فایل یا ۶۴ هېکس توري دي + د لاسلیک کوونکي seed ۶۴ هېکس توري دي + expected_recipients باید د پتو لیست وي + د کیلي فایل public_key هغه نه دی چې seed یې راباسي + د کیلي فایل پته هغه نه ده چې seed یې راباسي + هیڅ تمه شوی ترلاسه کوونکی نه دی ورکړل شوی: ترلاسه کوونکی ونه څېړل شو (NOT checked) + دا وړاندیز د بلې پالیسۍ لپاره دی: %1$s یادوي، دا پالیسي %2$s ده + محصول %1$s د دې حساب په توګه نه خلاصېږي: د هغه ارزښت لاسلیک ردېږي چې نشي لیدل کېدای + د witness ارزښت له راکړې ورکړې سره نه برابرېږي: ننوتنه %1$s د bundle nullifier ته نه رسېږي + د witness ارزښت له محصولاتو سره نه برابرېږي: شتمنۍ %1$s %2$s راتلونکی او %3$s وتلونکی لري، له فیس او سوځولو سره + وړاندیز په %1$s کې پای ته رسېږي، د خپل bundle له کړکۍ (%2$s, %3$s] بهر + راکړه ورکړه د زنځیر %1$s لپاره ده، وړاندیز زنځیر %2$s وايي + د لاسلیک کوونکي %1$s لاسلیک د دې وړاندیز لپاره سم نه دی + لاسلیک کوونکی %1$s په دې پالیسۍ کې نشته (دا %2$s لاسلیک کوونکي لري) + تمه شوی ترلاسه کوونکی %1$s: %2$s + پالیسي نشي تحلیل کېدای: %1$s + وړاندیز نشي تحلیل کېدای: %1$s + د وړاندیز څېړنې ناقص ځواب راستون کړ، نو دا وړاندیز نه لاسلیک کېږي. + د هر تادیه ترلاسه کوونکي پته ولیکئ: هغه محصول چې له لیکل شوې پتې سره نه برابرېږي، یا پته یې خالي ده، لاسلیک نشي کېدای. + په %1$s کې هیڅ نوډ ځواب نه کړ، نو پای ته رسېدل نشي څېړل کېدای او دا وړاندیز نه لاسلیک کېږي. کله چې نوډ ترلاسه وي بیا هڅه وکړئ. + دا وړاندیز د زنځیر %1$s لپاره دی؛ دا والټ په زنځیر %2$s کې دی. + دا وړاندیز له هغه genesis پرته بل ته تړل شوی چې د دې والټ نوډ یې وړاندې کوي. + دا وړاندیز په %1$s لوړوالي کې پای ته رسېږي او زنځیر په %2$s کې دی: د لاسلیک لپاره ډېر ناوخته دی. نوی وړاندیز وغواړئ. + داسې د لاسلیک کوونکي پروفایل نشته. + دا د لاسلیک کوونکي پروفایل په دې وسیله کې کیلي نه لري: لرې یې کړئ او بیا یې زیات کړئ. + د لاسلیک کوونکي نښه ۱-۶۴ توري ده + دا لاسلیک کوونکی لا دمخه د %1$s په نامه خوندي دی + لومړی د سکرین قلف وټاکئ (او په Android 10 یا زوړ کې د ګوتې نښه هم): د لاسلیک کوونکي کلیځ یوازې ستاسو د تصدیق سمدستي وروسته کارول کېږي. + په دې وسیله د لاسلیک کوونکو ټولې کلیګانې لرې نشوې، نو بټوه وساتل شوه. بیا هڅه وکړئ. + د ملټي‌سېګ لاسلیک کوونکي + د ملټي‌سېګ لاسلیک کوونکي اداره کړئ + تر اوسه د لاسلیک کوونکي پروفایلونه نشته + د خصوصي ملټي‌سېګ حساب لپاره د لاسلیک کولو لپاره د پالیسۍ فایل او خپله د لاسلیک کوونکي کیلي زیاته کړئ. + د %2$s څخه %1$s · لاسلیک کوونکی %3$s + لاسلیک کوونکی زیات کړئ + نښه + د پالیسۍ فایل + د لاسلیک کوونکي د کیلي فایل، یا د هغه ۶۴ هېکس seed + فایل خلاص کړئ + وڅېړئ + د حساب ګوتنښه — مخکې له خوندي کولو یې له هر بل لاسلیک کوونکي سره پرتله کړئ + د %2$s څخه %1$s · دا کیلي لاسلیک کوونکی %3$s دی + لاسلیک کوونکی خوندي کړئ + د لاسلیک کوونکي پروفایل د دې تلیفون په Android Keystore کې ساتل کېږي. هر لاسلیک بیا ستاسو د ګوتې نښه، مخ یا د سکرین قلف غواړي. + دا لاسلیک کوونکی لرې کړئ + %1$s لرې کړئ؟ + د دې لاسلیک کوونکي د بیا زیاتولو لپاره ستاسو د هغه د کلیځ فایل ته اړتیا ده. + د ملټي‌سېګ لاسلیک کوونکی لرې کړئ + د ملټي‌سېګ لاسلیک کوونکی خوندي کړئ + د دې لاسلیک کوونکي کلیځ نور نه پرانیستل کېږي: له خوندي کېدو وروسته د تلیفون د ګوتو نښې یا د سکرین قلف بدل شوي. لاسلیک کوونکی لرې کړئ او بیا یې د کلیځ له فایل څخه زیات کړئ. + وړاندیز لاسلیک کړئ + وړاندیز (proposal.json) + وړاندیز وڅېړئ + څېړل کېږي… + موخه شوې تادیات + هره تادیه چې کول یې غواړئ ولیکئ: پته او په RAND کې اندازه. هماغې پتې ته دوه ځله تادیه یو ځل د ټولیز مجموعې سره لیکل کېږي. هغه محصول چې له هیڅ یوې سره نه برابرېږي لاسلیک نشي کېدای. + د ترلاسه کوونکي پته + په RAND کې اندازه + لرې یې کړئ + تادیه زیاته کړئ + تادیات وڅېړئ + د غیر RAND تادیاتو اندازه نشي څېړل کېدای: د هغوی لپاره اندازه خالي پرېږدئ. + دا ترلاسه کوونکی RAND او یو ټوکن دواړه ترلاسه کوي: یو ترلاسه کوونکي ته د RAND او ټوکن تادیات جلا وړاندیزونو ته اړتیا لري. + د هرې تادیې پته ولیکئ. + د هرې تادیې اندازه ولیکئ: همدا هغه څه دي چې ویش یا پړسول شوې تادیه ځنډوي. + اندازه د RAND شمېره ده، لکه 1.5. + دا وړاندیز څه کوي، ستاسو د پالیسۍ پر وړاندې څېړل شوی + فیس: %1$s RAND · په %2$s لوړوالي کې پای ته رسېږي + د %2$s تادیاتو څخه %1$s سره برابر شول. + بیا هڅه وکړئ + د ثبوت لپاره %1$s بلاکونه پاتې دي. + د حساب د ګوتنښې وروستي څلور توري ولیکئ + تادیات بدل شول: مخکې له لاسلیک بیا یې وڅېړئ. + لاسلیک + لاسلیک کېږي… + د ملټي‌سېګ وړاندیز لاسلیک کړئ + ستاسو د لاسلیک برخه — هغه چا ته یې ولېږئ چې وړاندیز ثابتوي + کاپي کړئ + د لاسلیک برخه + د ملټي‌سېګ د لاسلیک برخه + د لاسلیک برخه وڅېړئ + د لاسلیک برخه وڅېړئ + د لاسلیک کوونکي %1$s د لاسلیک برخه سمه ده. د %3$s څخه %2$s په لاس کې دي: لاسلیک کوونکي %4$s. + دا فایل له هغه څخه لوی دی چې یو وړاندیز، پالیسي یا د کیلي فایل کېدای شي. + دا فایل ونه لوستل شو. diff --git a/android/app/src/main/res/values-pt-rBR/strings.xml b/android/app/src/main/res/values-pt-rBR/strings.xml index fadd6b6..28d2984 100644 --- a/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/android/app/src/main/res/values-pt-rBR/strings.xml @@ -465,4 +465,99 @@ Outra transação está sendo provada. Tente de novo quando ela terminar. Não é possível ler esta troca. Nada foi enviado. %1$s: %2$s + + o hash da transação não é o vínculo da proposta + o signing_message da proposta não é o do seu vínculo + o auth_commit da proposta não é a conta desta política + o digest esperado da proposta não é o do seu bundle + a transação não carrega nenhum bundle + a transação carrega uma ação; uma proposta multiassinatura privada é um pagamento apenas do seu bundle + o valor do witness não pode ser verificado: seu nk ou salt não é o desta proposta + esta chave não é de nenhum dos signatários da política + uma parte da assinatura é index:hex + uma chave de signatário é um arquivo keygen do rand-node ou 64 caracteres hexadecimais + uma seed de signatário tem 64 caracteres hexadecimais + expected_recipients deve ser uma lista de endereços + o public_key do arquivo de chave não é o que sua seed deriva + o endereço do arquivo de chave não é o que sua seed deriva + nenhum destinatário esperado informado: o beneficiário NÃO foi verificado + esta proposta é de outra política: nomeia %1$s, esta política é %2$s + a saída %1$s não abre como desta conta: recusa-se a assinar um valor que não consegue ver + o valor do witness não confere com a transação: a entrada %1$s não gera o nullifier do bundle + o valor do witness não confere com as saídas: o ativo %1$s tem %2$s de entrada e %3$s de saída com a taxa e as queimas + a proposta expira em %1$s, fora da janela do seu bundle (%2$s, %3$s] + a transação é para a cadeia %1$s, a proposta diz a cadeia %2$s + a assinatura do signatário %1$s não é válida para esta proposta + o signatário %1$s não está nesta política (ela tem %2$s signatários) + destinatário esperado %1$s: %2$s + a política não pôde ser lida: %1$s + a proposta não pôde ser lida: %1$s + A verificação da proposta devolveu uma resposta incompleta, então esta proposta não é assinada. + Informe o endereço de cada beneficiário: uma saída que não corresponde ao endereço informado, ou cujo endereço está vazio, não pode ser assinada. + Nenhum nó respondeu em %1$s, então a expiração não pode ser verificada e esta proposta não é assinada. Tente de novo quando o nó estiver acessível. + Esta proposta é para a cadeia %1$s; esta carteira está na cadeia %2$s. + Esta proposta está vinculada a um genesis diferente do que o nó desta carteira serve. + Esta proposta expira na altura %1$s e a cadeia está em %2$s: tarde demais para assinar. Peça uma nova proposta. + Não existe esse perfil de signatário. + Este perfil de signatário não tem chave neste dispositivo: remova-o e adicione-o de novo. + o rótulo de um signatário tem 1-64 caracteres + este signatário já está salvo como %1$s + Defina primeiro um bloqueio de tela (e, no Android 10 ou anterior, uma digital): a chave de signatário só é usada logo depois de você se autenticar. + Não foi possível remover todas as chaves de signatário deste dispositivo, então a carteira foi mantida. Tente de novo. + Signatários multiassinatura + Gerenciar signatários multiassinatura + Ainda não há perfis de signatário + Adicione o arquivo de política e sua chave de signatário para assinar por uma conta multiassinatura privada. + %1$s de %2$s · signatário %3$s + Adicionar um signatário + Rótulo + Arquivo de política + Arquivo de chave do signatário, ou sua seed de 64 hex + Abrir arquivo + Verificar + Impressão digital da conta — compare-a com a de cada outro signatário antes de salvar + %1$s de %2$s · esta chave é o signatário %3$s + Salvar signatário + Um perfil de signatário fica guardado no Android Keystore deste telefone. Cada assinatura pede de novo sua digital, seu rosto ou o bloqueio de tela. + Remover este signatário + Remover %1$s? + Você precisa do arquivo de chave do signatário para adicioná-lo de novo. + Remover um signatário multiassinatura + Salvar um signatário multiassinatura + A chave deste signatário não abre mais: as digitais ou o bloqueio de tela do telefone mudaram desde que ela foi salva. Remova o signatário e adicione-o de novo a partir do arquivo de chave. + Assinar uma proposta + Proposta (proposal.json) + Verificar proposta + Verificando… + Pagamentos pretendidos + Informe cada pagamento que você pretende fazer: o endereço e o valor em RAND. Um pagamento ao mesmo endereço duas vezes é informado uma vez só, com o total. Uma saída que não corresponde a nenhum não pode ser assinada. + Endereço do beneficiário + Valor em RAND + Remover + Adicionar pagamento + Verificar pagamentos + Os valores não podem ser verificados em pagamentos que não são em RAND: deixe o valor vazio nesses. + Este beneficiário recebe RAND e também um token: pagamentos em RAND e em tokens a um mesmo beneficiário precisam de propostas separadas. + Informe o endereço de cada pagamento. + Informe o valor de cada pagamento: é o que impede uma divisão ou um pagamento inflado. + Um valor é um número de RAND, como 1.5. + O que esta proposta faz, verificado contra a sua política + Taxa: %1$s RAND · expira na altura %2$s + %1$s de %2$s pagamentos conferem. + Tentar de novo + Faltam %1$s blocos para prová-la. + Digite os últimos quatro caracteres da impressão digital da conta + Os pagamentos mudaram: verifique-os de novo antes de assinar. + Assinar + Assinando… + Assinar uma proposta multiassinatura + Sua parte da assinatura — envie-a a quem prova a proposta + Copiar + Parte da assinatura + Parte da assinatura multiassinatura + Verificar uma parte da assinatura + Verificar parte da assinatura + A parte da assinatura do signatário %1$s é válida. %2$s de %3$s em mãos: signatários %4$s. + Esse arquivo é maior do que uma proposta, uma política ou um arquivo de chave pode ser. + Não foi possível ler esse arquivo. diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 0784720..439d8ed 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -473,4 +473,99 @@ Сейчас строится доказательство другой транзакции. Попробуйте снова, когда оно завершится. Не удаётся прочитать этот обмен. Ничего не отправлено. %1$s: %2$s + + хеш транзакции — не привязка предложения + signing_message предложения — не signing_message его привязки + auth_commit предложения — не аккаунт этой политики + ожидаемый digest предложения — не digest его bundle + транзакция не несёт bundle + транзакция несёт действие; приватное мультиподписное предложение — это платёж только из его bundle + значение witness проверить нельзя: его nk или salt — не от этого предложения + этот ключ не входит в число подписантов политики + доля подписи имеет вид index:hex + ключ подписанта — это файл keygen от rand-node или 64 шестнадцатеричных символа + seed подписанта — 64 шестнадцатеричных символа + expected_recipients должен быть списком адресов + public_key в файле ключа не тот, что выводится из его seed + адрес в файле ключа не тот, что выводится из его seed + ожидаемый получатель не указан: получатель НЕ проверялся + это предложение для другой политики: в нём указано %1$s, эта политика — %2$s + выход %1$s не открывается как принадлежащий этому аккаунту: отказ подписывать значение, которое нельзя увидеть + значение witness не совпадает с транзакцией: вход %1$s не даёт nullifier bundle + значение witness не совпадает с выходами: у актива %1$s на входе %2$s, на выходе %3$s с комиссией и сжиганиями + предложение истекает на %1$s, вне окна его bundle (%2$s, %3$s] + транзакция предназначена для сети %1$s, а предложение указывает сеть %2$s + подпись подписанта %1$s не верна для этого предложения + подписанта %1$s нет в этой политике (в ней %2$s подписантов) + ожидаемый получатель %1$s: %2$s + политику не удалось разобрать: %1$s + предложение не удалось разобрать: %1$s + Проверка предложения вернула неполный ответ, поэтому это предложение не подписано. + Введите адрес каждого получателя: выход, не совпадающий с введённым адресом или с пустым адресом, подписать нельзя. + Ни один узел не ответил по адресу %1$s, поэтому срок действия проверить нельзя, и это предложение не подписано. Повторите, когда узел станет доступен. + Это предложение для сети %1$s; этот кошелёк в сети %2$s. + Это предложение привязано к другому genesis, чем тот, который обслуживает узел этого кошелька. + Это предложение истекает на высоте %1$s, а сеть на %2$s: подписывать поздно. Запросите новое предложение. + Такого профиля подписанта нет. + У этого профиля подписанта нет ключа на этом устройстве: удалите его и добавьте снова. + метка подписанта — 1-64 символа + этот подписант уже сохранён как %1$s + Сначала задайте блокировку экрана (а на Android 10 и более ранних версиях — ещё и отпечаток пальца): ключ подписанта используется только сразу после вашей аутентификации. + Не удалось удалить все ключи подписанта на этом устройстве, поэтому кошелёк сохранён. Попробуйте снова. + Подписанты мультиподписи + Управление подписантами мультиподписи + Профилей подписантов пока нет + Добавьте файл политики и ключ подписанта, чтобы подписывать за приватный мультиподписной аккаунт. + %1$s из %2$s · подписант %3$s + Добавить подписанта + Метка + Файл политики + Файл ключа подписанта или его seed из 64 hex-символов + Открыть файл + Проверить + Отпечаток аккаунта — сверьте его с каждым другим подписантом, прежде чем сохранять + %1$s из %2$s · этот ключ — подписант %3$s + Сохранить подписанта + Профиль подписанта хранится в Android Keystore этого телефона. Каждая подпись снова запрашивает отпечаток пальца, лицо или блокировку экрана. + Удалить этого подписанта + Удалить %1$s? + Чтобы снова добавить этого подписанта, нужен его файл ключа. + Удалить подписанта мультиподписи + Сохранить подписанта мультиподписи + Ключ этого подписанта больше не открывается: отпечатки пальцев или блокировка экрана телефона изменились после его сохранения. Удалите подписанта и добавьте его снова из файла ключа. + Подписать предложение + Предложение (proposal.json) + Проверить предложение + Проверка… + Предполагаемые платежи + Введите каждый платёж, который вы собираетесь сделать: адрес и сумму в RAND. Платёж на один и тот же адрес дважды вводится один раз, с итоговой суммой. Выход, не совпадающий ни с одним из них, подписать нельзя. + Адрес получателя + Сумма в RAND + Удалить + Добавить платёж + Проверить платежи + Суммы нельзя проверить для платежей не в RAND: оставьте для них сумму пустой. + Этот получатель получает и RAND, и токен: платежи в RAND и в токенах одному получателю требуют отдельных предложений. + Введите адрес каждого платежа. + Введите сумму каждого платежа: именно она не даёт разбить платёж или завысить его. + Сумма — это число RAND, например 1.5. + Что делает это предложение — проверено по вашей политике + Комиссия: %1$s RAND · истекает на высоте %2$s + Совпало платежей: %1$s из %2$s. + Повторить + Осталось %1$s блоков, чтобы доказать это. + Введите последние четыре символа отпечатка аккаунта + Платежи изменились: проверьте их снова перед подписью. + Подписать + Подписание… + Подписать предложение мультиподписи + Ваша доля подписи — отправьте её тому, кто доказывает предложение + Копировать + Доля подписи + Доля мультиподписи + Проверить долю подписи + Проверить долю подписи + Доля подписи подписанта %1$s верна. Собрано %2$s из %3$s: подписанты %4$s. + Этот файл больше, чем может быть предложение, политика или файл ключа. + Не удалось прочитать этот файл. diff --git a/android/app/src/main/res/values-ta/strings.xml b/android/app/src/main/res/values-ta/strings.xml index 54900bb..054d85c 100644 --- a/android/app/src/main/res/values-ta/strings.xml +++ b/android/app/src/main/res/values-ta/strings.xml @@ -482,4 +482,99 @@ மற்றொரு பரிவர்த்தனை நிரூபிக்கப்படுகிறது. அது முடிந்ததும் மீண்டும் முயலவும். இந்த ஸ்வாப்பைப் படிக்க முடியாது. எதுவும் அனுப்பப்படவில்லை. %1$s: %2$s + + பரிவர்த்தனையின் ஹாஷ் முன்மொழிவின் binding அல்ல + முன்மொழிவின் signing_message அதன் binding-இனுடையது அல்ல + முன்மொழிவின் auth_commit இந்தக் கொள்கையின் கணக்கு அல்ல + முன்மொழிவின் எதிர்பார்க்கப்படும் digest அதன் bundle-இனுடையது அல்ல + பரிவர்த்தனையில் bundle எதுவும் இல்லை + பரிவர்த்தனை ஒரு செயலைக் கொண்டுள்ளது; தனிப்பட்ட மல்டிசிக் முன்மொழிவு என்பது அதன் bundle-இலிருந்து மட்டும் செய்யும் கட்டணம் + witness-இன் மதிப்பைச் சரிபார்க்க முடியவில்லை: அதன் nk அல்லது salt இந்த முன்மொழிவினுடையது அல்ல + இந்த விசை கொள்கையின் கையொப்பமிடுபவர்களில் ஒன்றல்ல + கையொப்பப் பங்கு index:hex வடிவில் இருக்கும் + கையொப்பமிடுபவர் விசை என்பது rand-node keygen கோப்பு அல்லது 64 ஹெக்ஸ் எழுத்துகள் + கையொப்பமிடுபவர் சீட் 64 ஹெக்ஸ் எழுத்துகள் + expected_recipients முகவரிகளின் பட்டியலாக இருக்க வேண்டும் + விசைக் கோப்பின் public_key அதன் சீட் வருவிப்பதல்ல + விசைக் கோப்பின் முகவரி அதன் சீட் வருவிப்பதல்ல + எதிர்பார்க்கப்படும் பெறுநர் கொடுக்கப்படவில்லை: பெறுநர் சரிபார்க்கப்படவில்லை (NOT checked) + இந்த முன்மொழிவு வேறொரு கொள்கைக்கானது: அது %1$s என்கிறது, இந்தக் கொள்கை %2$s + வெளியீடு %1$s இந்தக் கணக்கினுடையதாகத் திறக்கவில்லை: பார்க்க முடியாத மதிப்புக்குக் கையொப்பமிட மறுக்கப்படுகிறது + witness-இன் மதிப்பு பரிவர்த்தனையுடன் பொருந்தவில்லை: உள்ளீடு %1$s bundle-இன் nullifier-க்கு வரவில்லை + witness-இன் மதிப்பு வெளியீடுகளுடன் பொருந்தவில்லை: சொத்து %1$s-இல் உள்ளீடு %2$s, கட்டணம் மற்றும் எரிப்புகளுடன் வெளியீடு %3$s + முன்மொழிவு %1$s-இல் காலாவதியாகிறது, அதன் bundle-இன் சாளரத்துக்கு (%2$s, %3$s] வெளியே + பரிவர்த்தனை செயின் %1$s-க்கானது, முன்மொழிவு செயின் %2$s என்கிறது + கையொப்பமிடுபவர் %1$s-இன் கையொப்பம் இந்த முன்மொழிவுக்குச் சரியானதல்ல + கையொப்பமிடுபவர் %1$s இந்தக் கொள்கையில் இல்லை (இதில் %2$s கையொப்பமிடுபவர்கள் உள்ளனர்) + எதிர்பார்க்கப்படும் பெறுநர் %1$s: %2$s + கொள்கையைப் பாகுபடுத்த முடியவில்லை: %1$s + முன்மொழிவைப் பாகுபடுத்த முடியவில்லை: %1$s + முன்மொழிவுச் சரிபார்ப்பு முழுமையற்ற பதிலைத் தந்தது, எனவே இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை. + பெறுபவர் ஒவ்வொருவரின் முகவரியையும் உள்ளிடுங்கள்: நீங்கள் உள்ளிட்ட முகவரியுடன் பொருந்தாத அல்லது முகவரி வெறுமையாக உள்ள வெளியீட்டில் கையொப்பமிட முடியாது. + %1$s-இல் எந்த நோட்டும் பதிலளிக்கவில்லை, எனவே காலாவதியைச் சரிபார்க்க முடியாது; இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை. நோட் எட்டக்கூடியதாகும்போது மீண்டும் முயலுங்கள். + இந்த முன்மொழிவு செயின் %1$s-க்கானது; இந்த வாலட் செயின் %2$s-இல் உள்ளது. + இந்த முன்மொழிவு, இந்த வாலட்டின் நோட் வழங்கும் genesis-ஐ விட வேறொன்றுடன் பிணைக்கப்பட்டுள்ளது. + இந்த முன்மொழிவு உயரம் %1$s-இல் காலாவதியாகும், செயின் %2$s-இல் உள்ளது: கையொப்பமிட மிகவும் தாமதம். புதிய முன்மொழிவைக் கேளுங்கள். + அப்படி ஒரு கையொப்பமிடுபவர் சுயவிவரம் இல்லை. + இந்தக் கையொப்பமிடுபவர் சுயவிவரத்துக்கு இந்தச் சாதனத்தில் விசை இல்லை: அதை நீக்கி மீண்டும் சேருங்கள். + கையொப்பமிடுபவர் லேபிள் 1-64 எழுத்துகள் + இந்தக் கையொப்பமிடுபவர் ஏற்கெனவே %1$s என்று சேமிக்கப்பட்டுள்ளார் + முதலில் திரைப் பூட்டை அமைக்கவும் (Android 10 அல்லது பழையதில் கைரேகையும்): நீங்கள் அங்கீகரித்த உடனேயே மட்டுமே கையொப்பமிடுபவர் விசை பயன்படுத்தப்படும். + இந்தச் சாதனத்தில் உள்ள கையொப்பமிடுபவர் விசைகள் அனைத்தையும் நீக்க முடியவில்லை, எனவே பணப்பை வைக்கப்பட்டது. மீண்டும் முயலவும். + மல்டிசிக் கையொப்பமிடுபவர்கள் + மல்டிசிக் கையொப்பமிடுபவர்களை நிர்வகி + கையொப்பமிடுபவர் சுயவிவரங்கள் இன்னும் இல்லை + தனிப்பட்ட மல்டிசிக் கணக்கிற்குக் கையொப்பமிட, கொள்கைக் கோப்பையும் உங்கள் கையொப்பமிடுபவர் விசையையும் சேருங்கள். + %2$s-இல் %1$s · கையொப்பமிடுபவர் %3$s + கையொப்பமிடுபவரைச் சேர் + லேபிள் + கொள்கைக் கோப்பு + கையொப்பமிடுபவர் விசைக் கோப்பு, அல்லது அதன் 64-ஹெக்ஸ் சீட் + கோப்பைத் திற + சரிபார் + கணக்கு கைரேகை — சேமிப்பதற்கு முன் மற்ற ஒவ்வொரு கையொப்பமிடுபவருடனும் ஒப்பிடுங்கள் + %2$s-இல் %1$s · இந்த விசை கையொப்பமிடுபவர் %3$s + கையொப்பமிடுபவரைச் சேமி + கையொப்பமிடுபவர் சுயவிவரம் இந்தத் தொலைபேசியின் Android Keystore-இல் வைக்கப்படுகிறது. ஒவ்வொரு கையொப்பமும் மீண்டும் உங்கள் கைரேகை, முகம் அல்லது திரைப் பூட்டைக் கேட்கும். + இந்தக் கையொப்பமிடுபவரை நீக்கு + %1$s ஐ நீக்கவா? + இந்தக் கையொப்பமிடுபவரை மீண்டும் சேர்க்க அவருடைய விசைக் கோப்பு தேவை. + மல்டிசிக் கையொப்பமிடுபவரை நீக்கு + மல்டிசிக் கையொப்பமிடுபவரைச் சேமி + இந்தக் கையொப்பமிடுபவரின் விசை இனி திறக்காது: சேமித்த பிறகு தொலைபேசியின் கைரேகைகள் அல்லது திரைப் பூட்டு மாறியுள்ளது. கையொப்பமிடுபவரை நீக்கி, அதன் விசைக் கோப்பிலிருந்து மீண்டும் சேருங்கள். + முன்மொழிவில் கையொப்பமிடு + முன்மொழிவு (proposal.json) + முன்மொழிவைச் சரிபார் + சரிபார்க்கிறது… + நோக்கிய கட்டணங்கள் + நீங்கள் செய்யவிருக்கும் ஒவ்வொரு கட்டணத்தையும் உள்ளிடுங்கள்: முகவரியும் RAND-இல் தொகையும். ஒரே முகவரிக்கு இருமுறை செலுத்தும் கட்டணம் மொத்தத்துடன் ஒருமுறை உள்ளிடப்படும். இவற்றில் எதனுடனும் பொருந்தாத வெளியீட்டில் கையொப்பமிட முடியாது. + பெறுபவர் முகவரி + RAND-இல் தொகை + நீக்கு + கட்டணத்தைச் சேர் + கட்டணங்களைச் சரிபார் + RAND அல்லாத கட்டணங்களின் தொகையைச் சரிபார்க்க முடியாது: அவற்றுக்குத் தொகையை வெறுமையாக விடுங்கள். + இந்தப் பெறுநர் RAND மற்றும் ஒரு டோக்கன் இரண்டையும் பெறுகிறார்: ஒரே பெறுநருக்கான RAND மற்றும் டோக்கன் கொடுப்பனவுகளுக்குத் தனித்தனி முன்மொழிவுகள் தேவை. + ஒவ்வொரு கட்டணத்தின் முகவரியையும் உள்ளிடுங்கள். + ஒவ்வொரு கட்டணத்தின் தொகையையும் உள்ளிடுங்கள்: பிரித்தலையோ பெருக்கிய கட்டணத்தையோ தடுப்பது இதுவே. + தொகை என்பது RAND-இன் எண், எடுத்துக்காட்டாக 1.5. + இந்த முன்மொழிவு என்ன செய்கிறது, உங்கள் கொள்கைக்கு எதிராகச் சரிபார்க்கப்பட்டது + கட்டணம்: %1$s RAND · உயரம் %2$s-இல் காலாவதியாகும் + %2$s கட்டணங்களில் %1$s பொருந்துகின்றன. + மீண்டும் முயல் + இதை நிரூபிக்க இன்னும் %1$s பிளாக்குகள் உள்ளன. + கணக்கு கைரேகையின் கடைசி நான்கு எழுத்துகளை உள்ளிடுங்கள் + கட்டணங்கள் மாறின: கையொப்பமிடும் முன் மீண்டும் சரிபாருங்கள். + கையொப்பமிடு + கையொப்பமிடுகிறது… + மல்டிசிக் முன்மொழிவில் கையொப்பமிடு + உங்கள் கையொப்பப் பங்கு — முன்மொழிவை நிரூபிப்பவருக்கு அனுப்புங்கள் + நகலெடு + கையொப்பப் பங்கு + மல்டிசிக் கையொப்பப் பங்கு + கையொப்பப் பங்கைச் சரிபார் + கையொப்பப் பங்கைச் சரிபார் + கையொப்பமிடுபவர் %1$s-இன் கையொப்பப் பங்கு சரியானது. %3$s-இல் %2$s கையில் உள்ளன: கையொப்பமிடுபவர்கள் %4$s. + அந்தக் கோப்பு ஒரு முன்மொழிவு, கொள்கை அல்லது விசைக் கோப்பு இருக்கக்கூடியதை விடப் பெரியது. + அந்தக் கோப்பைப் படிக்க முடியவில்லை. diff --git a/android/app/src/main/res/values-ur/strings.xml b/android/app/src/main/res/values-ur/strings.xml index 9855765..6dc949b 100644 --- a/android/app/src/main/res/values-ur/strings.xml +++ b/android/app/src/main/res/values-ur/strings.xml @@ -457,4 +457,99 @@ ایک اور لین دین کا پروف بن رہا ہے۔ اس کے مکمل ہونے پر دوبارہ کوشش کریں۔ یہ سواپ پڑھا نہیں جا سکتا۔ کچھ نہیں بھیجا گیا۔ %1$s: %2$s + + لین دین کا ہیش تجویز کی بائنڈنگ کا نہیں ہے + تجویز کا signing_message اس کی بائنڈنگ کا نہیں ہے + تجویز کا auth_commit اس پالیسی کا اکاؤنٹ نہیں ہے + تجویز کا متوقع ڈائجسٹ اس کے بنڈل کا نہیں ہے + لین دین میں کوئی بنڈل نہیں ہے + لین دین میں ایک عمل ہے؛ نجی ملٹی سگ تجویز صرف اپنے بنڈل سے ادائیگی ہوتی ہے + ویٹنس کی قدر جانچی نہیں جا سکتی: اس کا nk یا سالٹ اس تجویز کا نہیں ہے + یہ کی پالیسی کے دستخط کنندگان میں سے نہیں ہے + دستخطی حصہ index:hex ہوتا ہے + دستخط کنندہ کی ایک rand-node keygen فائل یا 64 ہیکس حروف ہوتی ہے + دستخط کنندہ کا سیڈ 64 ہیکس حروف کا ہوتا ہے + expected_recipients پتوں کی فہرست ہونی چاہیے + کی فائل کی public_key وہ نہیں جو اس کا سیڈ اخذ کرتا ہے + کی فائل کا پتہ وہ نہیں جو اس کا سیڈ اخذ کرتا ہے + کوئی متوقع وصول کنندہ نہیں دیا گیا: وصول کنندہ کی جانچ نہیں ہوئی (NOT checked) + یہ تجویز کسی اور پالیسی کی ہے: اس میں %1$s ہے، جبکہ یہ پالیسی %2$s ہے + آؤٹ پٹ %1$s اس اکاؤنٹ کے طور پر نہیں کھلتا: جو قدر نظر نہیں آتی اس پر دستخط سے انکار + ویٹنس کی قدر لین دین سے مطابقت نہیں رکھتی: ان پٹ %1$s بنڈل کے نلیفائر تک نہیں پہنچتا + ویٹنس کی قدر آؤٹ پٹس سے مطابقت نہیں رکھتی: اثاثہ %1$s میں %2$s ان پٹ اور فیس و برن کے ساتھ %3$s آؤٹ پٹ ہے + تجویز %1$s پر ختم ہوتی ہے، جو اس کے بنڈل کی حد (%2$s, %3$s] سے باہر ہے + لین دین چین %1$s کے لیے ہے، تجویز چین %2$s بتاتی ہے + دستخط کنندہ %1$s کا دستخط اس تجویز کے لیے درست نہیں + دستخط کنندہ %1$s اس پالیسی میں نہیں ہے (اس میں %2$s دستخط کنندگان ہیں) + متوقع وصول کنندہ %1$s: %2$s + پالیسی پارس نہیں ہوتی: %1$s + تجویز پارس نہیں ہوتی: %1$s + تجویز کی جانچ نے ادھورا جواب لوٹایا، اس لیے اس تجویز پر دستخط نہیں کیے گئے۔ + ہر وصول کنندہ کا پتہ درج کریں: جو آؤٹ پٹ درج کیے گئے پتے سے مطابقت نہیں رکھتا، یا جس کا پتہ خالی ہے، اس پر دستخط نہیں ہو سکتے۔ + %1$s پر کسی نوڈ نے جواب نہیں دیا، اس لیے میعاد کی جانچ نہیں ہو سکتی اور اس تجویز پر دستخط نہیں کیے گئے۔ نوڈ دستیاب ہونے پر دوبارہ کوشش کریں۔ + یہ تجویز چین %1$s کے لیے ہے؛ یہ والیٹ چین %2$s پر ہے۔ + یہ تجویز اس جینیسس سے مختلف جینیسس سے بندھی ہے جو اس والیٹ کا نوڈ فراہم کرتا ہے۔ + یہ تجویز اونچائی %1$s پر ختم ہوتی ہے اور چین %2$s پر ہے: دستخط کے لیے بہت دیر ہو چکی۔ نئی تجویز مانگیں۔ + ایسی کوئی دستخط کنندہ پروفائل نہیں ہے۔ + اس دستخط کنندہ پروفائل کی اس ڈیوائس پر کوئی کی نہیں ہے: اسے ہٹا کر دوبارہ شامل کریں۔ + دستخط کنندہ کا لیبل 1-64 حروف کا ہوتا ہے + یہ دستخط کنندہ پہلے سے %1$s کے طور پر محفوظ ہے + پہلے اسکرین لاک سیٹ کریں (اور Android 10 یا پرانے پر فنگر پرنٹ بھی): دستخط کنندہ کی کلید صرف آپ کی تصدیق کے فوراً بعد استعمال ہوتی ہے۔ + اس ڈیوائس پر دستخط کنندہ کی تمام کلیدیں ہٹائی نہ جا سکیں، اس لیے والیٹ رکھا گیا۔ دوبارہ کوشش کریں۔ + ملٹی سگ دستخط کنندگان + ملٹی سگ دستخط کنندگان کا انتظام کریں + ابھی کوئی دستخط کنندہ پروفائل نہیں + نجی ملٹی سگ اکاؤنٹ کے لیے دستخط کرنے کی غرض سے پالیسی فائل اور اپنی دستخط کنندہ کی شامل کریں۔ + %2$s میں سے %1$s · دستخط کنندہ %3$s + دستخط کنندہ شامل کریں + لیبل + پالیسی فائل + دستخط کنندہ کی فائل، یا اس کا 64-ہیکس سیڈ + فائل کھولیں + جانچیں + اکاؤنٹ کا فنگر پرنٹ — محفوظ کرنے سے پہلے اسے ہر دوسرے دستخط کنندہ سے ملائیں + %2$s میں سے %1$s · یہ کی دستخط کنندہ %3$s ہے + دستخط کنندہ محفوظ کریں + دستخط کنندہ کا پروفائل اس فون کے Android Keystore میں رکھا جاتا ہے۔ ہر دستخط پر دوبارہ آپ کا فنگر پرنٹ، چہرہ یا اسکرین لاک مانگا جاتا ہے۔ + اس دستخط کنندہ کو ہٹائیں + %1$s کو ہٹائیں؟ + اس دستخط کنندہ کو دوبارہ شامل کرنے کے لیے آپ کو اس کی کلیدی فائل چاہیے ہوگی۔ + ملٹی سگ دستخط کنندہ ہٹائیں + ملٹی سگ دستخط کنندہ محفوظ کریں + اس دستخط کنندہ کی کلید اب نہیں کھلتی: محفوظ کرنے کے بعد فون کے فنگر پرنٹ یا اسکرین لاک بدل گئے۔ دستخط کنندہ کو ہٹائیں اور اس کی کلید فائل سے دوبارہ شامل کریں۔ + تجویز پر دستخط کریں + تجویز (proposal.json) + تجویز جانچیں + جانچ ہو رہی ہے… + مطلوبہ ادائیگیاں + جو بھی ادائیگی آپ کرنا چاہتے ہیں ہر ایک درج کریں: پتہ اور RAND میں رقم۔ ایک ہی پتے پر دو بار ادائیگی کل رقم کے ساتھ ایک بار درج ہوتی ہے۔ جو آؤٹ پٹ ان میں سے کسی سے مطابقت نہیں رکھتا اس پر دستخط نہیں ہو سکتے۔ + وصول کنندہ کا پتہ + رقم (RAND میں) + ہٹائیں + ادائیگی شامل کریں + ادائیگیاں جانچیں + غیر RAND ادائیگیوں کی رقم جانچی نہیں جا سکتی: ان کے لیے رقم خالی چھوڑیں۔ + یہ وصول کنندہ RAND اور ایک ٹوکن دونوں وصول کرتا ہے: ایک وصول کنندہ کو RAND اور ٹوکن کی ادائیگیوں کے لیے الگ الگ تجاویز درکار ہیں۔ + ہر ادائیگی کا پتہ درج کریں۔ + ہر ادائیگی کی رقم درج کریں: یہی ادائیگی کو بانٹنے یا پھلانے سے روکتی ہے۔ + رقم RAND کی ایک عددی مقدار ہے، جیسے 1.5۔ + یہ تجویز کیا کرتی ہے، آپ کی پالیسی کے مقابل جانچا گیا + فیس: %1$s RAND · اونچائی %2$s پر ختم + %2$s میں سے %1$s ادائیگیاں مطابقت رکھتی ہیں۔ + دوبارہ کوشش کریں + اسے ثابت کرنے کے لیے %1$s بلاک باقی ہیں۔ + اکاؤنٹ کے فنگر پرنٹ کے آخری چار حروف لکھیں + ادائیگیاں بدل گئیں: دستخط سے پہلے انہیں دوبارہ جانچیں۔ + دستخط کریں + دستخط ہو رہے ہیں… + ملٹی سگ تجویز پر دستخط کریں + آپ کا دستخطی حصہ — اسے اس کو بھیجیں جو تجویز ثابت کرتا ہے + کاپی کریں + دستخطی حصہ + ملٹی سگ دستخطی حصہ + دستخطی حصہ جانچیں + دستخطی حصہ جانچیں + دستخط کنندہ %1$s کا دستخطی حصہ درست ہے۔ %3$s میں سے %2$s ہاتھ میں ہیں: دستخط کنندگان %4$s۔ + وہ فائل کسی تجویز، پالیسی یا کی فائل کے ممکنہ سائز سے بڑی ہے۔ + وہ فائل پڑھی نہ جا سکی۔ diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml index ecebb81..de307a0 100644 --- a/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/android/app/src/main/res/values-zh-rCN/strings.xml @@ -449,4 +449,99 @@ 正在为另一笔交易生成证明。请在其完成后重试。 无法读取此兑换。未发送任何内容。 %1$s:%2$s + + 交易的哈希不是提案的绑定 + 提案的 signing_message 与其绑定的不一致 + 提案的 auth_commit 不是此策略的账户 + 提案的预期摘要与其捆绑包的不一致 + 交易不含捆绑包 + 交易带有一个动作;私密多签提案只是仅来自其捆绑包的付款 + 无法核对见证数据的金额:其 nk 或盐值不属于此提案 + 此密钥不属于该策略的签名人 + 签名份额的格式为 index:hex + 签名人密钥是 rand-node 的 keygen 文件或 64 位十六进制字符 + 签名人种子为 64 位十六进制字符 + expected_recipients 必须是地址列表 + 密钥文件中的 public_key 与其种子派生出的不一致 + 密钥文件中的地址与其种子派生出的地址不一致 + 未给出预期收款人:收款人未经核对(NOT checked) + 此提案属于另一个策略:它指向 %1$s,而此策略是 %2$s + 输出 %1$s 无法作为此账户的输出打开:拒绝对看不到的金额签名 + 见证数据的金额与交易不一致:输入 %1$s 未对应到捆绑包的 nullifier + 见证数据的金额与输出不一致:资产 %1$s 输入 %2$s,输出 %3$s(含手续费和销毁) + 提案在 %1$s 过期,超出其捆绑包的有效区间 (%2$s, %3$s] + 交易属于链 %1$s,而提案指向链 %2$s + 签名人 %1$s 的签名对此提案验证未通过 + 签名人 %1$s 不在此策略中(该策略有 %2$s 位签名人) + 预期收款人 %1$s:%2$s + 策略无法解析:%1$s + 提案无法解析:%1$s + 提案核对返回了不完整的结果,因此此提案不会签名。 + 请输入每位收款人的地址:与所输入地址不匹配、或地址为空的输出无法签名。 + %1$s 没有节点应答,因此无法核对过期时间,此提案不会签名。待节点可访问后重试。 + 此提案属于链 %1$s;此钱包在链 %2$s 上。 + 此提案绑定的创世区块与此钱包的节点所提供的不同。 + 此提案在高度 %1$s 过期,而链已到 %2$s:已来不及签名。请索取新的提案。 + 没有这个签名人资料。 + 此签名人资料在此设备上没有密钥:请移除后重新添加。 + 签名人标签为 1-64 个字符 + 此签名人已保存为 %1$s + 请先设置屏幕锁定(Android 10 或更早版本还需录入指纹):签名人密钥只在你完成验证后立即使用。 + 无法移除此设备上的全部签名人密钥,因此钱包已保留。请重试。 + 多签签名人 + 管理多签签名人 + 尚无签名人资料 + 添加策略文件和你的签名人密钥,即可为私密多签账户签名。 + %1$s/%2$s · 签名人 %3$s + 添加签名人 + 标签 + 策略文件 + 签名人密钥文件,或其 64 位十六进制种子 + 打开文件 + 核对 + 账户指纹 — 保存前请与其他每位签名人核对 + %1$s/%2$s · 此密钥是签名人 %3$s + 保存签名人 + 签名人配置保存在这部手机的 Android Keystore 中。每次签名都会再次要求指纹、面部或屏幕锁定。 + 移除此签名人 + 移除%1$s? + 要重新添加此签名人,需要他的密钥文件。 + 移除多签签名人 + 保存多签签名人 + 此签名人的密钥已无法打开:保存后手机的指纹或屏幕锁定发生了变化。请移除该签名人,并用其密钥文件重新添加。 + 签署提案 + 提案(proposal.json) + 核对提案 + 正在核对… + 拟进行的付款 + 请输入你打算进行的每一笔付款:地址和以 RAND 计的金额。向同一地址的两笔付款只输入一次,填合计金额。与其中任何一笔都不匹配的输出无法签名。 + 收款人地址 + 金额(RAND) + 移除 + 添加付款 + 核对付款 + 非 RAND 付款无法核对金额:这类付款请将金额留空。 + 此收款人同时收取 RAND 和代币:向同一收款人支付 RAND 和代币需要分开的提案。 + 请输入每笔付款的地址。 + 请输入每笔付款的金额:它可以防止拆分或虚增付款。 + 金额是 RAND 的数值,例如 1.5。 + 此提案的作用(已对照你的策略核对) + 手续费:%1$s RAND · 在高度 %2$s 过期 + %2$s 笔付款中有 %1$s 笔匹配。 + 重试 + 还差 %1$s 个区块即可证明。 + 请输入账户指纹的后四个字符 + 付款已变更:签名前请重新核对。 + 签名 + 正在签名… + 签署多签提案 + 你的签名份额 — 请发给负责证明提案的人 + 复制 + 签名份额 + 多签签名份额 + 核对签名份额 + 核对签名份额 + 签名人 %1$s 的签名份额验证通过。已收集 %2$s/%3$s:签名人 %4$s。 + 该文件大于提案、策略或密钥文件所允许的大小。 + 无法读取该文件。 diff --git a/android/app/src/main/res/values-zh-rHK/strings.xml b/android/app/src/main/res/values-zh-rHK/strings.xml index 583756d..1e4325a 100644 --- a/android/app/src/main/res/values-zh-rHK/strings.xml +++ b/android/app/src/main/res/values-zh-rHK/strings.xml @@ -449,4 +449,99 @@ 正在為另一筆交易生成證明。請在它完成後再試。 無法讀取此兌換。沒有傳送任何東西。 %1$s:%2$s + + 交易的雜湊不是提案的綁定 + 提案的 signing_message 與其綁定的不一致 + 提案的 auth_commit 不是此政策的帳戶 + 提案的預期摘要與其捆綁包的不一致 + 交易不含捆綁包 + 交易帶有一個動作;私密多重簽署提案只是僅來自其捆綁包的付款 + 無法核對見證資料的金額:其 nk 或鹽值不屬於此提案 + 此金鑰不屬於該政策的簽署人 + 簽署份額的格式為 index:hex + 簽署人金鑰是 rand-node 的 keygen 檔案或 64 位十六進制字元 + 簽署人種子為 64 位十六進制字元 + expected_recipients 必須是地址清單 + 金鑰檔案內的 public_key 與其種子衍生出的不一致 + 金鑰檔案內的地址與其種子衍生出的地址不一致 + 未提供預期收款人:收款人未經核對(NOT checked) + 此提案屬於另一個政策:它指向 %1$s,而此政策是 %2$s + 輸出 %1$s 無法作為此帳戶的輸出開啟:拒絕簽署看不到的金額 + 見證資料的金額與交易不一致:輸入 %1$s 未對應到捆綁包的 nullifier + 見證資料的金額與輸出不一致:資產 %1$s 輸入 %2$s,輸出 %3$s(含手續費及銷毀) + 提案在 %1$s 過期,超出其捆綁包的有效區間 (%2$s, %3$s] + 交易屬於鏈 %1$s,而提案指向鏈 %2$s + 簽署人 %1$s 的簽署對此提案驗證不通過 + 簽署人 %1$s 不在此政策內(該政策有 %2$s 位簽署人) + 預期收款人 %1$s:%2$s + 政策無法解析:%1$s + 提案無法解析:%1$s + 提案核對傳回不完整的結果,因此此提案不會簽署。 + 請輸入每位收款人的地址:與所輸入地址不相符、或地址為空的輸出無法簽署。 + %1$s 沒有節點回應,因此無法核對過期時間,此提案不會簽署。待節點可連線後再試。 + 此提案屬於鏈 %1$s;此錢包在鏈 %2$s 上。 + 此提案綁定的創世區塊與此錢包的節點所提供的不同。 + 此提案在高度 %1$s 過期,而鏈已去到 %2$s:已來不及簽署。請索取新的提案。 + 沒有這個簽署人資料。 + 此簽署人資料在此裝置上沒有金鑰:請移除後重新新增。 + 簽署人標籤為 1-64 個字元 + 此簽署人已儲存為 %1$s + 請先設定螢幕鎖定(Android 10 或更舊版本亦需登記指紋):簽署人密鑰只會在你完成驗證後立即使用。 + 無法移除此裝置上的全部簽署人密鑰,因此錢包已保留。請再試一次。 + 多重簽署簽署人 + 管理多重簽署簽署人 + 尚未有簽署人資料 + 新增政策檔案及你的簽署人金鑰,即可為私密多重簽署帳戶簽署。 + %1$s/%2$s · 簽署人 %3$s + 新增簽署人 + 標籤 + 政策檔案 + 簽署人金鑰檔案,或其 64 位十六進制種子 + 開啟檔案 + 核對 + 帳戶指紋 — 儲存前請與其他每位簽署人核對 + %1$s/%2$s · 此金鑰是簽署人 %3$s + 儲存簽署人 + 簽署人設定檔保存在這部手機的 Android Keystore 中。每次簽署都會再次要求指紋、面孔或螢幕鎖定。 + 移除此簽署人 + 移除%1$s? + 要重新新增此簽署人,需要他的密鑰檔案。 + 移除多重簽署簽署人 + 儲存多重簽署簽署人 + 此簽署人的密鑰已無法開啟:儲存後手機的指紋或螢幕鎖定有所變更。請移除該簽署人,並用其密鑰檔案重新新增。 + 簽署提案 + 提案(proposal.json) + 核對提案 + 正在核對… + 擬進行的付款 + 請輸入你打算進行的每一筆付款:地址及以 RAND 計的金額。向同一地址的兩筆付款只輸入一次,填合計金額。與其中任何一筆都不相符的輸出無法簽署。 + 收款人地址 + 金額(RAND) + 移除 + 新增付款 + 核對付款 + 非 RAND 付款無法核對金額:該類付款請將金額留空。 + 此收款人同時收取 RAND 及代幣:向同一收款人支付 RAND 及代幣,需要分開的提案。 + 請輸入每筆付款的地址。 + 請輸入每筆付款的金額:它可防止拆分或虛增付款。 + 金額是 RAND 的數值,例如 1.5。 + 此提案的作用(已對照你的政策核對) + 手續費:%1$s RAND · 於高度 %2$s 過期 + %2$s 筆付款中有 %1$s 筆相符。 + 重試 + 尚欠 %1$s 個區塊便可證明。 + 請輸入帳戶指紋的最後四個字元 + 付款已更改:簽署前請重新核對。 + 簽署 + 正在簽署… + 簽署多重簽署提案 + 你的簽署份額 — 請交給負責證明提案的人 + 複製 + 簽署份額 + 多重簽署簽署份額 + 核對簽署份額 + 核對簽署份額 + 簽署人 %1$s 的簽署份額驗證通過。已收集 %2$s/%3$s:簽署人 %4$s。 + 該檔案大於提案、政策或金鑰檔案所容許的大小。 + 無法讀取該檔案。 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 896928f..9197342 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -509,4 +509,102 @@ This swap cannot be read. Nothing was sent. %1$s: %2$s + + the transaction does not hash to the proposal\'s binding + the proposal\'s signing_message is not its binding\'s + the proposal\'s auth_commit is not this policy\'s account + the proposal\'s expected digest is not its bundle\'s + the transaction carries no bundle + the transaction carries an action; a private multisig proposal is a payment from its bundle alone + the witness\'s value cannot be checked: its nk or salt is not this proposal\'s + this key is not one of the policy\'s signers + a share is index:hex + a signer key is a rand-node keygen file or 64 hex characters + a signer seed is 64 hex characters + expected_recipients must be a list of addresses + the key file\'s public_key is not the one its seed derives + the key file\'s address is not the one its seed derives + no expected recipient given: the payee was NOT checked + this proposal is for another policy: it names %1$s, this policy is %2$s + output %1$s does not open as this account\'s: refusing to sign value it cannot see + the witness\'s value does not match the transaction: input %1$s does not nullify to the bundle\'s nullifier + the witness\'s value does not match the outputs: asset %1$s has %2$s in and %3$s out with the fee and burns + the proposal expires at %1$s, outside its bundle\'s window (%2$s, %3$s] + the transaction is for chain %1$s, the proposal says chain %2$s + signer %1$s\'s signature does not verify over this proposal + signer %1$s is not in this policy (it has %2$s signers) + expected recipient %1$s: %2$s + the policy does not parse: %1$s + the proposal does not parse: %1$s + + The proposal check returned an incomplete answer, so this proposal is not signed. + Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed. + No node answered at %1$s, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable. + This proposal is for chain %1$s; this wallet is on chain %2$s. + This proposal is bound to another genesis than the one this wallet\'s node serves. + This proposal expires at height %1$s and the chain is at %2$s: too late to sign. Ask for a new proposal. + No such signer profile. + This signer profile has no key on this device: remove it and add it again. + a signer label is 1-64 characters + this signer is already saved as %1$s + Set a screen lock (and on Android 10 or older, a fingerprint) first: a signer key is used only right after you authenticate. + The signer keys on this device could not all be removed, so the wallet was kept. Try again. + + Multisig signers + Manage multisig signers + No signer profiles yet + Add the policy file and your signer key to sign for a private multisig account. + %1$s of %2$s · signer %3$s + Add a signer + Label + Policy file + Signer key file, or its 64-hex seed + Open file + Check + Account fingerprint — compare it with every other signer before you save + %1$s of %2$s · this key is signer %3$s + Save signer + A signer profile is kept in this phone\'s Android Keystore. Every signature asks for your fingerprint, face or screen lock again. + Remove this signer + Remove %1$s? + You need the signer\'s key file to add this signer back. + Remove a multisig signer + Save a multisig signer + This signer\'s key no longer opens: the phone\'s fingerprints or screen lock changed since it was saved. Remove the signer and add it again from its key file. + Sign a proposal + Proposal (proposal.json) + Check proposal + Checking… + Intended payments + Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed. + Payee address + Amount in RAND + Remove + Add payment + Check payments + Amounts cannot be checked for non-RAND payments: leave the amount empty for those. + This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals. + Enter the address of every payment. + Enter the amount of every payment: it is what stops a split or an inflated payment. + An amount is a number of RAND, such as 1.5. + What this proposal does, as checked against your policy + Fee: %1$s RAND · expires at height %2$s + %1$s of %2$s payments matched. + Retry + %1$s blocks left to prove it. + Type the last four characters of the account fingerprint + Payments changed: check them again before signing. + Sign + Signing… + Sign a multisig proposal + Your share — send it to whoever proves the proposal + Copy + Share + Multisig share + Check a share + index:hex + Check share + Signer %1$s\'s share verifies. %2$s of %3$s in hand: signers %4$s. + That file is larger than a proposal, a policy or a key file can be. + That file could not be read. diff --git a/android/app/src/test/java/org/randprotocol/wallet/core/CoreMsauthTest.java b/android/app/src/test/java/org/randprotocol/wallet/core/CoreMsauthTest.java new file mode 100644 index 0000000..8fad886 --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/core/CoreMsauthTest.java @@ -0,0 +1,31 @@ +package org.randprotocol.wallet.core; + +import static org.junit.Assert.assertEquals; + +import org.json.JSONObject; +import org.junit.Test; + +import java.util.List; + +/** The five {@code msauth_*} calls' parameters, named as wallet-core's dispatch reads them (the web's coreApi test). */ +public class CoreMsauthTest { + private static void same(String json, JSONObject got) throws Exception { + JSONObject want = new JSONObject(json); + assertEquals(got.toString(), want.length(), got.length()); + for (java.util.Iterator it = want.keys(); it.hasNext(); ) { + String k = it.next(); + assertEquals(k, String.valueOf(want.get(k)), String.valueOf(got.get(k))); + } + } + + @Test + public void theParamsAreTheCoresNames() throws Exception { + same("{\"proposal\":\"P\",\"policy\":\"Q\",\"expected_recipients\":[\"rand1a:10\"]}", Core.msauthCheckParams("P", "Q", List.of("rand1a:10"))); + same("{\"proposal\":\"P\",\"policy\":\"Q\",\"expected_recipients\":[]}", Core.msauthCheckParams("P", "Q", List.of())); + same("{\"proposal\":\"P\",\"policy\":\"Q\",\"seed\":\"S\",\"expected_recipients\":[\"rand1a:10\"]}", Core.msauthSignParams("P", "Q", "S", List.of("rand1a:10"))); + same("{\"proposal\":\"P\",\"policy\":\"Q\",\"share\":\"0:aa\"}", Core.msauthVerifyShareParams("P", "Q", "0:aa")); + same("{\"input\":\"K\",\"policy\":\"Q\"}", Core.msauthSignerImportParams("K", "Q")); + same("{\"input\":\"K\"}", Core.msauthSignerImportParams("K", null)); + same("{\"policy\":\"Q\"}", Core.msauthPolicyInfoParams("Q")); + } +} diff --git a/android/app/src/test/java/org/randprotocol/wallet/security/PrefsMigrationTest.java b/android/app/src/test/java/org/randprotocol/wallet/security/PrefsMigrationTest.java new file mode 100644 index 0000000..aa20457 --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/security/PrefsMigrationTest.java @@ -0,0 +1,88 @@ +package org.randprotocol.wallet.security; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.SharedPreferences; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * The one-time chain-id migration (wallet v0.8.0 runs chain 21 onward): a saved 20 becomes 21 on + * the first launch, once, and nothing else changes. On the JVM, with a map for the preferences. + */ +public class PrefsMigrationTest { + /** SharedPreferences over a map: enough for {@link Prefs}. */ + static final class MapPrefs implements SharedPreferences { + final Map m = new HashMap<>(); + + public Map getAll() { return new HashMap<>(m); } + public String getString(String k, String d) { return m.containsKey(k) ? (String) m.get(k) : d; } + @SuppressWarnings("unchecked") + public Set getStringSet(String k, Set d) { return m.containsKey(k) ? (Set) m.get(k) : d; } + public int getInt(String k, int d) { return m.containsKey(k) ? (Integer) m.get(k) : d; } + public long getLong(String k, long d) { return m.containsKey(k) ? (Long) m.get(k) : d; } + public float getFloat(String k, float d) { return m.containsKey(k) ? (Float) m.get(k) : d; } + public boolean getBoolean(String k, boolean d) { return m.containsKey(k) ? (Boolean) m.get(k) : d; } + public boolean contains(String k) { return m.containsKey(k); } + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener l) {} + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener l) {} + + public Editor edit() { + Map pending = new HashMap<>(); + Set removed = new java.util.HashSet<>(); + return new Editor() { + public Editor putString(String k, String v) { pending.put(k, v); return this; } + public Editor putStringSet(String k, Set v) { pending.put(k, v); return this; } + public Editor putInt(String k, int v) { pending.put(k, v); return this; } + public Editor putLong(String k, long v) { pending.put(k, v); return this; } + public Editor putFloat(String k, float v) { pending.put(k, v); return this; } + public Editor putBoolean(String k, boolean v) { pending.put(k, v); return this; } + public Editor remove(String k) { removed.add(k); return this; } + public Editor clear() { removed.addAll(m.keySet()); return this; } + public boolean commit() { for (String k : removed) m.remove(k); m.putAll(pending); return true; } + public void apply() { commit(); } + }; + } + } + + @Test + public void aSavedTwentyBecomesTwentyOneAndNothingElseChanges() { + MapPrefs sp = new MapPrefs(); + sp.m.put("chain_id", 20L); + sp.m.put("rpc_url", "http://10.0.0.5:8545"); + sp.m.put("auto_lock_minutes", 5); + Prefs prefs = new Prefs(sp); + assertEquals(21L, prefs.chainId()); + assertEquals("the RPC URL is not touched", "http://10.0.0.5:8545", prefs.rpcUrl()); + assertEquals(5, prefs.autoLockMinutes()); + assertTrue(sp.getBoolean(Prefs.CHAIN_MIGRATION_KEY, false)); + assertEquals(4, sp.m.size()); + } + + @Test + public void itRunsOnce() { + MapPrefs sp = new MapPrefs(); + sp.m.put("chain_id", 20L); + new Prefs(sp).setChainId(20); // the user chose 20 again afterwards + assertEquals("a later launch leaves the user's choice alone", 20L, new Prefs(sp).chainId()); + } + + @Test + public void otherValuesAndNoValueAreLeftAlone() { + MapPrefs sp = new MapPrefs(); + sp.m.put("chain_id", 13L); + assertEquals(13L, new Prefs(sp).chainId()); + + MapPrefs empty = new MapPrefs(); + new Prefs(empty); + assertFalse("no saved chain id: none is written", empty.contains("chain_id")); + assertEquals(1, empty.m.size()); + assertEquals(Prefs.DEFAULT_CHAIN_ID, new Prefs(empty).chainId()); + } +} diff --git a/android/app/src/test/java/org/randprotocol/wallet/security/SignerProfilesTest.java b/android/app/src/test/java/org/randprotocol/wallet/security/SignerProfilesTest.java new file mode 100644 index 0000000..698baa0 --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/security/SignerProfilesTest.java @@ -0,0 +1,348 @@ +package org.randprotocol.wallet.security; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.biometric.BiometricManager; + +import org.junit.Test; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * Signer profiles beside the spend key (spec §3), the per-use key's API-level rule, wiping them + * with the wallet, and reading a seed through an authenticated cipher — on the JVM, with a map for + * the encrypted preferences, a list for the Keystore's aliases and a software AES key for the + * Keystore's (which a unit test cannot reach). + */ +public class SignerProfilesTest { + static class MapKv implements SignerProfiles.KeyValue { + final Map m = new HashMap<>(); + String failOn; + + public String get(String k) { return m.get(k); } + + public void put(String k, String v) { + if (k.equals(failOn)) throw new IllegalStateException("disk full"); + m.put(k, v); + } + + public void remove(String k) { + if (k.equals(failOn)) throw new IllegalStateException("disk full"); + m.remove(k); + } + + public Set keys() { return new LinkedHashSet<>(m.keySet()); } + } + + static final class FakeAliases implements SignerProfiles.KeyAliases { + final List aliases = new ArrayList<>(); + final List deleted = new ArrayList<>(); + boolean failList; + String failDelete; + + public List list() throws Exception { + if (failList) throw new java.security.KeyStoreException("keystore busy"); + return new ArrayList<>(aliases); + } + + public void delete(String alias) throws Exception { + if (alias.equals(failDelete)) throw new java.security.KeyStoreException("cannot delete " + alias); + aliases.remove(alias); + deleted.add(alias); + } + } + + static SignerProfiles.Profile profile(String id, String label, int index) { + return new SignerProfiles.Profile(id, label, index, "33".repeat(32)); + } + + @Test public void addListRemoveKeepsBothHalvesUnderTheirNames() throws Exception { + MapKv kv = new MapKv(); + SignerProfiles p = new SignerProfiles(kv); + p.add(profile("a1", "treasury", 2), "{policy}", "iv:ct"); + assertEquals(1, p.list().size()); + assertEquals("treasury", p.list().get(0).label); + assertEquals(2, p.list().get(0).index); + assertEquals("{policy}", kv.get("msauth.a1.policy")); + assertEquals("iv:ct", kv.get("msauth.a1.seed")); + assertEquals("{policy}", p.policy("a1")); + assertEquals("iv:ct", p.sealedSeed("a1")); + try { + p.add(profile("b2", "again", 2), "{policy}", "x:y"); + fail("duplicate"); + } catch (IllegalStateException e) { + assertEquals("this signer is already saved as treasury", e.getMessage()); + } + assertNull("a refused add leaves nothing", kv.get("msauth.b2.seed")); + p.add(profile("c3", "other position", 1), "{policy}", "x:y"); + assertEquals(2, p.list().size()); + p.remove("a1"); + assertNull(kv.get("msauth.a1.policy")); + assertNull(kv.get("msauth.a1.seed")); + assertEquals(1, p.list().size()); + assertEquals("c3", p.list().get(0).id); + } + + @Test public void removingAProfileDeletesItsKeystoreKey() throws Exception { + MapKv kv = new MapKv(); + FakeAliases ks = new FakeAliases(); + ks.aliases.addAll(List.of("msauth.a1", "msauth.c3")); + SignerProfiles p = new SignerProfiles(kv, ks); + p.add(profile("a1", "treasury", 2), "{policy}", "iv:ct"); + p.remove("a1"); + assertEquals(List.of("msauth.a1"), ks.deleted); + assertEquals(List.of("msauth.c3"), ks.aliases); + } + + @Test public void aLabelIsOneToSixtyFourCodePoints() throws Exception { + SignerProfiles p = new SignerProfiles(new MapKv()); + String emoji = "🔐"; // one code point, two chars + p.add(profile("a1", emoji.repeat(64), 0), "{policy}", "iv:ct"); + assertEquals(64, p.list().get(0).label.codePointCount(0, p.list().get(0).label.length())); + p.add(profile("a2", " trimmed ", 1), "{policy}", "iv:ct"); + assertEquals("trimmed", p.list().get(1).label); + for (String bad : new String[] {emoji.repeat(65), "x".repeat(65), "", " ", null}) { + try { + p.add(profile("b" + (bad == null ? 0 : bad.length()), bad, 5), "{policy}", "iv:ct"); + fail("label " + bad); + } catch (IllegalArgumentException e) { + assertEquals("a signer label is 1-64 characters", e.getMessage()); + } + } + assertEquals(2, p.list().size()); + } + + @Test public void anIdIsAKeystoreSafeName() throws Exception { + SignerProfiles p = new SignerProfiles(new MapKv()); + for (String bad : new String[] {"", "a.b", "A1", "../x", "x".repeat(65)}) { + try { + p.add(profile(bad, "l", 0), "{policy}", "iv:ct"); + fail("id " + bad); + } catch (IllegalArgumentException expected) { + // refused + } + } + String id = SignerProfiles.newId(); + assertTrue(id.matches("[0-9a-f]{32}")); + p.add(profile(id, "l", 0), "{policy}", "iv:ct"); + } + + @Test public void aFailedAddLeavesNothingBehind() throws Exception { + MapKv kv = new MapKv(); + SignerProfiles p = new SignerProfiles(kv); + kv.failOn = "msauth.index"; + try { + p.add(profile("a1", "treasury", 2), "{policy}", "iv:ct"); + fail(); + } catch (IllegalStateException expected) { + // the index could not be written + } + kv.failOn = null; + assertTrue(kv.m.isEmpty()); + assertTrue(p.list().isEmpty()); + } + + @Test public void anUnreadableIndexListsNothing() { + MapKv kv = new MapKv(); + kv.m.put("msauth.index", "not json"); + assertTrue(new SignerProfiles(kv).list().isEmpty()); + } + + // ------------------------------------------------------------------ forgetting the wallet + + static MapKv walletWithSigners() throws Exception { + MapKv kv = new MapKv(); + kv.m.put("spend_key", "5a".repeat(32)); + kv.m.put("prover_pairing", "{\"token\":1}"); + kv.m.put("prover_token", "legacy"); + SignerProfiles p = new SignerProfiles(kv); + p.add(profile("a1", "treasury", 2), "{policy}", "iv:ct"); + p.add(profile("c3", "ops", 1), "{policy}", "iv:ct"); + // A profile the index no longer lists (a crash between the seed's save and the index's). + kv.m.put("msauth.orphan.seed", "iv:ct"); + kv.m.put("msauth.orphan.policy", "{policy}"); + return kv; + } + + static FakeAliases keystore() { + FakeAliases ks = new FakeAliases(); + ks.aliases.addAll(List.of("_androidx_security_master_key_", "msauth.a1", "msauth.c3", "msauth.orphan", "other_app_key")); + return ks; + } + + @Test public void eraseSweepsEveryProfileAndKeyByPrefixAndNothingElse() throws Exception { + MapKv kv = walletWithSigners(); + FakeAliases ks = keystore(); + SignerProfiles.eraseAll(kv, ks); + assertEquals(Set.of("spend_key", "prover_pairing", "prover_token"), kv.m.keySet()); + assertEquals(List.of("_androidx_security_master_key_", "other_app_key"), ks.aliases); + assertTrue(new SignerProfiles(kv).list().isEmpty()); + } + + @Test public void theSweepNeverTouchesTheSpendKeyOrTheProverByName() { + for (String kept : new String[] {"spend_key", "prover_pairing", "prover_token", "_androidx_security_master_key_", "msauth", "msauthx.1"}) { + assertFalse(kept, SignerProfiles.isSignerEntry(kept)); + assertFalse(kept, SignerProfiles.isSignerAlias(kept)); + } + assertTrue(SignerProfiles.isSignerEntry("msauth.a1.seed")); + assertTrue(SignerProfiles.isSignerEntry("msauth.index")); + assertTrue(SignerProfiles.isSignerAlias("msauth.a1")); + } + + @Test public void aFailedSweepKeepsTheIndex() throws Exception { + // The Keystore cannot list: nothing is removed. + MapKv kv = walletWithSigners(); + FakeAliases ks = keystore(); + ks.failList = true; + Map before = new HashMap<>(kv.m); + try { + SignerProfiles.eraseAll(kv, ks); + fail(); + } catch (Exception expected) { + assertTrue(expected.getMessage().contains("keystore busy")); + } + assertEquals(before, kv.m); + // One key cannot be deleted: the index stays, so a retry finds what is left. + kv = walletWithSigners(); + ks = keystore(); + ks.failDelete = "msauth.c3"; + try { + SignerProfiles.eraseAll(kv, ks); + fail(); + } catch (Exception expected) { + // thrown + } + assertNotNull(kv.get("msauth.index")); + assertEquals(2, new SignerProfiles(kv).list().size()); + ks.failDelete = null; + SignerProfiles.eraseAll(kv, ks); + assertEquals(Set.of("spend_key", "prover_pairing", "prover_token"), kv.m.keySet()); + // An entry cannot be removed: the index stays. + kv = walletWithSigners(); + kv.failOn = "msauth.orphan.seed"; + try { + SignerProfiles.eraseAll(kv, keystore()); + fail(); + } catch (IllegalStateException expected) { + // thrown + } + assertNotNull(kv.get("msauth.index")); + } + + // ------------------------------------------------------------------ the per-use key + + @Test public void aSealedSeedRoundTrips() { + byte[][] parts = KeystoreSeedCipher.unseal(KeystoreSeedCipher.seal(new byte[] {1, 2}, new byte[] {3, 4, 5})); + assertArrayEquals(new byte[] {1, 2}, parts[0]); + assertArrayEquals(new byte[] {3, 4, 5}, parts[1]); + for (String bad : new String[] {"", "abc", ":", "AQI=:", ":AQI=", "!!:AQI="}) { + try { + KeystoreSeedCipher.unseal(bad); + fail(bad); + } catch (IllegalArgumentException expected) { + // refused + } + } + } + + @Test public void beforeAndroid11ASignerNeedsABiometric() { + // API 26-29: per-use auth with a CryptoObject exists only for biometrics. + for (int sdk = 26; sdk <= 29; sdk++) { + assertFalse(KeystoreSeedCipher.credentialPerUse(sdk)); + assertEquals(BiometricManager.Authenticators.BIOMETRIC_STRONG, KeystoreSeedCipher.promptAuthenticators(sdk)); + assertNull(KeystoreSeedCipher.refusal(sdk, true, true)); + assertNull(KeystoreSeedCipher.refusal(sdk, true, false)); + assertEquals("a screen lock alone is not enough on API " + sdk, + "Set a screen lock (and on Android 10 or older, a fingerprint) first: a signer key is used only right after you authenticate.", + KeystoreSeedCipher.refusal(sdk, false, true)); + assertNotNull(KeystoreSeedCipher.refusal(sdk, false, false)); + } + for (int sdk : new int[] {30, 33, 36}) { + assertTrue(KeystoreSeedCipher.credentialPerUse(sdk)); + assertEquals(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL, + KeystoreSeedCipher.promptAuthenticators(sdk)); + assertNull(KeystoreSeedCipher.refusal(sdk, false, true)); + assertNull(KeystoreSeedCipher.refusal(sdk, true, false)); + assertNotNull(KeystoreSeedCipher.refusal(sdk, false, false)); + } + } + + @Test public void aSeedIsReadThroughTheAuthenticatedCipherOnly() throws Exception { + KeyGenerator g = KeyGenerator.getInstance("AES"); + g.init(256); + SecretKey key = g.generateKey(); + byte[] seed = new byte[32]; + new SecureRandom().nextBytes(seed); + Cipher enc = Cipher.getInstance("AES/GCM/NoPadding"); + enc.init(Cipher.ENCRYPT_MODE, key); + String sealed = KeystoreSeedCipher.seal(enc.getIV(), enc.doFinal(seed)); + + MapKv kv = new MapKv(); + SignerProfiles profiles = new SignerProfiles(kv); + profiles.add(profile("a1", "treasury", 2), "{policy}", sealed); + List asked = new ArrayList<>(); + KeystoreSeedSource src = new KeystoreSeedSource(profiles, (id, iv) -> { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + asked.add("cipher " + id); + return c; + }, cipher -> { + asked.add("prompt"); + return cipher; + }); + assertArrayEquals(seed, src.seed("a1")); + assertArrayEquals(seed, src.seed("a1")); + assertEquals("one prompt per read", List.of("cipher a1", "prompt", "cipher a1", "prompt"), asked); + try { + src.seed("nope"); + fail(); + } catch (IllegalStateException e) { + assertEquals("This signer profile has no key on this device: remove it and add it again.", e.getMessage()); + } + // A prompt that is cancelled or fails: no seed. + KeystoreSeedSource refused = new KeystoreSeedSource(profiles, (id, iv) -> Cipher.getInstance("AES/GCM/NoPadding"), cipher -> { + throw new java.util.concurrent.CancellationException("cancelled"); + }); + try { + refused.seed("a1"); + fail(); + } catch (java.util.concurrent.CancellationException expected) { + // no seed + } + // A sealed seed that does not open under its key (another key, a tampered ct): no seed. + SignerProfiles other = new SignerProfiles(new MapKv()); + Cipher enc2 = Cipher.getInstance("AES/GCM/NoPadding"); + KeyGenerator g2 = KeyGenerator.getInstance("AES"); + g2.init(256); + enc2.init(Cipher.ENCRYPT_MODE, g2.generateKey()); + other.add(profile("a1", "treasury", 2), "{policy}", KeystoreSeedCipher.seal(enc2.getIV(), enc2.doFinal(seed))); + KeystoreSeedSource wrongKey = new KeystoreSeedSource(other, (id, iv) -> { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + return c; + }, cipher -> cipher); + try { + wrongKey.seed("a1"); + fail(); + } catch (javax.crypto.AEADBadTagException expected) { + // the tag does not verify + } + } +} diff --git a/android/app/src/test/java/org/randprotocol/wallet/ui/CoreErrorsTest.java b/android/app/src/test/java/org/randprotocol/wallet/ui/CoreErrorsTest.java index 7bde81c..c63979d 100644 --- a/android/app/src/test/java/org/randprotocol/wallet/ui/CoreErrorsTest.java +++ b/android/app/src/test/java/org/randprotocol/wallet/ui/CoreErrorsTest.java @@ -21,4 +21,49 @@ public void englishIsTheInput() { assertEquals(null, CoreErrors.translate(null)); assertEquals("", CoreErrors.translate("")); } + + /** The private-multisig refusals (Task 5's table, ui/engine/core-errors.js), mapped and unchanged in English. */ + @Test + public void theMultisigSentencesAreMappedAndEnglishIsTheInput() { + String[] exact = { + "the transaction does not hash to the proposal's binding", + "the proposal's signing_message is not its binding's", + "the proposal's auth_commit is not this policy's account", + "the proposal's expected digest is not its bundle's", + "the transaction carries no bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone", + "the witness's value cannot be checked: its nk or salt is not this proposal's", + "this key is not one of the policy's signers", + "a share is index:hex", + "a signer key is a rand-node keygen file or 64 hex characters", + "a signer seed is 64 hex characters", + "expected_recipients must be a list of addresses", + "the key file's public_key is not the one its seed derives", + "the key file's address is not the one its seed derives", + "no expected recipient given: the payee was NOT checked", + }; + String[] patterned = { + "this proposal is for another policy: it names ab12, this policy is cd34", + "output 2 does not open as this account's: refusing to sign value it cannot see", + "the witness's value does not match the transaction: input 1 does not nullify to the bundle's nullifier", + "the witness's value does not match the outputs: asset 0 has 25 in and 24 out with the fee and burns", + "the proposal expires at 9000, outside its bundle's window (100, 2124]", + "the transaction is for chain 7, the proposal says chain 21", + "signer 1's signature does not verify over this proposal", + "signer 4 is not in this policy (it has 3 signers)", + "expected recipient rand1abc: shielded address is not base58", + "the policy does not parse: expected value at line 1 column 1", + "the proposal does not parse: EOF while parsing", + }; + for (String m : exact) { + assertEquals(m, true, CoreErrors.knows(m)); + same(m); + } + for (String m : patterned) { + assertEquals(m, true, CoreErrors.knows(m)); + same(m); + } + // The crate's payee refusals are shown verbatim, by design (Task 3). + same("output 2 pays b9856a063c1fb742, not an expected recipient"); + } } diff --git a/android/app/src/test/java/org/randprotocol/wallet/ui/RemoveAuthTest.java b/android/app/src/test/java/org/randprotocol/wallet/ui/RemoveAuthTest.java new file mode 100644 index 0000000..e0aa057 --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/ui/RemoveAuthTest.java @@ -0,0 +1,69 @@ +package org.randprotocol.wallet.ui; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import androidx.biometric.BiometricManager; + +import org.junit.Test; + +/** + * How removing a signer asks for the device's authentication ({@link RemoveAuth}): never skipped on + * a device with a secure lock, whatever BiometricManager answers; a confirm-credential screen + * where BiometricPrompt cannot ask. + */ +public class RemoveAuthTest { + static final int WEAK = BiometricManager.Authenticators.BIOMETRIC_WEAK; + static final int CRED = BiometricManager.Authenticators.DEVICE_CREDENTIAL; + static final int[] RESULTS = { + BiometricManager.BIOMETRIC_SUCCESS, + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED, + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED, + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, + BiometricManager.BIOMETRIC_STATUS_UNKNOWN, + }; + + @Test public void theAuthenticatorsAskedForPerApiLevel() { + assertEquals(WEAK, RemoveAuth.authenticators(28)); + assertEquals(WEAK, RemoveAuth.authenticators(29)); + assertEquals(WEAK | CRED, RemoveAuth.authenticators(30)); + assertEquals(WEAK | CRED, RemoveAuth.authenticators(34)); + } + + @Test public void onlyADeviceWithoutASecureLockSkips() { + for (int sdk : new int[] {26, 28, 29, 30, 34}) { + for (int r : RESULTS) { + assertEquals(RemoveAuth.Kind.NONE, RemoveAuth.choose(sdk, false, r).kind); + assertNotEquals("sdk " + sdk + " result " + r, RemoveAuth.Kind.NONE, RemoveAuth.choose(sdk, true, r).kind); + } + } + } + + @Test public void belowApi30AWeakBiometricPromptsWithACancelButtonElseTheCredentialScreen() { + for (int sdk : new int[] {28, 29}) { + RemoveAuth ok = RemoveAuth.choose(sdk, true, BiometricManager.BIOMETRIC_SUCCESS); + assertEquals(RemoveAuth.Kind.PROMPT, ok.kind); + assertEquals(WEAK, ok.authenticators); + assertTrue(ok.negativeButton); + for (int r : RESULTS) { + if (r == BiometricManager.BIOMETRIC_SUCCESS) continue; + assertEquals("sdk " + sdk + " result " + r, RemoveAuth.Kind.CONFIRM_CREDENTIAL, RemoveAuth.choose(sdk, true, r).kind); + } + } + } + + @Test public void fromApi30ThePromptWithTheCredentialElseTheCredentialScreen() { + RemoveAuth ok = RemoveAuth.choose(30, true, BiometricManager.BIOMETRIC_SUCCESS); + assertEquals(RemoveAuth.Kind.PROMPT, ok.kind); + assertEquals(WEAK | CRED, ok.authenticators); + assertFalse("DEVICE_CREDENTIAL takes no negative button", ok.negativeButton); + for (int r : RESULTS) { + if (r == BiometricManager.BIOMETRIC_SUCCESS) continue; + assertEquals("result " + r, RemoveAuth.Kind.CONFIRM_CREDENTIAL, RemoveAuth.choose(30, true, r).kind); + } + } +} diff --git a/android/app/src/test/java/org/randprotocol/wallet/ui/SignScreenTest.java b/android/app/src/test/java/org/randprotocol/wallet/ui/SignScreenTest.java new file mode 100644 index 0000000..4649dba --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/ui/SignScreenTest.java @@ -0,0 +1,489 @@ +package org.randprotocol.wallet.ui; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.randprotocol.wallet.ui.SignRows.Row; +import org.randprotocol.wallet.wallet.SignFlow; +import org.randprotocol.wallet.wallet.SignFlow.Verdict; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The sign screen's decisions, apart from the activity (iOS SignRows / PaymentsGate / + * CheckSession / SignProposalModel): the payment rows to the core's entries, the stale gate, + * which check's result lands, the last-four confirmation, and each refusal's sentence. The + * summaries are the vendored fullnode vectors (ui/test/fixtures/msauth). + */ +public class SignScreenTest { + static final String GENESIS = "0".repeat(64); + static final String URL = "https://rpc.example.org"; + + static JSONObject fixture() throws Exception { + try (InputStream in = SignScreenTest.class.getResourceAsStream("/expected.json")) { + return new JSONObject(new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } + + static String recipient() throws Exception { + return fixture().getString("recipient"); + } + + /** msauth_check's reply for the valid proposal checked with the payee at 10 RAND. */ + static JSONObject summary() throws Exception { + JSONObject f = fixture(); + return f.getJSONObject("summary_expecting_recipient") + .put("binding_domain", 0) + .put("lines", f.getJSONArray("lines_expecting_recipient")) + .put("expected", new JSONArray().put(new JSONObject().put("entry", recipient() + ":10").put("fingerprint", f.getString("recipient_owner_fingerprint")))); + } + + static List payees() throws Exception { + return List.of(recipient() + ":10"); + } + + static SignFlow.Judgement judge(Object summary, Long head) throws Exception { + return SignFlow.judge(summary, head, 7, GENESIS, URL, payees()); + } + + static SignFlow.Judgement signable() throws Exception { + return judge(summary(), 2000L); + } + + // ------------------------------------------------------------------ rows to entries + + @Test public void aRowIsAddrColonAmountAsTyped() throws Exception { + assertEquals("rand1abc:1.5", SignRows.entry(new Row(" rand1abc ", " 1.5 "), false)); + assertEquals("rand1abc:10", SignRows.entry(new Row("rand1abc", "10"), true)); + } + + @Test public void aRowWithoutAnAddressOrAmountIsRefusedWithItsSentence() { + assertRefused(new Row("", "1"), false, "Enter the address of every payment."); + assertRefused(new Row(" ", "1"), true, "Enter the address of every payment."); + assertRefused(new Row("rand1abc", ""), false, "Enter the amount of every payment: it is what stops a split or an inflated payment."); + assertRefused(new Row("rand1abc", "abc"), false, "An amount is a number of RAND, such as 1.5."); + assertRefused(new Row("rand1abc", "0"), false, "An amount is a number of RAND, such as 1.5."); + assertRefused(new Row("rand1abc", "0.0000000001"), false, "An amount is a number of RAND, such as 1.5."); + assertRefused(new Row("rand1abc", "-1"), true, "An amount is a number of RAND, such as 1.5."); + } + + @Test public void theAddressAloneOnlyWhereTheAmountIsOptional() throws Exception { + assertEquals("rand1abc", SignRows.entry(new Row("rand1abc", " "), true)); + assertRefused(new Row("rand1abc", ""), false, "Enter the amount of every payment: it is what stops a split or an inflated payment."); + } + + static void assertRefused(Row r, boolean optional, String sentence) { + try { + SignRows.entry(r, optional); + fail("accepted " + r.addr + " / " + r.amount); + } catch (IllegalArgumentException e) { + assertEquals(sentence, e.getMessage()); + } + } + + /** A RAND-only proposal: no row may go without its amount, whatever its address. */ + @Test public void anAmountIsRequiredWhenEveryPayingOutputIsRand() throws Exception { + JSONObject s = summary(); + assertFalse(SignRows.amountOptional(new Row(recipient(), ""), s)); + assertFalse(SignRows.amountOptional(new Row("rand1unknown", ""), s)); + assertFalse(SignRows.amountOptional(new Row(recipient(), ""), null)); + } + + /** A token output: the row whose fingerprint owns it may go address-only; a RAND payee's row may not. */ + @Test public void theAmountIsOptionalOnlyOnTheRowWhoseFingerprintOwnsATokenOutput() throws Exception { + JSONObject s = summary(); + JSONObject token = new JSONObject().put("amount", 5).put("asset", 3).put("to_account", false).put("matches", "X").put("owner_fingerprint", "tokenowner"); + s.getJSONArray("outputs").put(token); + s.getJSONArray("expected").put(new JSONObject().put("entry", "rand1token").put("fingerprint", "tokenowner")); + assertTrue(SignRows.amountOptional(new Row("rand1token", ""), s)); + assertTrue(SignRows.amountOptional(new Row(" rand1token ", "1"), s)); + assertFalse("the RAND payee keeps its pin", SignRows.amountOptional(new Row(recipient(), ""), s)); + // Not fingerprinted yet: may be sent address-only so the next check can tell. + assertTrue(SignRows.amountOptional(new Row("rand1new", ""), s)); + assertFalse(SignRows.amountOptional(new Row("", ""), new JSONObject().put("outputs", new JSONArray()).put("expected", new JSONArray()))); + } + + @Test public void theEntriesAreBuiltPerRowWithEachRowsOwnRule() throws Exception { + JSONObject s = summary(); + s.getJSONArray("outputs").put(new JSONObject().put("amount", 5).put("asset", 3).put("to_account", false).put("owner_fingerprint", "tokenowner")); + s.getJSONArray("expected").put(new JSONObject().put("entry", "rand1token").put("fingerprint", "tokenowner")); + List rows = List.of(new Row(recipient(), "10"), new Row("rand1token", "")); + assertEquals(List.of(recipient() + ":10", "rand1token"), SignRows.entries(rows, s)); + try { + SignRows.entries(List.of(new Row(recipient(), ""), new Row("rand1token", "")), s); + fail("the RAND row went without its amount"); + } catch (IllegalArgumentException expected) { + // its own sentence + } + } + + @Test public void payingOutputsAreDecidedFromTheOutputs() throws Exception { + JSONObject s = summary(); + assertEquals(1, SignRows.payingTotal(s)); // two zero outputs and the change are not paying + assertEquals(1, SignRows.payingMatched(s)); + assertTrue(SignRows.showsPayments(s)); + output(s, 2).put("matches", JSONObject.NULL); + assertEquals(0, SignRows.payingMatched(s)); + output(s, 2).put("matches", " "); + assertEquals(0, SignRows.payingMatched(s)); + assertFalse(SignRows.showsPayments(null)); + assertFalse(SignRows.showsPayments(new JSONObject().put("outputs", "x"))); + assertEquals(0, SignRows.payingTotal(new JSONObject().put("outputs", new JSONArray().put("x").put(JSONObject.NULL)))); + assertFalse(SignRows.needsPayee(new JSONObject().put("amount", "000").put("to_account", false))); + assertTrue(SignRows.needsPayee(new JSONObject().put("amount", "7").put("to_account", "true"))); + assertFalse(SignRows.needsPayee(new JSONObject().put("amount", "7").put("to_account", true))); + } + + static JSONObject output(JSONObject s, int i) throws Exception { + return s.getJSONArray("outputs").getJSONObject(i); + } + + // ------------------------------------------------------------------ the stale gate + + @Test public void editingARowAfterACheckDisarmsUntilACheckOfTheseRowsLands() throws Exception { + PaymentsGate g = new PaymentsGate(); + assertEquals(1, g.rows().size()); + assertFalse(g.stale()); + g.rows().get(0).addr = recipient(); + g.rows().get(0).amount = "10"; + g.edited(); + assertTrue(g.stale()); + g.adopt(List.of(recipient() + ":10")); + String sent = g.sentKey(); + g.landed(sent); + assertFalse(g.stale()); + // Editing after the check. + g.rows().get(0).amount = "11"; + g.edited(); + assertTrue(g.stale()); + // The top Check / Retry resend the last adopted payments: their snapshot is the old rows. + g.landed(g.sentKey()); + assertTrue("Check and Retry cannot re-arm Sign after an edit", g.stale()); + // Edited back to exactly what was checked: a check now lands armed. + g.rows().get(0).amount = "10"; + g.edited(); + g.landed(g.sentKey()); + assertFalse(g.stale()); + } + + @Test public void anEditWhileTheCheckIsOutLandsStale() throws Exception { + PaymentsGate g = new PaymentsGate(); + g.rows().get(0).addr = recipient(); + g.rows().get(0).amount = "10"; + g.adopt(List.of(recipient() + ":10")); + String sent = g.sentKey(); + g.addRow(); + g.landed(sent); + assertTrue(g.stale()); + g.removeRow(g.rows().get(1)); + assertTrue(g.stale()); + g.landed(sent); + assertFalse("the snapshot taken at send time equals the rows now", g.stale()); + } + + @Test public void theRowsKeyCannotBeForgedBySeparators() { + assertFalse(SignRows.key(List.of(new Row("a|1:b", ""))).equals(SignRows.key(List.of(new Row("a", "b"))))); + assertFalse(SignRows.key(List.of(new Row("a\n1:", "x"))).equals(SignRows.key(List.of(new Row("a", ""), new Row("", "x"))))); + } + + // ------------------------------------------------------------------ which check lands + + @Test public void onlyTheLatestCheckLands() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket first = c.begin("{p}"); + CheckSession.Ticket second = c.begin("{p}"); + assertTrue(second.id > first.id); + assertFalse(c.land(first, summary(), signable())); + assertNull(c.review()); + assertTrue(c.inFlight()); + assertTrue(c.land(second, summary(), signable())); + assertFalse(c.inFlight()); + assertNotNull(c.review()); + assertEquals("{p}", c.reviewedProposal()); + assertFalse(c.fail(first, "late")); + assertNull(c.error()); + } + + @Test public void theSequenceNeverResets() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.close(); + CheckSession.Ticket after = c.begin("{p}"); + assertTrue(after.id > t.id); + assertFalse("a check that lands after the screen was left is ignored", c.land(t, summary(), signable())); + c.proposalEdited("{q}"); + assertFalse(c.land(after, summary(), signable())); + assertTrue(c.begin("{q}").id > after.id); + } + + @Test public void theTicketSnapshotsThePayeesAndRows() throws Exception { + CheckSession c = new CheckSession(); + c.begin("{p}"); + c.gate().rows().get(0).addr = "rand1x"; + c.gate().rows().get(0).amount = "1"; + c.gate().adopt(List.of("rand1x:1")); + CheckSession.Ticket t = c.begin("{p}"); + assertEquals(List.of("rand1x:1"), t.payees); + c.gate().adopt(List.of("rand1y:1")); + assertEquals("the ticket keeps what it was sent with", List.of("rand1x:1"), t.payees); + try { + t.payees.add("x"); + fail("a ticket's payees are fixed"); + } catch (UnsupportedOperationException expected) { + // fixed + } + } + + @Test public void signSignsOnlyTheTextTheReviewWasCheckedOn() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.land(t, summary(), signable()); + c.setTyped("aak3"); + assertTrue(c.canSign("{p}")); + c.gate().rows().get(0).addr = recipient(); + assertFalse("the editor holds other text", c.canSign("{p} ")); + c.proposalEdited("{p} "); + assertNull(c.review()); + assertNull(c.reviewedProposal()); + assertEquals("", c.typed()); + assertNull("the payments were asked for another text", c.lastSummary()); + assertEquals("", c.gate().rows().get(0).addr); + assertFalse(c.canSign("{p} ")); + } + + @Test public void editingBackToTheCheckedTextKeepsThePaymentsButNotTheReview() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.gate().rows().get(0).addr = recipient(); + c.land(t, summary(), signable()); + c.proposalEdited("{p}"); + assertNull(c.review()); + assertNotNull(c.lastSummary()); + assertEquals(recipient(), c.gate().rows().get(0).addr); + } + + @Test public void aNewProposalTextStartsThePaymentsOver() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.land(t, summary(), signable()); + c.gate().rows().get(0).addr = "rand1x"; + c.gate().adopt(List.of("rand1x")); + c.begin("{q}"); + assertEquals(Collections.emptyList(), c.gate().payees()); + assertEquals("", c.gate().rows().get(0).addr); + assertNull(c.lastSummary()); + } + + @Test public void aRefusedCheckKeepsTheRowsAndTheLastGoodSummary() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.land(t, summary(), judge(summary(), 2000L)); + c.gate().rows().get(0).addr = "rand1typo"; + c.gate().rows().get(0).amount = "10"; + c.gate().adopt(List.of("rand1typo:10")); + CheckSession.Ticket t2 = c.begin("{p}"); + assertTrue(c.fail(t2, "expected recipient rand1typo: not an address")); + assertNull(c.review()); + assertEquals("expected recipient rand1typo: not an address", c.error()); + assertNotNull(c.lastSummary()); + assertTrue(SignRows.showsPayments(c.lastSummary())); + assertEquals("rand1typo", c.gate().rows().get(0).addr); + assertFalse(c.canSign("{p}")); + } + + @Test public void aStaleGateKeepsSignOff() throws Exception { + CheckSession c = new CheckSession(); + c.land(c.begin("{p}"), summary(), signable()); + c.gate().rows().get(0).addr = recipient(); + c.gate().rows().get(0).amount = "10"; + c.gate().edited(); + c.gate().adopt(payees()); + CheckSession.Ticket t = c.begin("{p}"); + assertEquals(payees(), t.payees); + c.gate().rows().get(0).amount = "12"; + c.gate().edited(); + c.land(t, summary(), signable()); + c.setTyped("AAK3"); + assertTrue(c.gate().stale()); + assertFalse(c.canSign("{p}")); + c.gate().rows().get(0).amount = "10"; + c.gate().edited(); + CheckSession.Ticket t2 = c.begin("{p}"); + c.land(t2, summary(), signable()); + c.setTyped("AAK3"); + assertTrue(c.canSign("{p}")); + assertEquals("Sign sends the list the review was checked with", payees(), c.reviewedPayees()); + c.gate().adopt(List.of("rand1other:1")); + assertEquals(payees(), c.reviewedPayees()); + } + + @Test public void aSignaturesRefusalReplacesTheVerdictOnTheSameSummary() throws Exception { + CheckSession c = new CheckSession(); + CheckSession.Ticket t = c.begin("{p}"); + c.land(t, summary(), signable()); + c.setTyped("aak3"); + c.refused(judge(summary(), 2020L)); + assertEquals(Verdict.EXPIRED, c.review().judgement.verdict); + assertTrue(c.review().showsSummary()); + assertFalse(c.canSign("{p}")); + assertEquals("This proposal expires at height 2024 and the chain is at 2020: too late to sign. Ask for a new proposal.", c.review().refusal()); + } + + // ------------------------------------------------------------------ the confirmation + + @Test public void theLastFourIgnoreDashesAndCase() throws Exception { + SignReview r = new SignReview(summary(), signable()); // NFYW-K8E9-E5K9-AAK3 + assertTrue(r.canSign("aak3", false)); + assertTrue(r.canSign("AAK3", false)); + assertTrue(r.canSign("a-ak3", false)); + assertTrue(r.canSign(" AA-K3 ", false)); + assertFalse(r.canSign("aak", false)); + assertFalse(r.canSign("9aak3", false)); + assertFalse(r.canSign("", false)); + assertFalse("stale", r.canSign("aak3", true)); + assertFalse(new SignReview(summary().put("policy_fingerprint", 5), signable()).canSign("aak3", false)); + } + + // ------------------------------------------------------------------ the review and refusals + + @Test public void theLinesAreTheCoresVerbatim() throws Exception { + SignReview r = new SignReview(summary(), signable()); + JSONArray want = fixture().getJSONArray("lines_expecting_recipient"); + assertEquals(want.length(), r.lines().size()); + for (int i = 0; i < want.length(); i++) assertEquals(want.getString(i), r.lines().get(i)); + assertTrue(r.showsSummary()); + assertNull(r.refusal()); + assertEquals(24, r.blocksLeft()); + assertEquals("0.001", r.fee()); + assertEquals("2024", r.expiresAt()); + assertFalse(r.retryable()); + } + + @Test public void everyRefusalIsShownByItsSentence() throws Exception { + assertEquals("The proposal check returned an incomplete answer, so this proposal is not signed.", + new SignReview(null, judge("x", 2000L)).refusal()); + assertEquals("Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.", + new SignReview(summary(), judge(summary().put("recipients_checked", false), 2000L)).refusal()); + SignReview noNode = new SignReview(summary(), judge(summary(), null)); + assertEquals("No node answered at " + URL + ", so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.", noNode.refusal()); + assertTrue(noNode.retryable()); + assertEquals("This proposal is for chain 7; this wallet is on chain 21.", + new SignReview(summary(), SignFlow.judge(summary(), 2000L, 21, GENESIS, URL, payees())).refusal()); + assertEquals("This proposal is bound to another genesis than the one this wallet's node serves.", + new SignReview(summary(), judge(summary().put("binding_domain", 1).put("genesis", "ab"), 2000L)).refusal()); + assertEquals("This proposal expires at height 2024 and the chain is at 2010: too late to sign. Ask for a new proposal.", + new SignReview(summary(), judge(summary(), 2010L)).refusal()); + for (Verdict v : Verdict.values()) { + if (v == Verdict.SIGNABLE) continue; + assertEquals(v == Verdict.NO_NODE, SignReview.retryable(v)); + } + } + + @Test public void aMalformedSummaryNeverCrashesAndShowsOnlyItsRefusal() throws Exception { + for (Object bad : new Object[] {null, "x", new JSONArray(), JSONObject.NULL, new JSONObject(), + new JSONObject().put("lines", "x").put("outputs", new JSONArray()), + new JSONObject().put("lines", new JSONArray().put(1).put("ok")).put("outputs", new JSONArray().put("x")).put("fee", "abc").put("expires_at", true)}) { + JSONObject s = bad instanceof JSONObject ? (JSONObject) bad : null; + SignReview r = new SignReview(s, judge(bad, 2000L)); + assertFalse(r.judgement.signable()); + assertNotNull(r.refusal()); + assertFalse(r.canSign("aak3", false)); + r.lines(); + r.fee(); + r.expiresAt(); + r.payingTotal(); + r.payingMatched(); + assertEquals(0, r.blocksLeft()); + } + // Lines that are not all strings: malformed, shown as the refusal alone (final fix wave). + SignReview half = new SignReview(new JSONObject().put("lines", new JSONArray().put(1).put("ok")).put("outputs", new JSONArray()), judge("x", 1L)); + assertFalse(half.showsSummary()); + SignReview whole = new SignReview(new JSONObject().put("lines", new JSONArray().put("ok")).put("outputs", new JSONArray()), judge("x", 1L)); + assertTrue(whole.showsSummary()); + assertEquals(List.of("ok"), whole.lines()); + assertFalse(new SignReview(new JSONObject().put("lines", new JSONArray()), judge("x", 1L)).showsSummary()); + } + + // ------------------------------------------------------------------ files + + @Test public void aFileOverOneMiBIsRefusedBeforeItIsRead() throws Exception { + InputStream untouched = new InputStream() { + @Override public int read() throws IOException { + throw new AssertionError("read a file it should have refused by its size"); + } + }; + assertFileRefused((1 << 20) + 1L, untouched, "That file is larger than a proposal, a policy or a key file can be."); + assertEquals("{}", PickedFile.read(2, new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8)))); + byte[] exactly = new byte[1 << 20]; + Arrays.fill(exactly, (byte) 'a'); + assertEquals(1 << 20, PickedFile.read(-1, new ByteArrayInputStream(exactly)).length()); + } + + @Test public void aFileThatGrowsPastTheCapOrFailsToReadIsRefused() throws Exception { + byte[] big = new byte[(1 << 20) + 1]; + assertFileRefused(-1, new ByteArrayInputStream(big), "That file is larger than a proposal, a policy or a key file can be."); + assertFileRefused(10, new ByteArrayInputStream(big), "That file is larger than a proposal, a policy or a key file can be."); + InputStream broken = new InputStream() { + @Override public int read() throws IOException { + throw new IOException("gone"); + } + }; + assertFileRefused(10, broken, "That file could not be read."); + assertFileRefused(10, null, "That file could not be read."); + } + + static void assertFileRefused(long size, InputStream in, String sentence) { + try { + PickedFile.read(size, in); + fail("read it"); + } catch (IllegalArgumentException e) { + assertEquals(sentence, e.getMessage()); + } + } + + @Test public void theRowsListStartsWithOneEmptyRow() { + PaymentsGate g = new PaymentsGate(); + assertEquals(1, g.rows().size()); + assertEquals("", g.rows().get(0).addr); + assertEquals("", g.rows().get(0).amount); + assertEquals(new ArrayList(), g.payees()); + } + + // ---- final fix wave ---- + + /** A payee paid RAND and a token is not offered the exception, and the screen says why. */ + @Test public void aPayeePaidRandAndATokenIsNotOfferedTheExceptionAndIsToldWhy() throws Exception { + JSONObject s = new JSONObject() + .put("outputs", new JSONArray() + .put(new JSONObject().put("to_account", false).put("amount", "900").put("asset", 0).put("owner_fingerprint", "X").put("matches", "a")) + .put(new JSONObject().put("to_account", false).put("amount", "1").put("asset", 5).put("owner_fingerprint", "X").put("matches", "b")) + .put(new JSONObject().put("to_account", false).put("amount", "5").put("asset", 7).put("owner_fingerprint", "F").put("matches", "c"))) + .put("expected", new JSONArray() + .put(new JSONObject().put("entry", "rand1x").put("fingerprint", "X")) + .put(new JSONObject().put("entry", "rand1f").put("fingerprint", "F"))); + assertFalse(SignRows.amountOptional(new Row("rand1x", ""), s)); + assertTrue(SignRows.mixedPayee(new Row("rand1x", ""), s)); + assertTrue("a token-only payee keeps the exception", SignRows.amountOptional(new Row("rand1f", ""), s)); + assertFalse(SignRows.mixedPayee(new Row("rand1f", ""), s)); + assertFalse("unknown rows are not called mixed", SignRows.mixedPayee(new Row("rand1new", ""), s)); + assertFalse(SignRows.mixedPayee(new Row("rand1x", ""), null)); + try { + SignRows.entries(List.of(new Row("rand1x", "")), s); + fail("the mixed payee's row needs its amount"); + } catch (IllegalArgumentException expected) { /* refused on the screen */ } + } +} diff --git a/android/app/src/test/java/org/randprotocol/wallet/wallet/SignFlowTest.java b/android/app/src/test/java/org/randprotocol/wallet/wallet/SignFlowTest.java new file mode 100644 index 0000000..e742b99 --- /dev/null +++ b/android/app/src/test/java/org/randprotocol/wallet/wallet/SignFlowTest.java @@ -0,0 +1,696 @@ +package org.randprotocol.wallet.wallet; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.randprotocol.wallet.security.SignerProfiles; +import org.randprotocol.wallet.wallet.SignFlow.Verdict; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CancellationException; + +/** + * The signing rules (ui/engine/signers.js {@code verdict}, iOS {@code MsauthRules.verdict}) and the + * order a signature is made in (iOS {@code MsauthSigning}), on the JVM: the summaries are the + * vendored fullnode vectors (ui/test/fixtures/msauth, a test resource), the core and the node are + * stand-ins (a unit test cannot load the native core). + */ +public class SignFlowTest { + static final long MAX_SAFE = 9_007_199_254_740_991L; + static final String GENESIS = "0".repeat(64); + + static JSONObject fixture() throws Exception { + try (InputStream in = SignFlowTest.class.getResourceAsStream("/expected.json")) { + return new JSONObject(new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } + + static String recipient() throws Exception { + return fixture().getString("recipient"); + } + + /** The intended payment the summary below was checked with: the payee at 10 RAND. */ + static List payees() throws Exception { + return new ArrayList<>(List.of(recipient() + ":10")); + } + + /** + * {@code msauth_check}'s reply for the valid proposal with the payee named: fullnode's + * {@code summary_expecting_recipient} plus what wallet-core msauth.rs adds beside it. + */ + static JSONObject summary() throws Exception { + JSONObject f = fixture(); + JSONObject s = f.getJSONObject("summary_expecting_recipient"); + return s.put("binding_domain", 0) + .put("lines", f.getJSONArray("lines_expecting_recipient")) + .put("expected", new JSONArray().put(new JSONObject().put("entry", recipient() + ":10").put("fingerprint", f.getString("recipient_owner_fingerprint")))); + } + + static Verdict v(Object summary, Long head) throws Exception { + return SignFlow.verdict(summary, head, 7, GENESIS, payees()); + } + + static Verdict v(Object summary, Long head, List recipients) { + return SignFlow.verdict(summary, head, 7, GENESIS, recipients); + } + + static JSONObject output(JSONObject s, int i) throws Exception { + return s.getJSONArray("outputs").getJSONObject(i); + } + + // ------------------------------------------------------------------ the rule + + @Test public void theExpiryRuleIsTheClis() throws Exception { + JSONObject s = summary(); // expires_at 2024 + assertEquals(16, SignFlow.EXPIRY_MARGIN); + assertEquals(Verdict.SIGNABLE, v(s, 2007L)); + assertEquals(Verdict.EXPIRED, v(s, 2008L)); + assertEquals(Verdict.EXPIRED, v(s, 2024L)); + assertEquals(Verdict.EXPIRED, v(s, 5000L)); + assertEquals("overflow-safe at the top of the range", Verdict.EXPIRED, v(s, MAX_SAFE)); + assertEquals(17, SignFlow.judge(s, 2007L, 7, GENESIS, "", payees()).blocksLeft); + } + + @Test public void noNodeNoSignature() throws Exception { + JSONObject s = summary(); + assertEquals(Verdict.NO_NODE, v(s, null)); + assertEquals("a head past 2^53-1 is no answer", Verdict.NO_NODE, v(s, MAX_SAFE + 1)); + assertEquals(Verdict.NO_NODE, v(s, -1L)); + assertEquals(Verdict.NO_NODE, v(s, Long.MAX_VALUE)); + } + + @Test public void theChainAndTheGenesisAreThisWallets() throws Exception { + JSONObject s = summary(); + assertEquals(Verdict.WRONG_CHAIN, SignFlow.verdict(s, 1L, 21, GENESIS, payees())); + assertEquals("unbound: the genesis is not compared", Verdict.SIGNABLE, SignFlow.verdict(s, 1L, 7, "ab".repeat(32), payees())); + s.put("binding_domain", 1); + assertEquals(Verdict.WRONG_GENESIS, SignFlow.verdict(s, 1L, 7, "ab".repeat(32), payees())); + assertEquals(Verdict.WRONG_GENESIS, SignFlow.verdict(s, 1L, 7, null, payees())); + assertEquals("case-insensitive", Verdict.SIGNABLE, SignFlow.verdict(s.put("genesis", "AB".repeat(32)), 1L, 7, "ab".repeat(32), payees())); + } + + @Test public void anUncheckedPayeeRefusesBeforeAnythingElse() throws Exception { + JSONObject s = summary().put("recipients_checked", false); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(s, null)); + for (Object notTrue : new Object[] {"true", 1, JSONObject.NULL}) { + assertEquals(String.valueOf(notTrue), Verdict.UNCHECKED_RECIPIENT, v(summary().put("recipients_checked", notTrue), 2007L)); + } + JSONObject missing = summary(); + missing.remove("recipients_checked"); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(missing, 2007L)); + // Unchecked comes before a malformed expiry, as on iOS and the web. + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(summary().put("recipients_checked", false).put("expires_at", "soon"), 2007L)); + } + + @Test public void theCoresOwnSummaryWithoutAPayeeIsUnchecked() throws Exception { + JSONObject plain = fixture().getJSONObject("summary").put("binding_domain", 0); // recipients_checked false + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(plain, 2007L, Collections.emptyList())); + } + + @Test public void everyPayingOutputNeedsAMatch() throws Exception { + for (Object absent : new Object[] {JSONObject.NULL, "", " ", 7, true}) { + JSONObject s = summary(); + output(s, 2).put("matches", absent); + assertEquals("matches = " + absent, Verdict.UNCHECKED_RECIPIENT, v(s, 2007L)); + } + JSONObject s = summary(); + output(s, 2).remove("matches"); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(s, 2007L)); + // Change (to_account) and zero-amount slots need no payee: the fixture's outputs 0, 1 and 3. + JSONObject zero = summary(); + output(zero, 0).put("amount", " 000 "); + assertEquals(Verdict.SIGNABLE, v(zero, 2007L)); + // A zero-amount dummy with an amount written as "0.0" is not zero: it is malformed. + output(zero, 0).put("amount", "0.0"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(zero, 2007L)); + } + + @Test public void theListIsJudgedAsGiven() throws Exception { + JSONObject s = summary(); + assertEquals("no payee named while one is paid", Verdict.UNCHECKED_RECIPIENT, v(s, 2007L, Collections.emptyList())); + for (String blank : new String[] {"", " ", ":10", " :10", "\uFEFF"}) { + List l = payees(); + l.add(blank); + assertEquals("entry '" + blank + "'", Verdict.UNCHECKED_RECIPIENT, v(s, 2007L, l)); + } + List withNull = payees(); + withNull.add(null); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(s, 2007L, withNull)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(s, 2007L, null)); + } + + @Test public void anAmountIsPinnedUnlessTheRowOwnsANonRandOutput() throws Exception { + String fp = fixture().getString("recipient_owner_fingerprint"); + List bare = List.of(recipient()); + JSONObject exp = new JSONObject().put("entry", recipient()).put("fingerprint", fp); + // A RAND payment named without its amount: refused. + JSONObject rand = summary().put("expected", new JSONArray().put(exp)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(rand, 2007L, bare)); + assertEquals("an amount after the colon that is blank is no amount", Verdict.UNCHECKED_RECIPIENT, v(rand, 2007L, List.of(recipient() + ": "))); + // The same payee paid a token (asset 3): the address alone is the entry, as the core requires. + JSONObject token = summary().put("expected", new JSONArray().put(exp)); + output(token, 2).put("asset", 3); + assertEquals(Verdict.SIGNABLE, v(token, 2007L, bare)); + output(token, 2).put("asset", "3"); + assertEquals("an asset written as digits", Verdict.SIGNABLE, v(token, 2007L, bare)); + // expected missing, not a list, matched by another entry's text, or another fingerprint: refused. + JSONObject noExp = new JSONObject(token.toString()); + noExp.remove("expected"); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(noExp, 2007L, bare)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(new JSONObject(token.toString()).put("expected", "x"), 2007L, bare)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(new JSONObject(token.toString()).put("expected", new JSONArray().put(new JSONObject().put("entry", recipient() + " ").put("fingerprint", fp))), 2007L, bare)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(new JSONObject(token.toString()).put("expected", new JSONArray().put(new JSONObject().put("entry", recipient()).put("fingerprint", "ffff"))), 2007L, bare)); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(new JSONObject(token.toString()).put("expected", new JSONArray().put("not an object")), 2007L, bare)); + // A zero-amount token output does not pay: it cannot unpin a row. + JSONObject zeroToken = new JSONObject(token.toString()); + output(zeroToken, 1).put("asset", 3).put("owner_fingerprint", "aaaa"); + output(zeroToken, 2).put("asset", 0); + JSONObject e2 = new JSONObject().put("entry", recipient()).put("fingerprint", "aaaa"); + assertEquals(Verdict.UNCHECKED_RECIPIENT, v(zeroToken.put("expected", new JSONArray().put(e2)), 2007L, bare)); + } + + @Test public void aMalformedSummaryRefuses() throws Exception { + assertEquals(Verdict.MALFORMED_SUMMARY, v(null, 2007L)); + assertEquals(Verdict.MALFORMED_SUMMARY, v("{}", 2007L)); + assertEquals(Verdict.MALFORMED_SUMMARY, v(new JSONArray(), 2007L)); + assertEquals(Verdict.MALFORMED_SUMMARY, v(JSONObject.NULL, 2007L)); + Object[] badUint = {-1, MAX_SAFE + 1, "2024", true, 1.5, JSONObject.NULL, new JSONArray()}; + for (String field : new String[] {"expires_at", "chain_id"}) { + for (Object bad : badUint) { + assertEquals(field + " = " + bad, Verdict.MALFORMED_SUMMARY, v(summary().put(field, bad), 2007L)); + } + JSONObject missing = summary(); + missing.remove(field); + assertEquals(field + " missing", Verdict.MALFORMED_SUMMARY, v(missing, 2007L)); + } + for (Object bad : new Object[] {-1, "0", true, 0.5, JSONObject.NULL}) { + assertEquals("binding_domain = " + bad, Verdict.MALFORMED_SUMMARY, v(summary().put("binding_domain", bad), 2007L)); + } + JSONObject noDomain = summary(); + noDomain.remove("binding_domain"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(noDomain, 2007L)); + for (Object bad : new Object[] {"", "zz", "0x00", 0, JSONObject.NULL}) { + assertEquals("genesis = " + bad, Verdict.MALFORMED_SUMMARY, v(summary().put("genesis", bad), 2007L)); + } + JSONObject noGenesis = summary(); + noGenesis.remove("genesis"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(noGenesis, 2007L)); + // Integral numbers are numbers, however written. + assertEquals(Verdict.SIGNABLE, v(new JSONObject(summary().toString().replace("\"expires_at\":2024", "\"expires_at\":2024.0")), 2007L)); + } + + @Test public void malformedOutputsRefuse() throws Exception { + JSONObject noOuts = summary(); + noOuts.remove("outputs"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(noOuts, 2007L)); + assertEquals(Verdict.MALFORMED_SUMMARY, v(summary().put("outputs", "[]"), 2007L)); + JSONObject notObject = summary(); + notObject.getJSONArray("outputs").put("x"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(notObject, 2007L)); + for (Object bad : new Object[] {1, 0, "true", "false", JSONObject.NULL}) { + JSONObject s = summary(); + output(s, 3).put("to_account", bad); + assertEquals("to_account = " + bad, Verdict.MALFORMED_SUMMARY, v(s, 2007L)); + } + JSONObject noTo = summary(); + output(noTo, 3).remove("to_account"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(noTo, 2007L)); + // to_account as JSON text: 1 is not a boolean, even though org.json would coerce it. + assertEquals(Verdict.MALFORMED_SUMMARY, v(new JSONObject(summary().toString().replace("\"to_account\":true", "\"to_account\":1")), 2007L)); + for (Object bad : new Object[] {"-1", "1.5", "", " ", "1e3", "١٢", -1, MAX_SAFE + 1, true, 1.5, JSONObject.NULL}) { + JSONObject s = summary(); + output(s, 2).put("amount", bad); + assertEquals("amount = " + bad, Verdict.MALFORMED_SUMMARY, v(s, 2007L)); + } + JSONObject noAmount = summary(); + output(noAmount, 2).remove("amount"); + assertEquals(Verdict.MALFORMED_SUMMARY, v(noAmount, 2007L)); + JSONObject digits = summary(); + output(digits, 2).put("amount", " 10000000000 "); + assertEquals("a digit string is an amount", Verdict.SIGNABLE, v(digits, 2007L)); + // A malformed output is found before the match check. + JSONObject both = summary(); + output(both, 2).put("matches", JSONObject.NULL); + output(both, 3).put("to_account", 1); + assertEquals(Verdict.MALFORMED_SUMMARY, v(both, 2007L)); + } + + @Test public void theRefusalsReadAsTheWebsDo() throws Exception { + JSONObject s = summary(); + assertNull(SignFlow.judge(s, 2007L, 7, GENESIS, "", payees()).message()); + assertEquals("The proposal check returned an incomplete answer, so this proposal is not signed.", + SignFlow.judge(null, 2007L, 7, GENESIS, "", payees()).message()); + assertEquals("Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.", + SignFlow.judge(s, 2007L, 7, GENESIS, "", Collections.emptyList()).message()); + assertEquals("No node answered at https://rpc.example, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.", + SignFlow.judge(s, null, 7, GENESIS, "https://rpc.example", payees()).message()); + assertEquals("No node answered at —, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.", + SignFlow.judge(s, null, 7, GENESIS, "", payees()).message()); + assertEquals("This proposal is for chain 7; this wallet is on chain 21.", SignFlow.judge(s, 1L, 21, GENESIS, "", payees()).message()); + assertEquals("This proposal is bound to another genesis than the one this wallet's node serves.", + SignFlow.judge(summary().put("binding_domain", 1), 1L, 7, "ab", "", payees()).message()); + assertEquals("This proposal expires at height 2024 and the chain is at 2010: too late to sign. Ask for a new proposal.", + SignFlow.judge(s, 2010L, 7, GENESIS, "", payees()).message()); + } + + @Test public void confirmationIsTheLastFour() { + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", " aak3")); + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "a-ak3")); + assertFalse(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "aak")); + assertFalse(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "9aak3")); + assertFalse(SignFlow.confirms("", "")); + assertFalse(SignFlow.confirms(null, "aak3")); + assertFalse(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", null)); + } + + @Test public void linesAreTheCoresVerbatimAndFilesAreCleaned() throws Exception { + assertEquals(fixture().getJSONArray("lines_expecting_recipient").getString(2), SignFlow.lines(summary()).get(2)); + assertEquals("chain 7 · genesis " + GENESIS, SignFlow.lines(summary()).get(0)); + assertTrue(SignFlow.lines(new JSONObject()).isEmpty()); + assertEquals("{}", SignFlow.clean("\uFEFF {}\r\n")); + assertEquals("", SignFlow.clean(null)); + assertEquals(List.of("a:1", ""), SignFlow.payees(Arrays.asList(" a:1\n", null))); + } + + // ------------------------------------------------------------------ the order a signature is made in + + static final String PROPOSAL = "{\"proposal\":1}"; + static final String POLICY = "{\"policy\":1}"; + static final String SHARE = "2:abcd"; + + static class FakeCore implements MsauthCore { + final List calls = new ArrayList<>(); + final List> checkLists = new ArrayList<>(); + final List> signLists = new ArrayList<>(); + JSONObject reply; + String seedHex; + Exception signError; + + FakeCore(JSONObject reply) { + this.reply = reply; + } + + @Override public JSONObject msauthCheck(String proposal, String policy, List expectedRecipients) { + calls.add("check " + proposal + " " + policy); + checkLists.add(expectedRecipients); + return reply; + } + + @Override public String msauthSign(String proposal, String policy, String seedHex, List expectedRecipients) throws Exception { + calls.add("sign " + proposal + " " + policy); + signLists.add(expectedRecipients); + this.seedHex = seedHex; + if (signError != null) throw signError; + return SHARE; + } + + @Override public void msauthVerifyShare(String proposal, String policy, String share) { + calls.add("verify " + share); + } + + @Override public JSONObject msauthSignerImport(String input, String policy) { + throw new UnsupportedOperationException(); + } + + @Override public JSONObject msauthPolicyInfo(String policy) { + throw new UnsupportedOperationException(); + } + } + + static final class FakeNode implements SignFlow.Node { + final List heads = new ArrayList<>(); + long chainId = 7; + String genesis = GENESIS; + boolean fail; + int headReads; + + FakeNode(Long... heads) { + this.heads.addAll(Arrays.asList(heads)); + } + + @Override public String url() { + return "https://rpc.example"; + } + + @Override public long chainId() throws Exception { + if (fail) throw new java.io.IOException("down"); + return chainId; + } + + @Override public long headHeight() { + long h = heads.get(Math.min(headReads, heads.size() - 1)); + headReads++; + return h; + } + + @Override public String genesisHash() { + return genesis; + } + } + + /** The prompt and the decrypt, counted; the buffer it hands out is kept to see it zeroed. */ + static final class FakeSeeds implements SignFlow.SeedSource { + int prompts; + byte[] handed; + Runnable onPrompt = () -> { }; + + @Override public byte[] seed(String id) { + prompts++; + onPrompt.run(); + handed = new byte[32]; + Arrays.fill(handed, (byte) 0x11); + return handed; + } + } + + static final class Profiles { + final SignerProfiles store; + + Profiles() throws Exception { + Map m = new HashMap<>(); + store = new SignerProfiles(new SignerProfiles.KeyValue() { + public String get(String k) { return m.get(k); } + public void put(String k, String v) { m.put(k, v); } + public void remove(String k) { m.remove(k); } + public java.util.Set keys() { return m.keySet(); } + }); + store.add(new SignerProfiles.Profile("a1", "treasury", 2, "33".repeat(32)), POLICY, "AA==:AA=="); + } + } + + static SignFlow flow(FakeCore core, SignFlow.Node node, FakeSeeds seeds) throws Exception { + return new SignFlow(new Profiles().store, core, node, 7, seeds); + } + + static void assertZeroed(byte[] b) { + assertArrayEquals(new byte[b.length], b); + } + + @Test public void signChecksJudgesPromptsJudgesAgainAndSignsWithTheSameList() throws Exception { + FakeCore core = new FakeCore(summary()); + FakeNode node = new FakeNode(2000L); + FakeSeeds seeds = new FakeSeeds(); + List typed = new ArrayList<>(List.of(" " + recipient() + ":10\n")); + SignFlow.Share share = flow(core, node, seeds).sign("a1", "\uFEFF" + PROPOSAL + "\n", typed, null); + assertEquals(SHARE, share.share); + assertEquals(2, share.index); + assertEquals(List.of("check " + PROPOSAL + " " + POLICY, "sign " + PROPOSAL + " " + POLICY), core.calls); + assertEquals("cleaned, none dropped", payees(), core.checkLists.get(0)); + assertSame("msauth_sign gets the very list the check was given", core.checkLists.get(0), core.signLists.get(0)); + assertEquals("11".repeat(32), core.seedHex); + assertEquals(1, seeds.prompts); + assertEquals("the head is read before the prompt and again after it", 2, node.headReads); + assertZeroed(seeds.handed); + } + + @Test public void aRefusalComesBeforeThePrompt() throws Exception { + for (Object[] c : new Object[][] { + {summary(), new FakeNode(2010L), Verdict.EXPIRED}, + {summary().put("recipients_checked", false), new FakeNode(2000L), Verdict.UNCHECKED_RECIPIENT}, + {summary().put("chain_id", 21), new FakeNode(2000L), Verdict.WRONG_CHAIN}, + {summary().put("expires_at", "x"), new FakeNode(2000L), Verdict.MALFORMED_SUMMARY}, + {summary(), null, Verdict.NO_NODE}, + }) { + FakeCore core = new FakeCore((JSONObject) c[0]); + FakeSeeds seeds = new FakeSeeds(); + try { + flow(core, (SignFlow.Node) c[1], seeds).sign("a1", PROPOSAL, payees(), null); + fail("signed " + c[2]); + } catch (SignFlow.Refusal r) { + assertEquals(c[2], r.judgement.verdict); + assertEquals(r.judgement.message(), r.getMessage()); + } + assertEquals("no prompt, no seed: " + c[2], 0, seeds.prompts); + assertEquals(1, core.calls.size()); + } + } + + @Test public void aNodeThatCannotAnswerIsNoNode() throws Exception { + FakeNode wrongChain = new FakeNode(2000L); + wrongChain.chainId = 21; + FakeNode down = new FakeNode(2000L); + down.fail = true; + FakeNode huge = new FakeNode(MAX_SAFE + 1); + for (FakeNode n : new FakeNode[] {wrongChain, down, huge}) { + FakeCore core = new FakeCore(summary()); + FakeSeeds seeds = new FakeSeeds(); + SignFlow.Check c = flow(core, n, seeds).check("a1", PROPOSAL, payees()); + assertEquals(Verdict.NO_NODE, c.judgement.verdict); + assertTrue(c.judgement.message().contains("https://rpc.example")); + try { + flow(core, n, seeds).sign("a1", PROPOSAL, payees(), null); + fail(); + } catch (SignFlow.Refusal r) { + assertEquals(Verdict.NO_NODE, r.judgement.verdict); + } + assertEquals(0, seeds.prompts); + } + FakeNode wrongGenesis = new FakeNode(2000L); + wrongGenesis.genesis = "ab".repeat(32); + assertEquals(Verdict.WRONG_GENESIS, flow(new FakeCore(summary().put("binding_domain", 1)), wrongGenesis, new FakeSeeds()).check("a1", PROPOSAL, payees()).judgement.verdict); + } + + @Test public void theHeadIsReadAgainAfterThePromptAndTheSeedIsZeroedOnRefusal() throws Exception { + FakeCore core = new FakeCore(summary()); + FakeNode node = new FakeNode(2000L, 2008L); // the prompt took eight blocks too many + FakeSeeds seeds = new FakeSeeds(); + try { + flow(core, node, seeds).sign("a1", PROPOSAL, payees(), null); + fail(); + } catch (SignFlow.Refusal r) { + assertEquals(Verdict.EXPIRED, r.judgement.verdict); + } + assertEquals(1, seeds.prompts); + assertEquals("the core never signed", 1, core.calls.size()); + assertZeroed(seeds.handed); + } + + @Test public void theNodeGoingAwayDuringThePromptRefuses() throws Exception { + FakeCore core = new FakeCore(summary()); + FakeNode node = new FakeNode(2000L); + FakeSeeds seeds = new FakeSeeds(); + seeds.onPrompt = () -> node.fail = true; + try { + flow(core, node, seeds).sign("a1", PROPOSAL, payees(), null); + fail(); + } catch (SignFlow.Refusal r) { + assertEquals(Verdict.NO_NODE, r.judgement.verdict); + } + assertZeroed(seeds.handed); + } + + @Test public void aCoreRefusalStillZeroesTheSeed() throws Exception { + FakeCore core = new FakeCore(summary()); + core.signError = new Exception("output 2 pays b9856a063c1fb742, not an expected recipient"); + FakeSeeds seeds = new FakeSeeds(); + try { + flow(core, new FakeNode(2000L), seeds).sign("a1", PROPOSAL, payees(), null); + fail(); + } catch (Exception e) { + assertEquals("output 2 pays b9856a063c1fb742, not an expected recipient", e.getMessage()); + } + assertZeroed(seeds.handed); + } + + @Test public void cancellingStopsBeforeThePromptAndBeforeSigning() throws Exception { + FakeCore core = new FakeCore(summary()); + FakeSeeds seeds = new FakeSeeds(); + try { + flow(core, new FakeNode(2000L), seeds).sign("a1", PROPOSAL, payees(), () -> true); + fail(); + } catch (CancellationException expected) { + // left the screen before the prompt + } + assertEquals(0, seeds.prompts); + + boolean[] left = {false}; + FakeSeeds seeds2 = new FakeSeeds(); + seeds2.onPrompt = () -> left[0] = true; + FakeCore core2 = new FakeCore(summary()); + try { + flow(core2, new FakeNode(2000L), seeds2).sign("a1", PROPOSAL, payees(), () -> left[0]); + fail(); + } catch (CancellationException expected) { + // left the screen during the prompt + } + assertEquals(1, core2.calls.size()); + assertZeroed(seeds2.handed); + } + + @Test public void aBlankEntryIsRefusedBeforeTheCoreIsAsked() throws Exception { + FakeCore core = new FakeCore(summary()); + List l = payees(); + l.add(" "); + SignFlow f = flow(core, new FakeNode(2000L), new FakeSeeds()); + SignFlow.Check c = f.check("a1", PROPOSAL, l); + assertNull(c.summary); + assertEquals(Verdict.UNCHECKED_RECIPIENT, c.judgement.verdict); + try { + f.sign("a1", PROPOSAL, l, null); + fail(); + } catch (SignFlow.Refusal r) { + assertEquals(Verdict.UNCHECKED_RECIPIENT, r.judgement.verdict); + } + assertTrue(core.calls.isEmpty()); + } + + @Test public void checkReturnsTheSummaryAndTheVerdictNow() throws Exception { + FakeCore core = new FakeCore(summary()); + SignFlow.Check c = flow(core, new FakeNode(2000L), new FakeSeeds()).check("a1", PROPOSAL, payees()); + assertEquals(Verdict.SIGNABLE, c.judgement.verdict); + assertEquals(24, c.judgement.blocksLeft); + assertEquals("NFYW-K8E9-E5K9-AAK3", c.summary.getString("policy_fingerprint")); + } + + @Test public void anUnknownProfileOrAShareWithoutAnIndexRefuses() throws Exception { + try { + flow(new FakeCore(summary()), new FakeNode(2000L), new FakeSeeds()).sign("nope", PROPOSAL, payees(), null); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("No such signer profile.", e.getMessage()); + } + FakeCore odd = new FakeCore(summary()) { + @Override public String msauthSign(String p, String q, String s, List l) { + return "x:abcd"; + } + }; + FakeSeeds seeds = new FakeSeeds(); + try { + flow(odd, new FakeNode(2000L), seeds).sign("a1", PROPOSAL, payees(), null); + fail(); + } catch (IllegalStateException e) { + assertEquals("msauth_sign returned no share", e.getMessage()); + } + assertZeroed(seeds.handed); + } + + @Test public void theNodesHeadAndChainAreReadStrictly() throws Exception { + assertEquals(2000L, SignFlow.headHeightOf(new JSONObject().put("height", 2000).put("hash", "ab"))); + assertEquals(MAX_SAFE, SignFlow.headHeightOf(new JSONObject().put("height", MAX_SAFE))); + assertEquals(0L, SignFlow.headHeightOf(new JSONObject().put("height", 0))); + Object[] badHeads = { + new JSONObject(), new JSONObject().put("height", JSONObject.NULL), new JSONObject().put("height", "2000"), + new JSONObject().put("height", 2000.9), new JSONObject().put("height", -1), new JSONObject().put("height", MAX_SAFE + 1), + new JSONObject().put("height", true), null, "2000", new JSONArray(), + }; + for (Object bad : badHeads) { + try { + SignFlow.headHeightOf(bad); + fail("head " + bad); + } catch (IllegalStateException expected) { + // no height: judged as no node + } + } + assertEquals(21L, SignFlow.chainIdOf(21)); + for (Object bad : new Object[] {21.5, "21", -1, MAX_SAFE + 1, null, JSONObject.NULL, true}) { + try { + SignFlow.chainIdOf(bad); + fail("chain " + bad); + } catch (IllegalStateException expected) { + // not a chain id + } + } + } + + @Test public void anRpcNodeWithAMalformedHeadIsNoNode() throws Exception { + for (Object height : new Object[] {null, JSONObject.NULL, "2000", 2000.9}) { + JSONObject head = new JSONObject().put("hash", "ab"); + if (height != null) head.put("height", height); + org.randprotocol.wallet.rpc.RpcClient rpc = new org.randprotocol.wallet.rpc.RpcClient("https://rpc.example") { + @Override public Object call(String method, JSONArray params) { + switch (method) { + case "rand_chainId": return 7; + case "rand_getHead": return head; + case "rand_getGenesisHash": return GENESIS; + default: throw new IllegalArgumentException(method); + } + } + }; + SignFlow.Check c = new SignFlow(new Profiles().store, new FakeCore(summary()), SignFlow.node(rpc), 7, new FakeSeeds()).check("a1", PROPOSAL, payees()); + assertEquals("height " + height, Verdict.NO_NODE, c.judgement.verdict); + } + JSONObject ok = new JSONObject().put("height", 2000); + org.randprotocol.wallet.rpc.RpcClient fractionalChain = new org.randprotocol.wallet.rpc.RpcClient("https://rpc.example") { + @Override public Object call(String method, JSONArray params) { + return method.equals("rand_chainId") ? (Object) 7.5 : method.equals("rand_getHead") ? ok : GENESIS; + } + }; + assertEquals(Verdict.NO_NODE, new SignFlow(new Profiles().store, new FakeCore(summary()), SignFlow.node(fractionalChain), 7, new FakeSeeds()) + .check("a1", PROPOSAL, payees()).judgement.verdict); + org.randprotocol.wallet.rpc.RpcClient good = new org.randprotocol.wallet.rpc.RpcClient("https://rpc.example") { + @Override public Object call(String method, JSONArray params) { + return method.equals("rand_chainId") ? (Object) 7 : method.equals("rand_getHead") ? ok : GENESIS; + } + }; + assertEquals(Verdict.SIGNABLE, new SignFlow(new Profiles().store, new FakeCore(summary()), SignFlow.node(good), 7, new FakeSeeds()) + .check("a1", PROPOSAL, payees()).judgement.verdict); + } + + @Test public void verifyShareCleansAndAsksTheCore() throws Exception { + FakeCore core = new FakeCore(summary()); + assertEquals(0, flow(core, null, new FakeSeeds()).verifyShare("a1", PROPOSAL, " 0:aa\n")); + assertEquals(List.of("verify 0:aa"), core.calls); + } + + // ---- final fix wave ---- + + private static JSONObject probe() throws Exception { + String g = "0".repeat(64); + return new JSONObject().put("recipients_checked", true).put("expires_at", 1000).put("chain_id", 21).put("binding_domain", 1) + .put("genesis", g).put("lines", new JSONArray().put("x")) + .put("outputs", new JSONArray() + .put(new JSONObject().put("to_account", false).put("amount", "900000000000").put("asset", 0).put("owner_fingerprint", "X").put("matches", "AAAA (900 RAND)")) + .put(new JSONObject().put("to_account", false).put("amount", "1").put("asset", 5).put("owner_fingerprint", "X").put("matches", "AAAA (0.000000001 RAND)")) + .put(new JSONObject().put("to_account", true).put("amount", "5").put("asset", 0).put("owner_fingerprint", "self").put("matches", JSONObject.NULL))) + .put("expected", new JSONArray().put(new JSONObject().put("entry", "rand1payee").put("fingerprint", "X"))); + } + + /** The reviewer's probe: X is paid 900 RAND beside 1 unit of asset 5, named with no amount. */ + @Test public void aDustTokenBesideARandOutputToTheSamePayeeIsRefused() throws Exception { + String g = "0".repeat(64); + assertEquals(Verdict.UNCHECKED_RECIPIENT, SignFlow.verdict(probe(), 10L, 21, g, List.of("rand1payee"))); + JSONObject noAsset = probe(); + noAsset.getJSONArray("outputs").getJSONObject(0).remove("asset"); + assertEquals("a missing asset counts as RAND", Verdict.UNCHECKED_RECIPIENT, SignFlow.verdict(noAsset, 10L, 21, g, List.of("rand1payee"))); + JSONObject tokenOnly = probe(); + tokenOnly.getJSONArray("outputs").remove(0); + assertEquals("the token-only payee may still go unpinned", Verdict.SIGNABLE, SignFlow.verdict(tokenOnly, 10L, 21, g, List.of("rand1payee"))); + } + + @Test public void linesMissingOrNotAllStringsIsMalformed() throws Exception { + String g = "0".repeat(64); + List r = List.of("rand1payee:900"); + Object[] bad = { null, "x", new JSONObject(), new JSONArray().put(1), new JSONArray().put("x").put(JSONObject.NULL), new JSONArray().put(new JSONArray().put("y")) }; + for (Object lines : bad) { + JSONObject s = probe(); + s.remove("lines"); + if (lines != null) s.put("lines", lines); + assertEquals(String.valueOf(lines), Verdict.MALFORMED_SUMMARY, SignFlow.verdict(s, 10L, 21, g, r)); + } + JSONObject empty = probe().put("lines", new JSONArray()); + empty.getJSONArray("outputs").remove(1); + assertEquals("an empty array of lines is well formed", Verdict.SIGNABLE, SignFlow.verdict(empty, 10L, 21, g, r)); + } + + @Test public void theConfirmationIgnoresInnerWhitespaceAsTheWebDoes() { + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "a a k 3")); + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", " A-A\tk3\n")); + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "aa\u00A0k3")); + assertFalse(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "a a k")); + } +} diff --git a/core/Cargo.lock b/core/Cargo.lock index 6868075..b820b43 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -149,7 +149,7 @@ dependencies = [ [[package]] name = "bridge-codec" -version = "0.7.1" +version = "0.8.0" [[package]] name = "bs58" @@ -462,6 +462,10 @@ dependencies = [ "crypto-common 0.2.2", ] +[[package]] +name = "dilithium-core" +version = "0.1.0" + [[package]] name = "e2e-fixtures" version = "0.1.0" @@ -1367,7 +1371,7 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "randprotocol-core" -version = "0.7.1" +version = "0.8.0" dependencies = [ "bincode", "blake3", @@ -1385,9 +1389,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "randprotocol-msig" +version = "0.8.0" +dependencies = [ + "getrandom 0.2.17", + "getrandom 0.4.3", + "hex", + "randprotocol-core", + "randprotocol-zkvm", + "serde", + "serde_json", + "thiserror 2.0.20", +] + [[package]] name = "randprotocol-prover" -version = "0.7.1" +version = "0.8.0" dependencies = [ "blake3", "bs58", @@ -1407,10 +1425,11 @@ dependencies = [ [[package]] name = "randprotocol-zkvm" -version = "0.7.1" +version = "0.8.0" dependencies = [ "chacha20", "chacha20poly1305", + "dilithium-core", "evm-core", "ml-kem", "p3-air", @@ -1823,6 +1842,7 @@ dependencies = [ "getrandom 0.4.3", "hex", "randprotocol-core", + "randprotocol-msig", "randprotocol-prover", "randprotocol-zkvm", "serde", diff --git a/core/Cargo.toml b/core/Cargo.toml index e9202e7..0155e7e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -21,6 +21,9 @@ randprotocol-zkvm = { path = "vendor/fullnode/crates/randprotocol-zkvm" } # The delegated-proving wire (sealed jobs a light client sends a prover). Only the codec: the # `service` feature (axum, tokio, sysinfo) is the prover's own and never builds for a wallet. randprotocol-prover = { path = "vendor/fullnode/crates/randprotocol-prover", default-features = false } +# Private multisig signer checks (fullnode feat/private-multisig, spec 2026-10-11 phase 4 §6.5): +# pure, no I/O, wasm32-clean; its `tests/fixtures/*.json` are the wallet apps' test vectors. +randprotocol-msig = { path = "vendor/fullnode/crates/randprotocol-msig" } wallet-core = { path = "crates/wallet-core" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/core/README.md b/core/README.md index 2ca7548..eeaa0a4 100644 --- a/core/README.md +++ b/core/README.md @@ -108,8 +108,12 @@ a split-authorisation chain (any paired prover), the spend key on an older one ( already in place, so it is ~2.8 MB of hex at the production profile. A prover that quotes a fee (`prover.fee`, its `prover_info.fee` passed through) is refused: this build pays none. -To move to a new chain build: `cd vendor/fullnode && git fetch && git checkout `, update -`CHAIN_BUILD`/`DEFAULT_CHAIN_ID` in `wallet-core`, run the tests (`cargo test --release`, and +To move to a new chain build: `scripts/vendor-fullnode.sh []` (from +`core/`, or `core/scripts/…` from the repo root) checks the submodule out at `` — fetched from a +local clone when it is not pushed yet, the clone itself only read — checks the zkvm's wasm32 gate, +the patched Plonky3 forks and `randprotocol-msig`'s fixtures (printing their SHA-256s), rewrites +`CHAIN_BUILD` and the tests that pin it, and refreshes both lockfiles; then update +`CHAIN_BUILD`'s doc comment (and `DEFAULT_CHAIN_ID` when the chain moves), run the tests (`cargo test --release`, and `-- --ignored` for the two real delegated-proof tests), rebuild the three targets. `version`'s `hc_bundle`/`hc_auth` are pinned in a test against the digests the live chain's `rand_status` answers: if the vendored build assembles either guest differently, that test says so first. diff --git a/core/crates/wallet-core/Cargo.toml b/core/crates/wallet-core/Cargo.toml index 7c3037b..c7a63ce 100644 --- a/core/crates/wallet-core/Cargo.toml +++ b/core/crates/wallet-core/Cargo.toml @@ -13,6 +13,8 @@ name = "wallet_core" randprotocol-core = { workspace = true } randprotocol-zkvm = { workspace = true } randprotocol-prover = { workspace = true } +# The private multisig signer checks (summary, signing message, share) every app runs. +randprotocol-msig = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } diff --git a/core/crates/wallet-core/src/lib.rs b/core/crates/wallet-core/src/lib.rs index 10bd32b..eb30ccb 100644 --- a/core/crates/wallet-core/src/lib.rs +++ b/core/crates/wallet-core/src/lib.rs @@ -46,6 +46,8 @@ use randprotocol_zkvm::machine::{Backend, FriProfile}; use randprotocol_zkvm::notes::{Note, SpendKey, ViewingKey}; use randprotocol_zkvm::viewing::TxKey; +mod msauth; + /// The RPC namespace the fullnode defines (`rand_getCommitments`, …). A wire name: it follows /// the node, never this wallet. pub const RPC_NAMESPACE: &str = "rand"; @@ -56,7 +58,23 @@ pub const ADDRESS_HRP: &str = randprotocol_core::notes::ADDRESS_PREFIX; pub use randprotocol_core::UNITS_PER_RAND; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -/// The fullnode commit the vendored chain crates come from (core/vendor/fullnode): fullnode `main` +/// The fullnode commit the vendored chain crates come from (core/vendor/fullnode): fullnode +/// tag `v0.8.0` (`c0878c3`: private multisig, constraint set 9, chain 21 onward; over `main` +/// `a1785c9` it adds the chain-21 cutover runbook and scripts and the release assets, with +/// `randprotocol-msig`, the proof guests and keys unchanged). Before it: `main` `a1785c9` +/// (origin/main: the private-multisig merge `9b01492` plus the verified fold, the +/// admission verified set folded into the verify cache, and a cut-script change; `randprotocol-msig` +/// and the proof guests and keys are unchanged). Before it: `main` `9b01492` (the merge of +/// `feat/private-multisig` `2771be22`) — over `e34117be` it adds the +/// signer's payee check to `randprotocol-msig` (`check_proposal_expecting` / `sign_expecting` +/// with `rand1…[:AMOUNT]` expected recipients, each output's `matches`, `recipients_checked`, the +/// summary's chain/genesis first line) and the `proposal-substituted-recipient` vector; the proof +/// guests and keys are `e34117be`'s. Before it: fullnode `feat/private-multisig` `e34117be` — +/// constraint set 9 (the MODQ chip: every proof key and the +/// proof encoding move; `hc_bundle` `60af094a…` and `hc_auth` `1e4e347f…` unchanged), the `msauth` +/// guest, `randprotocol-msig`; a node on it refuses chains 18–20, so this build is chain-21-only +/// (chain 20 fixes go to `release/chain20`). `dilithium-core` and the zkvm's `dilithium_host` are +/// gated off wasm32 in the vendored zkvm crate. Before it: fullnode `main` /// `37c4fdd` (the v0.8.0 candidate) — over `v0.7.0` it adds the new action kinds (perps 34–39, /// multisig 40–43, `SetRandPrice` 44), `Action::Aggregate`'s `payout_total` and `layout`, and the /// scan heads (`rand_getScanLeaves`); chain 20 keeps running on it with every gated action refused, @@ -82,9 +100,10 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// set 7 — the delegated prover, `randprotocol-prover`'s sealed job wire, unchanged since), /// `e6d1327`, `109f47d`, `1a13359` (`feat/address-sharing` on v0.5.9: the address fingerprint, /// `randpay:` links and the encrypted memo), `9c142c1` (v0.5.1). -pub const CHAIN_BUILD: &str = "37c4fdd"; -/// The chain the defaults below describe: chain 20, cut 2026-10-01 on fullnode v0.6.8 (`c9c9bd3`) -/// from chain 19's state, with `program_state`, `binding_domain: 1` and `bridge.fees` (10 bps each +pub const CHAIN_BUILD: &str = "c0878c3"; +/// The chain the defaults below describe: chain 21 (fullnode v0.8.0, constraint set 9; perps, +/// multisig, the `fees` section, `hc_auth_multisig`), cut from chain 20's state. Before it, chain +/// 20, cut 2026-10-01 on fullnode v0.6.8 (`c9c9bd3`) from chain 19's state, with `program_state`, `binding_domain: 1` and `bridge.fees` (10 bps each /// way) in its genesis. Before it, chain 19, the next cut (not live on 2026-09-30; the /// public node answered chain 18, genesis `a7cb020c…`, that day; chain 18 was the aim from /// 2026-09-29, 16 from 2026-09-28). Chain 19 is chain 18 re-cut on the same build (fullnode @@ -101,7 +120,7 @@ pub const CHAIN_BUILD: &str = "37c4fdd"; /// never built with either: without them both constants are the ones written here. pub const DEFAULT_CHAIN_ID: u64 = match option_env!("RAND_WALLET_CHAIN_ID") { Some(id) => parse_chain_id(id), - None => 20, + None => 21, }; pub const DEFAULT_RPC_URL: &str = match option_env!("RAND_WALLET_RPC_URL") { Some(url) => url, @@ -3939,6 +3958,14 @@ pub fn constants() -> Value { }) } +/// `msauth_check`/`msauth_sign`'s `expected_recipients`: absent or null is the empty list. +fn expected_recipients_param(params: &Value) -> Result> { + match params.get("expected_recipients") { + None | Some(Value::Null) => Ok(vec![]), + Some(v) => serde_json::from_value(v.clone()).map_err(|_| "expected_recipients must be a list of addresses".to_string()), + } +} + fn str_param<'a>(p: &'a Value, name: &str) -> Result<&'a str> { p.get(name).and_then(Value::as_str).ok_or_else(|| format!("missing string parameter {name:?}")) } @@ -4140,6 +4167,27 @@ fn asset_param(p: &Value) -> Result { /// `payment_commitment`/`payment_tx_key` from a `prove_transfer` reply — never index 0 of the /// four-wide arrays, which on a RAND transfer is a dummy slot /// - `format_amount` `{units}` → `"1.5"`; `parse_amount` `{text}` → units string +/// - `msauth_check` `{proposal, policy, expected_recipients?}` → the checked summary (fullnode +/// `check_proposal_expecting`; `expected_recipients` are `rand1…` or `rand1…:AMOUNT` texts the +/// crate parses, absent or empty meaning no payee named): each output's `owner_fingerprint`, +/// `to_account`, `amount` (string), `asset`, `memo`, `matches` (`"

( RAND)"` +/// or null) and `needs_payee`; `recipients_checked`; `expected` (`[{entry, fingerprint}]`, each +/// entry's owner fingerprint or null); `fee`/`burns` (strings), `expires_at`, +/// `signing_message` (hex), `policy_fingerprint`, `lines` (verbatim) and the proposal's +/// `chain_id`/`genesis`/`binding_domain`. A proposal paying anyone else, or leaving a named payee +/// unpaid or off its amount, is refused in the crate's sentence. No RPC: the caller compares +/// `expires_at` with its node's head (refuse at `head + 16 >= expires_at`) +/// - `msauth_verify_share` `{proposal, policy, share}` → `true` when `share` (`i:hex`) verifies +/// - `msauth_policy_info` `{policy}` → `{fingerprint, threshold, signers, address, policy_hash}` +/// - `msauth_signer_import` `{input, policy?}` → `{seed, public_key, address, index}`: a `rand-node +/// keygen` file or 64 hex; a file whose `public_key`/`address` is not its seed's is refused; with +/// `policy`, a key outside it is refused ("this key is not one of the policy's signers"), and +/// `index` is its position (null without a policy) +/// - `msauth_sign` `{proposal, policy, seed, expected_recipients}` → `"i:hex"`: every `msauth_check` +/// check through the crate's `sign_expecting` (the payee check included), then the deterministic +/// Dilithium2 share. `expected_recipients` absent or empty while any output pays someone (not +/// change, amount > 0) is refused: "no expected recipient given: the payee was NOT checked". +/// `seed` (64 hex) is wiped here after use; the caller wipes its own copy /// - `fixture_prove_request` `{profile?, asset?}` → a valid `prove_transfer` request for smoke /// tests: a RAND transfer at `asset: 0` (the default), a token transfer at `asset >= 1` /// - `fixture_burn_request` `{profile?}` → a valid `prove_burn` request for smoke tests @@ -4328,6 +4376,17 @@ pub fn dispatch(method: &str, params: &Value) -> Result { "fixture_burn_request" => Ok(fixture_burn_request(params.get("profile").and_then(Value::as_str).unwrap_or("test"))?), "format_amount" => Ok(Value::String(format_amount(amount_param(params, "units")?))), "parse_amount" => Ok(Value::String(parse_amount(str_param(params, "text")?).map_err(|e| e.to_string())?.to_string())), + // Private multisig signing (msauth.rs). + "msauth_check" => msauth::check(str_param(params, "proposal")?, str_param(params, "policy")?, &expected_recipients_param(params)?), + "msauth_sign" => msauth::sign( + str_param(params, "proposal")?, + str_param(params, "policy")?, + str_param(params, "seed")?, + &expected_recipients_param(params)?, + ), + "msauth_signer_import" => msauth::signer_import(str_param(params, "input")?, params.get("policy").and_then(Value::as_str)), + "msauth_verify_share" => msauth::verify_share(str_param(params, "proposal")?, str_param(params, "policy")?, str_param(params, "share")?), + "msauth_policy_info" => msauth::policy_info(str_param(params, "policy")?), other => bad(format!("unknown method {other:?}")), } } @@ -5284,7 +5343,7 @@ mod tests { assert_eq!(ADDRESS_HRP, "rand1"); // The wire names come from upstream, not from a second literal here. assert_eq!(ADDRESS_HRP, randprotocol_core::notes::ADDRESS_PREFIX); - assert_eq!(v["default_chain_id"], 20); + assert_eq!(v["default_chain_id"], 21); assert!(v.get("units_per_rand").is_some()); } @@ -5298,7 +5357,7 @@ mod tests { fn json_entry_point_reports_errors_as_json() { let v: Value = serde_json::from_str(&call("version", "{}")).unwrap(); assert_eq!(v["ok"], true); - assert_eq!(v["value"]["default_chain_id"], 20); + assert_eq!(v["value"]["default_chain_id"], 21); assert_eq!(v["value"]["bundle_base_fee"], "1000000"); let v: Value = serde_json::from_str(&call("wallet_info", r#"{"spend_key":"zz"}"#)).unwrap(); assert_eq!(v["ok"], false); @@ -6115,12 +6174,20 @@ mod tests { assert!(v["error"].as_str().unwrap().contains("decimal strings"), "{v}"); } + /// The vendored private multisig signer checks link into the core (fullnode 9b01492). + #[test] + fn the_vendored_msig_crate_is_linked() { + assert_eq!(randprotocol_msig::POLICY_VERSION, 1); + assert_eq!(randprotocol_msig::SIG_BYTES, 2420); + assert_eq!(randprotocol_msig::PK_BYTES, 1312); + } + /// `version` reports the chain-14 bundle's constants, on a build aimed at chain 19. #[test] fn version_reports_chain_fourteen() { let v = constants(); - assert_eq!(v["default_chain_id"], 20); - assert_eq!(v["chain_build"], "37c4fdd"); + assert_eq!(v["default_chain_id"], 21); + assert_eq!(v["chain_build"], "c0878c3"); assert_eq!(v["rpl_transfer"], true); assert_eq!(v["bridge_burn"], true); assert_eq!(v["bridge_burn_proofs"], 1); @@ -7117,12 +7184,25 @@ mod tests { assert!(e.contains("tier 13"), "{e}"); // The same proof against another transaction (the chain id moved): the digest still // matches — it covers the bundle, not the chain — so only the verify refuses it. + // The fixture's chain is chain-id bound (14–19); the next one binds its genesis + // (BIND-1), so the moved pending names one, as a pending for that chain always does — + // without it `finish_proof` refuses before the verify, which is not what is tested here. let mut other = out.pending.clone(); let mut tx = Transaction::decode(&hex::decode(&other.tx_hex).unwrap()).unwrap(); tx.chain_id += 1; other.tx_hex = hex::encode(tx.encode()); + if !CHAIN_ID_BINDING_CHAIN_IDS.contains(&tx.chain_id) { + other.genesis = Some(hex::encode([0x5a; 32])); + } let e = finish(&other, &good).err().unwrap(); assert!(e.contains("does not verify"), "{e}"); + // And BIND-1 still holds: the same move without a genesis is refused before any verify. + if !CHAIN_ID_BINDING_CHAIN_IDS.contains(&tx.chain_id) { + let mut bare = other.clone(); + bare.genesis = None; + let e = finish(&bare, &good).err().unwrap(); + assert!(e.contains("BIND-1"), "{e}"); + } } } @@ -7285,6 +7365,27 @@ mod tests { fn verify_auth(&self, hc: &Word8, proof: &[u8], binding: &[u32; TX_BINDING_WORDS]) -> std::result::Result { StubExecutor.verify_auth(hc, proof, binding) } + // Constraint set 9 (fullnode e34117be): the auth proof's shape and the msauth key, the + // wrapped executor's (a deploy proves no multisig authorisation; none is called). + fn auth_header(&self, proof: &[u8]) -> std::result::Result<(randprotocol_core::confidential::AuthShape, Word8, Option), ConfidentialError> { + self.0.auth_header(proof) + } + fn warm_auth_multisig(&self) { + self.0.warm_auth_multisig() + } + fn auth_multisig_key_ready(&self) -> bool { + self.0.auth_multisig_key_ready() + } + fn install_msauth_vk(&self, blob: &[u8]) -> std::result::Result<(), String> { + // The trait's, not ZkExecutor's inherent method of the same name (a typed error). + ConfidentialExecutor::install_msauth_vk(self.0, blob) + } + fn enable_auth_multisig(&self) { + self.0.enable_auth_multisig() + } + fn build_msauth_key(&self) -> std::result::Result<(), String> { + self.0.build_msauth_key() + } fn verify_bundle(&self, hc: &Word8, proof: &[u8], binding: &[u32; TX_BINDING_WORDS]) -> std::result::Result<(), ConfidentialError> { StubExecutor.verify_bundle(hc, proof, binding) } diff --git a/core/crates/wallet-core/src/msauth.rs b/core/crates/wallet-core/src/msauth.rs new file mode 100644 index 0000000..31a4caa --- /dev/null +++ b/core/crates/wallet-core/src/msauth.rs @@ -0,0 +1,465 @@ +//! Private multisig signing (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md §2): the +//! `msauth_*` methods of [`crate::dispatch`], over fullnode's `randprotocol-msig` — the same checks +//! the `rand multisig-private` CLI makes. Pure: no I/O. A policy carries the account's `msk` and a +//! proposal its witness; neither is ever written into an error. +use crate::Result; +use randprotocol_core::notes::word8_to_hex; +use randprotocol_msig::{self as msig, Policy, Proposal}; +use randprotocol_core::Keypair; +use serde_json::{json, Value}; +use zeroize::Zeroizing; + +fn err(e: msig::MsigError) -> String { + e.to_string() +} + +/// A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. +pub fn clean(s: &str) -> &str { + s.strip_prefix('\u{feff}').unwrap_or(s).trim() +} + +fn parse_policy(policy: &str) -> Result { + let p: Policy = serde_json::from_str(clean(policy)).map_err(|e| format!("the policy does not parse: {e}"))?; + p.check().map_err(err)?; + Ok(p) +} + +/// An output a signer must name the payee of, as the crate's payee check counts them: not change, +/// and moving value (a zero-amount output is a dummy slot). +fn needs_payee(o: &msig::OutputSummary) -> bool { + !o.to_account && o.amount != 0 +} + +/// Each expected-recipient entry as the caller gave it, with the owner fingerprint +/// ([`msig::owner_fingerprint_of_text`]) of its address part — the text before the last `:` when +/// what follows parses as an amount, otherwise the whole entry — or null when that does not parse +/// as an address (the crate refuses such an entry anyway). An output's `owner_fingerprint` equal +/// to an entry's `fingerprint` pays that entry. +pub fn expected_entries(expected_recipients: &[String]) -> Value { + let rows: Vec = expected_recipients + .iter() + .map(|entry| { + let text = clean(entry); + let addr = match text.rsplit_once(':') { + Some((a, amt)) if randprotocol_core::parse_amount(amt.trim()).is_ok() => a, + _ => text, + }; + json!({ "entry": entry, "fingerprint": msig::owner_fingerprint_of_text(addr).ok() }) + }) + .collect(); + Value::Array(rows) +} + +/// `check_proposal_expecting`, re-shaped to this crate's wire rule (amounts as decimal strings), +/// with fullnode's `lines()` verbatim, each output's `matches` (the crate's `"
+/// ( RAND)"`, or null) and `needs_payee`, the crate's `recipients_checked` (false when +/// `expected_recipients` is empty: then no payee was checked and the lines say so), `expected` +/// ([`expected_entries`]), and the chain the proposal names (`chain_id`, `genesis`, +/// `binding_domain`) for the caller's comparison with its node. `expected_recipients` are +/// `rand1…` or `rand1…:AMOUNT` texts, passed to the crate as given (it parses the amounts). A +/// proposal paying anyone else, leaving an entry unpaid or missing an amount pin is refused in the +/// crate's own sentence. +pub fn check(proposal: &str, policy: &str, expected_recipients: &[String]) -> Result { + let (proposal, policy) = (clean(proposal), clean(policy)); + let s = msig::check_proposal_expecting(proposal, policy, expected_recipients).map_err(err)?; + let p: Proposal = serde_json::from_str(proposal).map_err(|e| format!("the proposal does not parse: {e}"))?; + let outputs: Vec = s + .outputs + .iter() + .map(|o| json!({ + "owner_fingerprint": o.owner_fingerprint, "to_account": o.to_account, "amount": o.amount.to_string(), + "asset": o.asset, "memo": o.memo, "matches": o.matches, "needs_payee": needs_payee(o), + })) + .collect(); + Ok(json!({ + "outputs": outputs, + "recipients_checked": s.recipients_checked, + "expected": expected_entries(expected_recipients), + "fee": s.fee.to_string(), + "burns": { "burn_a": s.burns.burn_a.to_string(), "burn_asset": s.burns.burn_asset, "burn_r": s.burns.burn_r.to_string() }, + "expires_at": s.expires_at, + "signing_message": hex::encode(s.signing_message), + "policy_fingerprint": s.policy_fingerprint, + "lines": s.lines(), + "chain_id": s.chain_id, + "genesis": s.genesis, + "binding_domain": p.binding_domain, + })) +} + +/// `verify_share`: `true`, or the refusal. +pub fn verify_share(proposal: &str, policy: &str, share: &str) -> Result { + msig::verify_share(clean(proposal), clean(policy), clean(share)).map_err(err)?; + Ok(Value::Bool(true)) +} + +/// What a signer compares out of band before saving a profile. +pub fn policy_info(policy: &str) -> Result { + let p = parse_policy(policy)?; + Ok(json!({ + "fingerprint": p.fingerprint(), + "threshold": p.threshold, + "signers": p.signers.len(), + "address": p.address().to_string(), + "policy_hash": word8_to_hex(&p.policy_hash()), + })) +} + +/// What a signer key looks like, when the input is neither. +pub const SIGNER_KEY_SHAPE: &str = "a signer key is a rand-node keygen file or 64 hex characters"; + +/// `msauth_sign`'s refusal when no payee was named but the proposal moves value to someone: the +/// core never makes a share whose payee nobody checked, whatever the caller did before. +pub const NO_PAYEE_NAMED: &str = "no expected recipient given: the payee was NOT checked"; + +const SEED_SHAPE: &str = "a signer seed is 64 hex characters"; + +/// A seed in a buffer that is wiped when dropped. The error never repeats the input. +fn seed_from_hex(s: &str) -> Result> { + let mut seed = Zeroizing::new([0u8; 32]); + hex::decode_to_slice(clean(s), &mut seed[..]).map_err(|_| SEED_SHAPE.to_string())?; + Ok(seed) +} + +/// A `rand-node keygen` file (`{seed, address, public_key}`) or a bare 64-hex seed: the seed, the +/// key it derives, and — given the policy — this key's position in it (refused if absent). A file +/// whose `public_key` or `address` is not its seed's is refused. No error repeats the seed. +pub fn signer_import(input: &str, policy: Option<&str>) -> Result { + let input = clean(input); + let (seed, claimed_pk, claimed_address) = if input.starts_with('{') { + let v: Value = serde_json::from_str(input).map_err(|_| SIGNER_KEY_SHAPE.to_string())?; + let s = v["seed"].as_str().ok_or(SIGNER_KEY_SHAPE)?; + (seed_from_hex(s).map_err(|_| SIGNER_KEY_SHAPE.to_string())?, v["public_key"].as_str().map(str::to_owned), v["address"].as_str().map(str::to_owned)) + } else { + (seed_from_hex(input).map_err(|_| SIGNER_KEY_SHAPE.to_string())?, None, None) + }; + let kp = Keypair::from_seed(*seed).map_err(|e| format!("the signer seed: {e}"))?; + let public_key = kp.public_key().to_hex(); + if claimed_pk.is_some_and(|c| !c.trim().eq_ignore_ascii_case(&public_key)) { + return crate::bad("the key file's public_key is not the one its seed derives"); + } + let address = kp.address().to_base58(); + if claimed_address.is_some_and(|a| a.trim() != address) { + return crate::bad("the key file's address is not the one its seed derives"); + } + let index = match policy { + None => Value::Null, + Some(pol) => { + let pks = parse_policy(pol)?.pks().map_err(err)?; + let i = pks.iter().position(|k| k[..] == kp.public_key().as_bytes()[..]).ok_or("this key is not one of the policy's signers")?; + json!(i) + } + }; + Ok(json!({ "seed": hex::encode(&seed[..]), "public_key": public_key, "address": address, "index": index })) +} + +/// `sign`: the share `"i:hex"`, through the crate's `sign_expecting` — every check [`check`] +/// makes, the payee check included, runs first. With `expected_recipients` empty, a proposal with +/// any paying output (not change, amount > 0) is refused ([`NO_PAYEE_NAMED`]); one paying no one +/// (all change or zero-amount slots) signs. The seed lives in a wiped buffer. +pub fn sign(proposal: &str, policy: &str, seed_hex: &str, expected_recipients: &[String]) -> Result { + let seed = seed_from_hex(seed_hex)?; + let (proposal, policy) = (clean(proposal), clean(policy)); + if expected_recipients.is_empty() { + let s = msig::check_proposal_expecting::<&str>(proposal, policy, &[]).map_err(err)?; + if s.outputs.iter().any(needs_payee) { + return crate::bad(NO_PAYEE_NAMED); + } + } + let (i, sig) = msig::sign_expecting(proposal, policy, &seed, expected_recipients).map_err(err)?; + Ok(Value::String(format!("{i}:{}", hex::encode(sig)))) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../ui/test/fixtures/msauth/"); + const VENDORED: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../vendor/fullnode/crates/randprotocol-msig/tests/fixtures/"); + pub(super) fn fx(name: &str) -> String { + std::fs::read_to_string(format!("{DIR}{name}.json")).unwrap() + } + pub(super) fn expected() -> Value { + serde_json::from_str(&fx("expected")).unwrap() + } + + /// The valid proposal's real payee: the address its informational summary names + /// ("pay 10 RAND to rand1…", fullnode's generator). Read from there, never from the app's view. + pub(super) fn payee() -> String { + let p: Value = serde_json::from_str(&fx("proposal-valid")).unwrap(); + let a = p["summary"][0].as_str().unwrap().rsplit(' ').next().unwrap().to_string(); + assert_eq!(a, expected()["recipient"].as_str().unwrap(), "the generator's recipient"); + a + } + + /// The coordinator paid its own key in the payee's slot: without a payee named the crate's + /// checks pass (and say the payee was not checked); with the payee's real address it refuses, + /// naming the output and the owner key it pays, in the crate's own sentence. + #[test] + fn a_substituted_recipient_is_refused_naming_the_output() { + let (prop, pol) = (fx("proposal-substituted-recipient"), fx("policy")); + let e = check(&prop, &pol, &[payee()]).unwrap_err(); + assert_eq!(e, "output 2 pays b9856a063c1fb742, not an expected recipient"); + assert_eq!(e, expected()["substituted_refusal"].as_str().unwrap()); + assert_eq!(check(&prop, &pol, &[format!("{}:10", payee())]).unwrap_err(), e, "an amount pin changes nothing"); + let plain = check(&prop, &pol, &[]).unwrap(); + assert_eq!(plain["recipients_checked"], false); + assert_eq!(plain["outputs"][2]["owner_fingerprint"], "b9856a063c1fb742"); + assert!(plain["outputs"][2]["matches"].is_null()); + assert_eq!(plain["outputs"][2]["needs_payee"], true); + assert_eq!(plain["lines"][1], randprotocol_msig::NO_EXPECTATION); + } + + #[test] + fn the_valid_proposal_matches_its_payee_and_nothing_given_checks_nothing() { + let v = check(&fx("proposal-valid"), &fx("policy"), &[payee()]).unwrap(); + assert_eq!(v["recipients_checked"], true); + assert_eq!(v["outputs"][2]["matches"], "A5SR-HRMB-SY4M-5ZYF (10 RAND)"); + assert!(v["lines"][3].as_str().unwrap().contains(" matches A5SR-HRMB-SY4M-5ZYF (10 RAND) "), "{}", v["lines"][3]); + let none = check(&fx("proposal-valid"), &fx("policy"), &[]).unwrap(); + assert_eq!(none["recipients_checked"], false); + assert!(none["outputs"].as_array().unwrap().iter().all(|o| o["matches"].is_null())); + assert!(none["lines"].as_array().unwrap().iter().any(|l| l.as_str().unwrap().contains("no expected recipient given: the payee was NOT checked"))); + assert_eq!(none["expected"], json!([])); + let e = check(&fx("proposal-valid"), &fx("policy"), &["rand1nope".into()]).unwrap_err(); + assert!(e.starts_with("expected recipient 0 is not a shielded address"), "{e}"); + } + + /// `rand1…:AMOUNT` pins the RAND paid; the crate parses the amount (wallet-core does not). + #[test] + fn an_amount_pin_is_met_exactly_or_refused() { + let (prop, pol) = (fx("proposal-valid"), fx("policy")); + for ok in [format!("{}:10", payee()), format!("{}:10.0", payee()), format!(" {}:10 \n", payee())] { + assert_eq!(check(&prop, &pol, &[ok.clone()]).unwrap()["recipients_checked"], true, "{ok:?}"); + } + assert_eq!(check(&prop, &pol, &[format!("{}:9", payee())]).unwrap_err(), "expected recipient A5SR-HRMB-SY4M-5ZYF is paid 10 RAND, not 9 RAND"); + let e = check(&prop, &pol, &[format!("{}:ten", payee())]).unwrap_err(); + assert!(e.starts_with("expected recipient 0 has a malformed amount"), "{e}"); + } + + /// `expected`: each entry as given, with the owner fingerprint of its address part (what the + /// output lines' `owner_fingerprint` is), null when the address does not parse. + #[test] + fn expected_names_each_entry_with_its_owner_fingerprint() { + let fp = expected()["recipient_owner_fingerprint"].as_str().unwrap().to_string(); + assert_eq!(fp, "35bad5bd21a6f0f7"); + let entries = [payee(), format!("{}:10", payee())]; + let v = check(&fx("proposal-valid"), &fx("policy"), &entries).unwrap(); + assert_eq!(v["expected"], json!([{ "entry": entries[0], "fingerprint": fp }, { "entry": entries[1], "fingerprint": fp }])); + assert_eq!(v["outputs"][2]["owner_fingerprint"], json!(fp), "the row's fingerprint is the output's"); + let raw = format!(" {}:1.5\n", payee()); + assert_eq!( + expected_entries(&[raw.clone(), "rand1nope".into(), "rand1nope:10".into(), format!("{}:nope", payee())]), + json!([ + { "entry": raw, "fingerprint": fp }, + { "entry": "rand1nope", "fingerprint": null }, + { "entry": "rand1nope:10", "fingerprint": null }, + { "entry": format!("{}:nope", payee()), "fingerprint": null }, + ]) + ); + } + + #[test] + fn the_fixture_copies_are_the_vendored_vectors() { + let names = |d: &str| -> Vec { + let mut v: Vec = std::fs::read_dir(d).unwrap().map(|e| e.unwrap().file_name().into_string().unwrap()).collect(); + v.sort(); + v + }; + assert_eq!(names(DIR), names(VENDORED), "the same files: re-copy after a re-pin (Task 3 Step 1)"); + assert_eq!(names(VENDORED).len(), 11); + for n in names(VENDORED) { + assert_eq!(std::fs::read(format!("{DIR}{n}")).unwrap(), std::fs::read(format!("{VENDORED}{n}")).unwrap(), "{n}"); + } + } + + #[test] + fn check_returns_the_expected_summary_with_amounts_as_strings() { + let v = check(&fx("proposal-valid"), &fx("policy"), &[payee()]).unwrap(); + let want = &expected()["summary_expecting_recipient"]; + assert_eq!(v["outputs"].as_array().unwrap().len(), 4); + for (i, o) in want["outputs"].as_array().unwrap().iter().enumerate() { + let got = &v["outputs"][i]; + assert_eq!(got["owner_fingerprint"], o["owner_fingerprint"]); + assert_eq!(got["to_account"], o["to_account"]); + assert_eq!(got["amount"], json!(o["amount"].as_u64().unwrap().to_string()), "amounts are strings"); + assert_eq!((&got["asset"], &got["memo"], &got["matches"]), (&o["asset"], &o["memo"], &o["matches"])); + assert_eq!(got["needs_payee"], json!(!o["to_account"].as_bool().unwrap() && o["amount"].as_u64().unwrap() != 0)); + } + assert_eq!(v["recipients_checked"], want["recipients_checked"]); + assert_eq!(v["fee"], "1000000"); + assert_eq!(v["burns"], json!({ "burn_a": "0", "burn_asset": 0, "burn_r": "0" })); + assert_eq!(v["expires_at"], 2024); + assert_eq!(v["signing_message"], expected()["signing_message"]); + assert_eq!(v["policy_fingerprint"], "NFYW-K8E9-E5K9-AAK3"); + assert_eq!((&v["chain_id"], &v["binding_domain"], &v["genesis"]), (&json!(7), &json!(0), &json!("0".repeat(64)))); + let lines = randprotocol_msig::check_proposal_expecting(&fx("proposal-valid"), &fx("policy"), &[payee()]).unwrap().lines(); + assert_eq!(v["lines"], json!(lines), "fullnode's lines, verbatim"); + assert_eq!(v["lines"], expected()["lines_expecting_recipient"]); + assert_eq!(lines[0], format!("chain 7 · genesis {}", "0".repeat(64))); + for l in &lines[1..5] { + assert!(l.contains("(owner key verified; delivery key NOT verifiable from this proposal)"), "{l}"); + } + assert!(lines[4].contains("this account 4333dd507bcc4286"), "change is labelled: {}", lines[4]); + assert!(lines.iter().all(|l| !l.contains("rand1")), "no address from the proposal's summary"); + let none = check(&fx("proposal-valid"), &fx("policy"), &[]).unwrap(); + assert_eq!(none["lines"], expected()["lines"]); + } + + #[test] + fn each_bad_proposal_is_refused_by_name() { + for (name, words) in [ + ("proposal-tampered-output", "does not open as this account's"), + ("proposal-unbound", "does not hash to the proposal's binding"), + ("proposal-wrong-expected", "expected digest is not its bundle's"), + ("proposal-fee-raised", "the witness's value does not match the outputs: asset 0"), + ("proposal-value-mismatch", "the witness's value does not match"), + ("proposal-expired", "the proposal expires at"), + ] { + for given in [vec![], vec![payee()]] { + let e = check(&fx(name), &fx("policy"), &given).unwrap_err(); + assert!(e.contains(words), "{name}: {e}"); + } + } + } + + #[test] + fn a_bom_and_whitespace_around_a_file_are_tolerated() { + assert_eq!(clean("\u{feff} {}\r\n"), "{}"); + check(&format!("\u{feff}{}\r\n", fx("proposal-valid")), &format!("\u{feff} {}\n", fx("policy")), &[format!(" {}\n", payee())]).unwrap(); + verify_share(&format!("\u{feff}{}", fx("proposal-valid")), &fx("policy"), &format!("{}\n", expected()["share_0"].as_str().unwrap())).unwrap(); + policy_info(&format!("\u{feff}{}\n", fx("policy"))).unwrap(); + } + + #[test] + fn verify_share_takes_the_expected_shares_and_refuses_others() { + let (prop, pol) = (fx("proposal-valid"), fx("policy")); + for k in ["share_0", "share_2"] { + assert_eq!(verify_share(&prop, &pol, expected()[k].as_str().unwrap()).unwrap(), Value::Bool(true)); + } + let sig = expected()["share_2"].as_str().unwrap().split_once(':').unwrap().1.to_string(); + assert!(verify_share(&prop, &pol, &format!("1:{sig}")).unwrap_err().contains("signer 1's signature does not verify")); + assert!(verify_share(&prop, &pol, &format!("7:{sig}")).unwrap_err().contains("signer 7 is not in this policy")); + assert!(verify_share(&prop, &pol, "2:zz").unwrap_err().contains("not hex")); + } + + #[test] + fn policy_info_names_the_account_every_signer_compares() { + let v = policy_info(&fx("policy")).unwrap(); + assert_eq!((&v["fingerprint"], &v["threshold"], &v["signers"]), (&json!("NFYW-K8E9-E5K9-AAK3"), &json!(2), &json!(3))); + let prop: Value = serde_json::from_str(&fx("proposal-valid")).unwrap(); + assert_eq!(v["policy_hash"], prop["policy"], "the hash a proposal names"); + assert!(v["address"].as_str().unwrap().starts_with("rand1")); + assert!(policy_info("{\"version\":1,\"threshold\":0,\"signers\":[],\"msk\":\"00\"}").is_err()); + assert!(policy_info("not json").unwrap_err().starts_with("the policy does not parse")); + } + + #[test] + fn the_dispatch_arms_answer_through_call() { + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "expected_recipients": [format!("{}:10", payee())] }); + let r: Value = serde_json::from_str(&crate::call("msauth_check", &p.to_string())).unwrap(); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(r["value"]["recipients_checked"], true); + assert_eq!(r["value"]["expected"][0]["fingerprint"], "35bad5bd21a6f0f7"); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy") }); + let r: Value = serde_json::from_str(&crate::call("msauth_check", &p.to_string())).unwrap(); + assert_eq!(r["value"]["recipients_checked"], false, "{r}"); + let p = json!({ "proposal": fx("proposal-substituted-recipient"), "policy": fx("policy"), "expected_recipients": [payee()] }); + let r: Value = serde_json::from_str(&crate::call("msauth_check", &p.to_string())).unwrap(); + assert_eq!((&r["ok"], &r["error"]), (&json!(false), &expected()["substituted_refusal"]), "{r}"); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "expected_recipients": [7] }); + let r: Value = serde_json::from_str(&crate::call("msauth_check", &p.to_string())).unwrap(); + assert_eq!((&r["ok"], &r["error"]), (&json!(false), &json!("expected_recipients must be a list of addresses")), "the sentence ui/engine/core-errors.js translates"); + let r: Value = serde_json::from_str(&crate::call("msauth_policy_info", &json!({ "policy": fx("policy") }).to_string())).unwrap(); + assert_eq!(r["value"]["threshold"], 2); + let s = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "share": expected()["share_0"] }); + let r: Value = serde_json::from_str(&crate::call("msauth_verify_share", &s.to_string())).unwrap(); + assert_eq!(r["value"], true); + } + + fn seeds() -> (Vec, String) { + let v: Value = serde_json::from_str(&fx("signer-seeds")).unwrap(); + (v["members"].as_array().unwrap().iter().map(|s| s.as_str().unwrap().to_string()).collect(), v["non_member"].as_str().unwrap().to_string()) + } + + #[test] + fn members_0_and_2_sign_the_expected_shares() { + let (m, _) = seeds(); + for (i, want) in [(0usize, "share_0"), (2, "share_2")] { + for given in [vec![payee()], vec![format!("{}:10", payee())]] { + let share = sign(&fx("proposal-valid"), &fx("policy"), &m[i], &given).unwrap(); + assert_eq!(share, expected()[want], "deterministic Dilithium2 reproduces the vector"); + } + } + } + + #[test] + fn an_outsider_a_bad_proposal_or_a_bad_seed_signs_nothing() { + let (m, outsider) = seeds(); + assert!(sign(&fx("proposal-valid"), &fx("policy"), &outsider, &[payee()]).unwrap_err().contains("not one of the policy's signers")); + assert!(sign(&fx("proposal-unbound"), &fx("policy"), &m[0], &[payee()]).unwrap_err().contains("binding")); + for bad in ["zz", "", &m[0][..62], &format!("{}00", m[0])] { + let e = sign(&fx("proposal-valid"), &fx("policy"), bad, &[payee()]).unwrap_err(); + assert_eq!(e, "a signer seed is 64 hex characters", "never echoes the seed"); + } + } + + /// The core itself checks the payee: a share is never made for a proposal paying someone the + /// signer did not name, nor for a paying proposal when no payee was named at all. + #[test] + fn sign_refuses_an_unnamed_or_substituted_payee() { + let (m, _) = seeds(); + assert_eq!(sign(&fx("proposal-valid"), &fx("policy"), &m[0], &[]).unwrap_err(), NO_PAYEE_NAMED); + assert_eq!(NO_PAYEE_NAMED, "no expected recipient given: the payee was NOT checked"); + assert_eq!( + sign(&fx("proposal-substituted-recipient"), &fx("policy"), &m[0], &[payee()]).unwrap_err(), + expected()["substituted_refusal"].as_str().unwrap() + ); + assert_eq!( + sign(&fx("proposal-valid"), &fx("policy"), &m[0], &[format!("{}:9", payee())]).unwrap_err(), + "expected recipient A5SR-HRMB-SY4M-5ZYF is paid 10 RAND, not 9 RAND" + ); + assert!(sign(&fx("proposal-valid"), &fx("policy"), &m[0], &["rand1nope".into()]).unwrap_err().starts_with("expected recipient 0 is not a shielded address")); + } + + #[test] + fn import_reads_a_keygen_file_or_bare_hex_and_finds_the_position() { + let (m, outsider) = seeds(); + let kp = randprotocol_core::Keypair::from_seed(hex::decode(&m[2]).unwrap().try_into().unwrap()).unwrap(); + let file = json!({ "seed": m[2], "address": kp.address().to_base58(), "public_key": kp.public_key().to_hex() }).to_string(); + let v = signer_import(&file, Some(&fx("policy"))).unwrap(); + assert_eq!((&v["seed"], &v["index"], &v["public_key"]), (&json!(m[2]), &json!(2), &json!(kp.public_key().to_hex()))); + assert_eq!(v["address"], json!(kp.address().to_base58())); + assert_eq!(signer_import(&format!("\u{feff}{file}\n"), None).unwrap()["seed"], json!(m[2]), "a BOM and whitespace are tolerated"); + assert_eq!(signer_import(&m[0], Some(&fx("policy"))).unwrap()["index"], 0); + assert_eq!(signer_import(&format!(" {}\n", m[0]), None).unwrap()["index"], Value::Null); + assert_eq!(signer_import(&outsider, Some(&fx("policy"))).unwrap_err(), "this key is not one of the policy's signers"); + let lying = json!({ "seed": m[2], "address": kp.address().to_base58(), "public_key": "00".repeat(1312) }).to_string(); + assert_eq!(signer_import(&lying, None).unwrap_err(), "the key file's public_key is not the one its seed derives"); + let lying = json!({ "seed": m[2], "address": "1111", "public_key": kp.public_key().to_hex() }).to_string(); + assert_eq!(signer_import(&lying, None).unwrap_err(), "the key file's address is not the one its seed derives"); + assert_eq!(signer_import("{\"spend_key\":\"aa\"}", None).unwrap_err(), SIGNER_KEY_SHAPE); + assert_eq!(signer_import("{\"seed\":\"zz\"}", None).unwrap_err(), SIGNER_KEY_SHAPE); + assert_eq!(signer_import("nope", None).unwrap_err(), SIGNER_KEY_SHAPE); + assert!(!signer_import(&format!("{}zz", m[0]), None).unwrap_err().contains(&m[0][..8]), "never echoes the seed"); + } + + #[test] + fn sign_and_import_answer_through_call() { + let (m, _) = seeds(); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "seed": m[0], "expected_recipients": [format!("{}:10", payee())] }); + let r: Value = serde_json::from_str(&crate::call("msauth_sign", &p.to_string())).unwrap(); + assert_eq!(r["value"], expected()["share_0"], "{r}"); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "seed": m[0] }); + let r: Value = serde_json::from_str(&crate::call("msauth_sign", &p.to_string())).unwrap(); + assert_eq!((&r["ok"], &r["error"]), (&json!(false), &json!(NO_PAYEE_NAMED)), "absent is empty: refused"); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "seed": m[0], "expected_recipients": "rand1x" }); + let r: Value = serde_json::from_str(&crate::call("msauth_sign", &p.to_string())).unwrap(); + assert_eq!(r["error"], "expected_recipients must be a list of addresses"); + let r: Value = serde_json::from_str(&crate::call("msauth_signer_import", &json!({ "input": m[2], "policy": fx("policy") }).to_string())).unwrap(); + assert_eq!(r["value"]["index"], 2, "{r}"); + let r: Value = serde_json::from_str(&crate::call("msauth_signer_import", &json!({ "input": m[2] }).to_string())).unwrap(); + assert_eq!(r["value"]["index"], Value::Null, "{r}"); + } +} diff --git a/core/scripts/vendor-fullnode.sh b/core/scripts/vendor-fullnode.sh new file mode 100755 index 0000000..cbe59f1 --- /dev/null +++ b/core/scripts/vendor-fullnode.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# Move core/vendor/fullnode (the submodule) to a fullnode commit, and everything that names it. +# +# core/scripts/vendor-fullnode.sh [] +# +# a fullnode commit (any form `git rev-parse` takes in the source). +# where to fetch it from: a local fullnode clone (read only — nothing is checked out +# there) or a URL. Default: the submodule's own `origin` (.gitmodules). A local clone is +# the way to vendor a commit not pushed yet. +# +# What it does, in order: +# 1. Populates the submodule if it is empty (a clone from , absorbed into this +# checkout's .git/modules, `origin` set back to .gitmodules' URL), otherwise fetches +# from into it; then checks out detached. +# 2. Checks the vendored tree still carries what the wallet's builds rely on: the zkvm crate +# gates `dilithium-core` and `dilithium_host` off wasm32 (its `asm!` does not build there), +# the [patch]ed Plonky3 forks core/Cargo.toml names exist, and `randprotocol-msig` comes +# with its tests/fixtures/*.json (listed with their SHA-256s, to compare with the fullnode), +# copied to ui/test/fixtures/msauth with their hash list ui/test/fixtures/msauth.sha256. +# 3. Rewrites CHAIN_BUILD (wallet-core) and the three tests that pin it (wallet-core's +# `version_reports_chain_fourteen`, the web core integration test, the iOS smoke test) from +# the old short hash to the new one. +# 4. Refreshes core/Cargo.lock and desktop/src-tauri/Cargo.lock (`cargo metadata`: new or +# changed path crates only, nothing upgraded). +# +# What it leaves to you (it prints the list): the prose that describes the build (CHAIN_BUILD's doc +# comment in wallet-core, README.md's submodule sentence), the test runs, and the commit. +set -euo pipefail +cd "$(dirname "$0")/../.." +ROOT=$(pwd) +REV=${1:?usage: core/scripts/vendor-fullnode.sh []} +SUB=core/vendor/fullnode +URL=$(git config -f .gitmodules submodule.$SUB.url) +SRC=${2:-$URL} + +# ── 1. the submodule at ──────────────────────────────────────────────────────────────────── +if [ ! -e "$SUB/.git" ]; then + rmdir "$SUB" 2>/dev/null || { echo "$SUB is not empty and not a checkout" >&2; exit 1; } + git clone -q --no-checkout "$SRC" "$SUB" + git -C "$SUB" remote set-url origin "$URL" + git submodule absorbgitdirs "$SUB" +fi +if ! git -C "$SUB" cat-file -e "$REV^{commit}" 2>/dev/null; then + git -C "$SUB" fetch -q "$SRC" "$REV" || git -C "$SUB" fetch -q "$SRC" +fi +FULL=$(git -C "$SUB" rev-parse --verify "$REV^{commit}") +git -C "$SUB" checkout -q --detach "$FULL" +NEW=${FULL:0:7} +echo "vendor-fullnode: $SUB at $FULL ($(git -C "$SUB" log -1 --format=%s "$FULL" | cut -c1-100))" + +# ── 2. what the wallet's builds rely on ───────────────────────────────────────────────────────── +fail() { echo "vendor-fullnode: $*" >&2; exit 1; } +ZK=$SUB/crates/randprotocol-zkvm +if grep -q dilithium-core "$ZK/Cargo.toml"; then + python3 - "$ZK/Cargo.toml" <<'EOF' || fail "randprotocol-zkvm's dilithium-core is not under cfg(not(target_arch = \"wasm32\")): the wasm build breaks" +import re, sys +s = open(sys.argv[1]).read() +sect = None +for line in s.splitlines(): + m = re.match(r'\s*\[(.*)\]\s*$', line) + if m: sect = m.group(1) + elif re.match(r'\s*dilithium-core\s*=', line): + sys.exit(0 if sect and 'not(target_arch = "wasm32")' in sect else 1) +sys.exit(1) +EOF + grep -B1 -E '^pub mod dilithium_host;' "$ZK/src/lib.rs" | grep -q 'cfg(not(target_arch = "wasm32"))' \ + || fail "randprotocol-zkvm's lib.rs does not gate dilithium_host off wasm32" + echo "vendor-fullnode: zkvm's dilithium-core / dilithium_host gated off wasm32 — ok" +fi +for p in $(sed -n 's/^p3-[a-z-]* = { path = "\(vendor\/fullnode\/vendor\/[^"]*\)".*/\1/p' core/Cargo.toml); do + [ -f "core/$p/Cargo.toml" ] || fail "core/Cargo.toml patches core/$p, which this commit does not carry" +done +MSIG=$SUB/crates/randprotocol-msig +if [ -d "$MSIG" ]; then + ls "$MSIG"/tests/fixtures/*.json >/dev/null 2>&1 || fail "randprotocol-msig has no tests/fixtures/*.json" + echo "vendor-fullnode: randprotocol-msig fixtures ($(ls "$MSIG"/tests/fixtures/*.json | wc -l | tr -d ' ')):" + ( cd "$MSIG/tests/fixtures" && shasum -a 256 *.json | sed 's/^/ /' ) + # The apps' copies (ui/test/fixtures/msauth) and their ONE hash list, rewritten from the vendored set. + rm -rf ui/test/fixtures/msauth && mkdir -p ui/test/fixtures/msauth + cp "$MSIG"/tests/fixtures/*.json ui/test/fixtures/msauth/ + ( cd ui/test/fixtures/msauth && shasum -a 256 *.json ) > ui/test/fixtures/msauth.sha256 + echo "vendor-fullnode: copied to ui/test/fixtures/msauth, hashes in ui/test/fixtures/msauth.sha256" +else + echo "vendor-fullnode: WARNING — this commit has no crates/randprotocol-msig, which wallet-core depends on" >&2 +fi + +# ── 3. CHAIN_BUILD and the tests that pin it ──────────────────────────────────────────────────── +LIB=core/crates/wallet-core/src/lib.rs +OLD=$(sed -n 's/^pub const CHAIN_BUILD: &str = "\([0-9a-f]*\)";/\1/p' "$LIB") +[ -n "$OLD" ] || fail "no CHAIN_BUILD in $LIB" +if [ "$OLD" != "$NEW" ]; then + python3 - "$OLD" "$NEW" "$LIB" web/wallet/test/core.integration.test.mjs ios/RandWalletTests/CoreSmokeTests.swift <<'EOF' +import sys +old, new, lib, web, ios = sys.argv[1:] +edits = { + lib: [(f'pub const CHAIN_BUILD: &str = "{old}";', f'pub const CHAIN_BUILD: &str = "{new}";'), + (f'assert_eq!(v["chain_build"], "{old}");', f'assert_eq!(v["chain_build"], "{new}");')], + web: [(f"assert.equal(k.chain_build, '{old}');", f"assert.equal(k.chain_build, '{new}');")], + ios: [(f'XCTAssertEqual(c.chainBuild, "{old}"', f'XCTAssertEqual(c.chainBuild, "{new}"')], +} +for path, pairs in edits.items(): + s = open(path).read() + for a, b in pairs: + if a not in s: + sys.exit(f"vendor-fullnode: {path} does not contain {a!r}") + s = s.replace(a, b) + open(path, "w").write(s) +EOF + echo "vendor-fullnode: CHAIN_BUILD $OLD → $NEW (wallet-core, its version test, the web and iOS tests)" +fi + +# ── 4. lockfiles ──────────────────────────────────────────────────────────────────────────────── +( cd core && cargo metadata --format-version 1 >/dev/null ) +( cd desktop/src-tauri && cargo metadata --format-version 1 >/dev/null ) + +cat <, store: State<' prover::rotate_pairing(&state, &prover::dir(), &store, prover::DEFAULT_ADDR).await } +/// A share file in the user's Downloads, never over an existing file. `name` is +/// `share-.txt`-shaped: letters, digits, `.`, `_`, `-`, ending `.txt`. +pub fn save_text_in(dir: &std::path::Path, name: &str, text: &str) -> Result { + let ok = name.ends_with(".txt") && name.len() <= 64 && name.chars().all(|c| c.is_ascii_alphanumeric() || "._-".contains(c)) && !name.starts_with('.'); + if !ok { + return Err(format!("{name:?} is not a file name this app writes")); + } + use std::io::Write; + // A name that is taken becomes `share-1-2.txt`, `-3`, … up to 99: a file is never overwritten. + let stem = &name[..name.len() - 4]; + for n in 1..=99u32 { + let path = if n == 1 { dir.join(name) } else { dir.join(format!("{stem}-{n}.txt")) }; + match std::fs::OpenOptions::new().write(true).create_new(true).open(&path) { + Ok(mut f) => { + f.write_all(text.as_bytes()).map_err(|e| e.to_string())?; + return Ok(path); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => return Err(format!("{}: {e}", path.display())), + } + } + Err(format!("{name:?}: 99 files of that name already exist")) +} + +#[tauri::command] +pub async fn save_text_to_downloads(app: tauri::AppHandle, name: String, text: String) -> Result { + use tauri::Manager; + let dir = app.path().download_dir().map_err(|e| e.to_string())?; + save_text_in(&dir, &name, &text).map(|p| p.display().to_string()) +} + #[cfg(test)] mod tests { use super::*; use serde_json::Value; + #[test] + fn save_text_in_writes_once_and_refuses_a_path() { + let dir = std::env::temp_dir().join(format!("rand-save-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let p = save_text_in(&dir, "share-1.txt", "1:ab\n").unwrap(); + assert_eq!(std::fs::read_to_string(&p).unwrap(), "1:ab\n"); + let q = save_text_in(&dir, "share-1.txt", "second").unwrap(); + assert_ne!(p, q, "a taken name gets a new file"); + assert_eq!(q.file_name().unwrap(), "share-1-2.txt"); + assert_eq!(std::fs::read_to_string(&p).unwrap(), "1:ab\n", "the first file is untouched"); + assert_eq!(std::fs::read_to_string(&q).unwrap(), "second"); + assert!(save_text_in(&dir, "../evil.txt", "x").is_err()); + assert!(save_text_in(&dir, "a.sh", "x").is_err()); + std::fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn core_call_is_a_passthrough_to_wallet_core() { // Byte for byte what calling the crate directly would have returned: this wrapper adds diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 2997176..0ad8ead 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -91,6 +91,7 @@ fn main() { commands::prover_stop, commands::prover_pairing_link, commands::prover_rotate_pairing, + commands::save_text_to_downloads, ]) .run(tauri::generate_context!()) .expect("Rand Wallet could not start"); diff --git a/desktop/ui-shell/backend-tauri.js b/desktop/ui-shell/backend-tauri.js index 0eb84b3..a05098d 100644 --- a/desktop/ui-shell/backend-tauri.js +++ b/desktop/ui-shell/backend-tauri.js @@ -73,6 +73,8 @@ async function makePlatform() { openExternal: (url) => invoke('plugin:opener|open_url', { url: String(url ?? '') }), copy: (text) => navigator.clipboard.writeText(String(text ?? '')), paste: () => navigator.clipboard.readText(), + // A share file into the user's Downloads (never over an existing file: a taken name gets -2, -3, …); resolves to its path. + saveText: ({ name, text }) => invoke('save_text_to_downloads', { name: String(name), text: String(text) }), }; try { platform.version = String(await invoke('app_version')); } catch { /* the Settings line is then simply absent */ } // "Prove for my other devices" (spec 2026-09-28 §5, src-tauri/src/prover.rs): the fullnode's diff --git a/docs/superpowers/plans/2026-10-12-msauth-signing.md b/docs/superpowers/plans/2026-10-12-msauth-signing.md new file mode 100644 index 0000000..8908c91 --- /dev/null +++ b/docs/superpowers/plans/2026-10-12-msauth-signing.md @@ -0,0 +1,2804 @@ +# Private Multisig Signing in Every App — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Execution method (decided): **subagent-driven**. + +**Goal:** Every wallet app — web, extension, desktop, iOS, Android — can hold signer profiles for a private multisig account and check, show, sign and share a proposal (`i:`), through fullnode's `randprotocol-msig`, with every signature re-authenticated and nothing signed without a live chain head. + +**Architecture:** `wallet-core` gains five `msauth_*` arms in `dispatch`, in a new module `core/crates/wallet-core/src/msauth.rs` over the vendored `randprotocol-msig` (pure, wasm32-clean); the wasm, FFI and Tauri bindings pick them up through the generic `call`. The shared JS engine gains an optional `signers` group (`ui/engine/signers.js`) that keeps profiles in the vault (policies in one record opened at unlock, each seed in its own record decrypted per signature with the password), compares the proposal's expiry with `rand_getHead`, and refuses with no node. Screens `#signers` / `#signer/` sit behind Settings. iOS (Keychain, seed item under `.userPresence`) and Android (Keystore AES key with per-use auth, BiometricPrompt `CryptoObject`) mirror it natively. A final devnet run signs one proposal from the web engine and from iOS, and the CLI proves and submits it. + +**Tech Stack:** Rust 1.98.1 (wallet-core, vendored fullnode crates), JavaScript ES modules (ui/, `node --test`, linkedom), Tauri 2 (desktop), Swift/SwiftUI + XCTest (iOS, xcodegen), Java + JUnit 4 (Android, Gradle). + +**Spec:** `docs/superpowers/specs/2026-10-11-msauth-signing-design.md` (this branch, head `639cae7` — §4 step 3: the signer enters each payee's address). Fullnode companions (read with `git -C ~/Github/randprotocol/fullnode show :`, never check out there): `e34117be:crates/randprotocol-msig/src/lib.rs`, `e34117be:crates/randprotocol-msig/tests/checks.rs`, `d5b2e652:docs/cli.md` §`rand multisig-private`. + +**Branch / base:** `feat/msauth-signing` in `~/rand-worktrees/clients-msauth` (local only, **never push**), off `main`. Task 1 runs on `feat/msauth-repin` in `~/rand-worktrees/clients-msauth-repin` and is merged into `feat/msauth-signing` before Task 2. + +## Global Constraints + +- Source of truth: the spec above. The library is fullnode `e34117be` `crates/randprotocol-msig` (API verified 2026-10-12): `check_proposal(&str, &str) -> Result`; `ProposalSummary::lines() -> Vec`; `sign(&str, &str, &[u8; 32]) -> Result<(u8, Vec), MsigError>` (deterministic Dilithium2 via `randprotocol_core::Keypair::from_seed`); `verify_share(&str, &str, &str) -> Result<(), MsigError>`; `Policy` (pub fields `version, threshold, signers, msk`; `check()`, `pks()`, `policy_hash()`, `address()`, `fingerprint()`); `Proposal` (pub `chain_id, genesis, binding_domain, expires_at, …`); `PK_BYTES = 1312`, `SIG_BYTES = 2420`. +- **Pending crate API (confirm against the final crate commit — 0e will send):** the fix wave adds to `randprotocol-msig` `owner_fingerprint_of(&ShieldedAddress) -> String`; an optional expected-recipients input to `check_proposal` — named here `check_proposal_expecting(proposal_json: &str, policy_json: &str, expected_recipients: Option<&[ShieldedAddress]>) -> Result`, with `check_proposal(p, q)` kept as the wrapper passing `None`; a per-output match field — named here `OutputSummary::matches: Option` (the matched address's fingerprint); output lines ending "matches
" or "no expected recipient given: the payee was NOT checked"; a chain-id + genesis line in `lines()`; a new fixture `proposal-substituted-recipient.json` and a changed `expected.json`. Every place this plan uses one of these names carries the tag **[confirm: 0e]**; on the final commit, rename to match and adjust nothing else. +- **Payees are checked, never assumed (spec §4 step 3, 2026-10-12):** before review the app asks for the expected recipient address of every output that is not `to_account` and pays a non-zero amount (a zero-amount output is a dummy slot — **[confirm: 0e]** that the crate treats it as needing no payee; if it does need one, drop the `amount != "0"` filter); `msauth_check` takes them as `expected_recipients: [address strings]`; signing is **refused** while any such output is unmatched or its address was left empty (refusal code `unchecked-recipient`). The app never signs on "no expected recipient given: the payee was NOT checked". +- The proposal's chain id and genesis (shown as the crate's chain-id/genesis line, and returned as `chain_id`/`genesis`/`binding_domain`) are compared with the node's `rand_chainId` and `rand_getGenesisHash`; a mismatch refuses. +- Output lines are fullnode's `lines()` **verbatim**; each output line carries the owner-key caveat `(owner key verified; delivery key NOT verifiable from this proposal)` at `e34117be`, and after the fix wave also "matches
" or "no expected recipient given: the payee was NOT checked" plus a chain-id/genesis line **[confirm: 0e]** for the exact wording — tests assert on the structured fields (`matches`, `recipients_checked`) and on `lines` being the crate's own output, not on a hand-copied sentence. They are never translated, reworded, or replaced by the proposal's informational `summary` (which no app shows). +- No node reachable → **refuse to sign**, naming the node URL tried, with Retry. Refuse when `head + 16 >= expires_at` (`EXPIRY_MARGIN = 16`, the CLI's `multisig-private prove` rule, fullnode `d5b2e652` `main.rs:1392/1505`). The head comes from `rand_getHead` on a node that passed the wallet's own chain check. +- Every signature re-authenticates: web/extension/desktop ask for the password and decrypt that profile's seed for one call; iOS reads the seed Keychain item (`.userPresence`) per signature with a fresh `LAContext`; Android decrypts with a Keystore key `setUserAuthenticationRequired(true)` through a per-use `BiometricPrompt` `CryptoObject`. The seed is never cached, logged, or held past the call. Listing/viewing a profile needs only the normal unlock. +- JavaScript cannot wipe a string: the engine decrypts the seed into a `Uint8Array` and `fill(0)`s it in `finally`; the hex copy it passes to the core becomes unreachable after the call; wallet-core keeps its copy in `zeroize::Zeroizing`. Say exactly this in code comments; claim no more. +- A proposal is never stored: not in `storage`, not in the session, not in a log, not in Settings. It lives in a screen-local variable and is dropped on leaving the screen. +- Profiles live only where the spend key lives: the vault (`ui/engine/crypto.js` AES-256-GCM / PBKDF2-SHA256 600 000), the iOS Keychain (`ThisDeviceOnly`), Android `KeyVault`'s EncryptedSharedPreferences + Keystore. Wiping the wallet removes every profile. +- Wire rule (wallet-core): amounts are decimal strings, heights/indices/counts JSON numbers. `msauth_check` therefore re-shapes `ProposalSummary` (whose amounts are `u64`). +- Confirmation before signing types the **last four characters of `policy_fingerprint`** (dashes ignored, case-insensitive) — the withdraw screen's pattern (`ui/screens/withdraw.js:109,403,848`). +- Profiles need an existing wallet on every platform in this plan (the vault password / the app's device-auth unlock gate them). The spec's "a profile does not need a spend key" is **deferred** (onboarding change in five apps) — flagged to the user, not built. +- 20 locales everywhere: `ui/locales/*.js` (held to `en.js` by `ui/test/i18n.test.mjs`), `ios/l10n/.json` merged into `ios/RandWallet/Localizable.xcstrings` (`scripts/i18n/native_check.py ios ` / `ios-merge`), `android/app/src/main/res/values*/strings.xml` (`native_check.py android values-`, `StringsParityTest`). +- After Task 1 `main` of this branch is **chain-21-only** (constraint set 9, `DEFAULT_CHAIN_ID = 21`); chain-20 fixes go to `origin/release/chain20` (`201757c`). +- Commits: every commit message ends with + `Co-Authored-By: Claude Opus 5.5 ` and `Claude-Session: https://claude.ai/code/session_01L6dmK1EC4QerMmqWR1h7d8`. No `cargo fmt`. Never edit `desktop/dist-ui/`, `web/wallet/dist/`, `extension/shared/core/` by hand (built copies). Never push. +- Disk: run `df -h /` before any build; **stop and report if free < 25 GiB** (46 GiB free on 2026-10-12). Standing approval exists to delete Time Machine local snapshots when free < ~40 GiB (`tmutil listlocalsnapshots /`, `tmutil deletelocalsnapshots `). +- Test commands (all from the worktree root unless stated): + - core: `cd core && cargo test --release` (the real-proof tests take minutes); one test: `cd core && cargo test --release -p wallet-core `; wasm32 check: `cd core && cargo check --target wasm32-unknown-unknown -p wallet-wasm --profile wasm`; wasm build: `core/scripts/build-wasm.sh` (writes `extension/shared/core/`). + - JS: `npm ci --prefix ui` once, then `node --test "ui/test/**/*.test.mjs"`; web/extension: `node --test "web/wallet/test/**/*.test.mjs" "extension/test/*.test.mjs"` (real-core tests skip unless the wasm is built). + - desktop: `cd desktop/src-tauri && node ../scripts/stage-ui.mjs && cargo test`. + - iOS (**runs here**: Xcode 27.0, xcodegen, simulators): `core/scripts/build-ios.sh` once (writes `ios/Frameworks/RandWalletCore.xcframework`, absent in this worktree), then `cd ios && xcodegen generate && xcodebuild -project RandWallet.xcodeproj -scheme RandWallet -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test` (the README's `iPhone 17` does not exist on this Mac). The simulator has no passcode: `.userPresence` items cannot be created there, so the Keychain ACL is unit-tested through a fake and checked by hand on a device (Task 10 Step 8). + - Android (**JVM unit tests run here** with env set; `/usr/bin/java` has no runtime): `cd android && JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" ANDROID_HOME="$HOME/Library/Android/sdk" ./gradlew testDebugUnitTest`. JVM tests cannot load the native core (`NativeCore` → `System.loadLibrary`), so core-dependent logic goes behind interfaces (the `ProverCore` pattern). No device/emulator run is part of the plan; BiometricPrompt + Keystore is checked by hand (Task 12 Step 9). + +## Review Focus + +0. **A substituted recipient** (the coordinator pays its own key; every other check passes): with the payee's real address entered, that output is unmatched and signing is refused — Task 3 (`a_substituted_recipient_is_unmatched`), Task 7 (`an unmatched or unchecked payee refuses signing`). +1. **A proposal for another chain or genesis** (prepared on a testnet, opened by a mainnet wallet): the head comparison is meaningless, so it must be refused by name, never signed — Task 7 (`a proposal for another chain or genesis is refused`). +2. **Signing twice, or a wrong password at signing**: each signature asks again, a wrong password counts toward the unlock throttle and decrypts nothing, and no seed is left in session or storage — Task 7 (`every signature asks for the password and caches nothing`, `a wrong password at signing is throttled like an unlock`). +3. **A node that answers `rand_chainId` but fails `rand_getHead`, or is on the wrong chain**: treated exactly as no node (refuse, name the URL, offer Retry) — Task 7 (`a node that cannot give a head is no node`). +4. **A file with a BOM or trailing newline** (a share saved by another app, a proposal from Windows): accepted — core strips `U+FEFF` and trims; the engine trims shares — Task 3 (`a_bom_and_whitespace_around_a_file_are_tolerated`) and Task 7. +5. **Choosing a file from the extension popup** (Chrome closes the popup when the file dialog takes focus, losing the in-memory proposal): in popup mode the screens offer "Open in the side panel" instead of a file input — Task 8 (`the popup offers the side panel instead of a file picker`). + +--- + +### Task 1: Re-pin `core/vendor/fullnode` to `e34117be`, move to chain 21, link `randprotocol-msig` — **in progress on `feat/msauth-repin`** (`~/rand-worktrees/clients-msauth-repin`) + +This task is the record of the step another session is running. Tasks 2+ assume its result: the submodule at `e34117be`, `CHAIN_BUILD = "e34117b"`, `DEFAULT_CHAIN_ID = 21`, and `wallet-core` depending on `randprotocol-msig`. Before Task 2, merge it: `git -C ~/rand-worktrees/clients-msauth merge --no-ff feat/msauth-repin`. + +**Files:** +- Modify: `core/vendor/fullnode` (submodule → `e34117bedc634d2d2def3d2c2506bd03aa59e81d`), `core/Cargo.toml` (workspace dep), `core/crates/wallet-core/Cargo.toml`, `core/crates/wallet-core/src/lib.rs` (`CHAIN_BUILD` doc + value ~line 57–85, `DEFAULT_CHAIN_ID` ~line 86–105, tests at ~5287, ~5301, ~6122, `DeployExec` ~7254), `core/Cargo.lock`, `desktop/src-tauri/Cargo.lock`, `android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java:11`, `ios/RandWallet/Storage/Settings.swift:52`, `ios/RandWalletTests/CoreSmokeTests.swift`, `ios/RandWalletTests/LiveSendTests.swift` (`chainId = 21`), `web/wallet/test/core.integration.test.mjs:248,252`, `web/wallet/test/prover.e2e.test.mjs:68`, `web/wallet/test/withdraw.e2e.test.mjs:75`, `README.md` (submodule line) + +**Interfaces:** +- Produces: workspace dependency `randprotocol-msig = { path = "vendor/fullnode/crates/randprotocol-msig" }`; `wallet-core` `[dependencies] randprotocol-msig = { workspace = true }`; `wallet_core::CHAIN_BUILD == "e34117b"`; `wallet_core::DEFAULT_CHAIN_ID == 21`. + +- [ ] **Step 0: Disk.** `df -h /` — stop if free < 25 GiB. +- [ ] **Step 1: Fill the submodule from the local fullnode** (the commit is not on GitHub; the shared `.git/config` keeps its GitHub URL — the `-c` override is per-command): + +```bash +cd ~/rand-worktrees/clients-msauth-repin +git -c protocol.file.allow=always -c submodule.core/vendor/fullnode.url=/Users/dendisuhubdy/Github/randprotocol/fullnode \ + submodule update --init core/vendor/fullnode +git -C core/vendor/fullnode fetch /Users/dendisuhubdy/Github/randprotocol/fullnode feat/private-multisig +git -C core/vendor/fullnode checkout --detach e34117bedc634d2d2def3d2c2506bd03aa59e81d +git config --get submodule.core/vendor/fullnode.url # must still print https://github.com/randprotocol/fullnode.git +``` + +- [ ] **Step 2: Failing tests.** In `core/crates/wallet-core/src/lib.rs` change the three `default_chain_id` asserts from `20` to `21` and `assert_eq!(v["chain_build"], "37c4fdd")` to `"e34117b"`, and add beside `version_reports_chain_fourteen`: + +```rust +#[test] +fn the_vendored_msig_crate_is_linked() { + assert_eq!(randprotocol_msig::POLICY_VERSION, 1); + assert_eq!(randprotocol_msig::SIG_BYTES, 2420); + assert_eq!(randprotocol_msig::PK_BYTES, 1312); +} +``` + +Run: `cd core && cargo test --release -p wallet-core the_vendored_msig_crate_is_linked` — Expected: compile error `unresolved crate randprotocol_msig` (and `DeployExec` missing trait items). + +- [ ] **Step 3: Implement.** + - `core/Cargo.toml` `[workspace.dependencies]`, after `randprotocol-prover`: + ```toml + # Private multisig signer checks (fullnode e34117be, spec 2026-10-11 phase 4 §6.5): pure, + # wasm32-clean; every app's check/sign/verify-share goes through it. + randprotocol-msig = { path = "vendor/fullnode/crates/randprotocol-msig" } + ``` + - `core/crates/wallet-core/Cargo.toml` `[dependencies]`: `randprotocol-msig = { workspace = true }`. + - `CHAIN_BUILD = "e34117b"`; prepend to its doc: "fullnode `feat/private-multisig` `e34117be` — constraint set 9 (the MODQ chip: every proof key and the proof encoding move; `hc_bundle` `60af094a…` and `hc_auth` `1e4e347f…` unchanged), the `msauth` guest, `randprotocol-msig`; a node on it refuses chains 18–20, so this build is chain-21-only. Before it: …". + - `DEFAULT_CHAIN_ID`: `None => 21,` and its doc's first sentence: "The chain the defaults below describe: chain 21 (fullnode v0.8.0, constraint set 9; perps, multisig, the `fees` section, `hc_auth_multisig`), cut from chain 20's state." + - `DeployExec` (inside the RPL-2 test module): forward the six new `ConfidentialExecutor` methods: + ```rust + fn auth_header(&self, proof: &[u8]) -> std::result::Result<(randprotocol_core::confidential::AuthShape, Word8, Option), ConfidentialError> { + self.0.auth_header(proof) + } + fn warm_auth_multisig(&self) { + self.0.warm_auth_multisig() + } + fn auth_multisig_key_ready(&self) -> bool { + self.0.auth_multisig_key_ready() + } + fn install_msauth_vk(&self, blob: &[u8]) -> std::result::Result<(), String> { + self.0.install_msauth_vk(blob) + } + fn enable_auth_multisig(&self) { + self.0.enable_auth_multisig() + } + fn build_msauth_key(&self) -> std::result::Result<(), String> { + self.0.build_msauth_key() + } + ``` + - Any other compile error from the vendored move: fix only if it is a mechanical rename; anything else, stop and report. + - `Prefs.java`: `DEFAULT_CHAIN_ID = 21`; `Settings.swift:52`: `?? 21`; `CoreSmokeTests`: `XCTAssertEqual(c.defaultChainId, 21)`, `XCTAssertEqual(c.chainBuild, "e34117b", "fullnode e34117be (private multisig, constraint set 9), chain 21")`; `core.integration.test.mjs`: `assert.equal(k.default_chain_id, 21, 'chain 21 from e34117b (constraint set 9); 20 from 2026-10-01 before that')`, `assert.equal(k.chain_build, 'e34117b')` (hc pins unchanged); both e2e files `const CHAIN_ID = 21;`; `LiveSendTests`: `settings.chainId = 21`. + - `cd desktop/src-tauri && cargo update -p wallet-core` (adds `randprotocol-msig` to the lock). +- [ ] **Step 4: Verify.** `cd core && cargo check --target wasm32-unknown-unknown -p wallet-wasm --profile wasm` → `Finished` (proves `dilithium-core` is gated off wasm32). `cd core && cargo test --release` → all pass, hc pins unchanged. `node --test "ui/test/**/*.test.mjs"` → pass. `core/scripts/build-wasm.sh && node --test web/wallet/test/core.integration.test.mjs` → pass. `cd desktop/src-tauri && node ../scripts/stage-ui.mjs && cargo test` → pass. +- [ ] **Step 5: Commit.** + +```bash +git add core/vendor/fullnode core/Cargo.toml core/Cargo.lock core/crates/wallet-core desktop/src-tauri/Cargo.lock \ + android/app/src/main/java/org/randprotocol/wallet/security/Prefs.java ios/RandWallet/Storage/Settings.swift \ + ios/RandWalletTests/CoreSmokeTests.swift ios/RandWalletTests/LiveSendTests.swift web/wallet/test README.md +git commit -m "core: vendor fullnode e34117be (private multisig, constraint set 9) — chain-21-only; randprotocol-msig linked; DeployExec forwards the msauth executor methods + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_01L6dmK1EC4QerMmqWR1h7d8" +``` + +--- + +### Task 2: Real-node prover e2e on a chain-21 shape + +**Files:** +- Modify: `web/wallet/test/prover.e2e.test.mjs` (assert the multisig auth guest when the chain names one) + +**Interfaces:** +- Consumes: Task 1's vendored build (`scripts/e2e-prover.sh` builds `rand-node`/`rand-prover` from `core/vendor/fullnode`). + +- [ ] **Step 1: The assertion.** In the send test, after `const status = await rpc(env.rpcUrl, 'rand_status');`: + +```js + // A chain-21 shape (RAND_E2E_GENESIS_ARGS carries --auth-multisig-guest): the node names the + // msauth guest and loaded its pinned verifier key, and this wallet's single-key spend still lands. + if ((process.env.RAND_E2E_GENESIS_ARGS || '').includes('--auth-multisig-guest')) { + assert.match(String(status.hc_auth_multisig), /^[0-9a-f]{64}$/, 'the node names its multisig auth guest'); + assert.equal(status.msauth_key, 'loaded', 'the node loaded the Test msauth key (RAND_MSAUTH_VK)'); + } +``` + +- [ ] **Step 2: Run on the chain-21 shape** (`df -h /` first; ~2.5 min of proving plus the release build; nothing else proving): + +```bash +cd ~/rand-worktrees/clients-msauth +S=/private/tmp/claude-501/msauth-e2e && mkdir -p $S && echo '{"burn_base": true, "burn_floor": true}' > $S/fees.json +RAND_MSAUTH_VK=$PWD/core/vendor/fullnode/crates/randprotocol-zkvm/keys/msauth-vk-test.bin \ +RAND_E2E_GENESIS_ARGS="--auth-multisig-guest --fees $S/fees.json --binding-domain 1" \ + scripts/e2e-prover.sh 2>&1 | tee $S/prover-e2e.log +``` + +Expected: `# pass 3`, `# fail 0` (the either-runs test, the no-prover test, the send). If `rand-node genesis` refuses `--binding-domain` twice (it is also in `chainArgs`), drop it from the env value. + +- [ ] **Step 3: Commit** (`git add web/wallet/test/prover.e2e.test.mjs`, message `e2e: the prover e2e asserts the msauth guest and key on a chain-21 shape (passed on e34117be: )` + trailer). + +--- + +### Task 3: Core — fixtures, `msauth_check`, `msauth_verify_share`, `msauth_policy_info` + +**Files:** +- Create: `ui/test/fixtures/msauth/*.json` (copies of every vendored fixture — 10 at `e34117be`, 11 once `proposal-substituted-recipient.json` lands), `ui/test/fixtures/msauth.sha256` (the ONE hash list; the re-vendor rewrites it), `ui/test/msauth-fixtures.test.mjs`, `core/crates/wallet-core/src/msauth.rs` +- Modify: `core/crates/wallet-core/src/lib.rs` (`mod msauth;`, three dispatch arms, dispatch doc) + +**Interfaces:** +- Consumes: `randprotocol_msig::{check_proposal, verify_share, Policy, Proposal, MsigError}`. +- Produces: `msauth::check(proposal: &str, policy: &str, expected_recipients: &[String]) -> Result` (addresses parsed with `ShieldedAddress::parse`, passed to `check_proposal_expecting` **[confirm: 0e]**; an empty list means none given) → `{outputs: [{owner_fingerprint, to_account, amount: "", asset, memo, matches:
|null, needs_payee: bool}], recipients_checked: bool, fee: "", burns: {burn_a: "", burn_asset, burn_r: ""}, expires_at: , signing_message: "", policy_fingerprint, lines: [String], chain_id, genesis, binding_domain}`; `msauth::verify_share(proposal, policy, share) -> Result` → `true`; `msauth::policy_info(policy) -> Result` → `{fingerprint, threshold, signers: , address, policy_hash}`; `msauth::clean(&str) -> &str` (strips a leading `U+FEFF`, trims). Dispatch: `msauth_check {proposal, policy, expected_recipients?: [address]}`, `msauth_verify_share {proposal, policy, share}`, `msauth_policy_info {policy}`. + +- [ ] **Step 0: Precondition — the fix-wave crate.** `check_proposal_expecting`, `OutputSummary::matches` and `proposal-substituted-recipient.json` **[confirm: 0e]** are not in `e34117be`. Before this task, re-pin `core/vendor/fullnode` to the fix-wave commit 0e sends (Task 1 Steps 1–4 with that SHA; `CHAIN_BUILD` its first 7 characters), then rename this task's tagged names to the crate's real ones. Until that commit exists, run Tasks 5–9 first (they use the stub core and do not need it); Tasks 3, 4, 10 Step 5 and 14 wait. + +- [ ] **Step 1: Copy the fixtures and pin them in one list.** + +```bash +mkdir -p ui/test/fixtures/msauth +cp core/vendor/fullnode/crates/randprotocol-msig/tests/fixtures/*.json ui/test/fixtures/msauth/ +( cd ui/test/fixtures/msauth && shasum -a 256 *.json ) > ui/test/fixtures/msauth.sha256 +``` + +At `e34117be` the list must read (check before committing; every re-vendor re-runs the three lines above and commits the new list, nothing else changes): + +``` +5b937523f177f1b2133214d36f99e678d9d104767481e6f7cf8c86eb7876ffb3 expected.json +31fbeb3212e024e5839e29f9bdf23c0bbd6e34eb2ffcefb5c9085b3196992ff2 policy.json +2144cafbbd06c1cd165105754a8ee1043167ed68b2256e150c991f82b055b8f7 proposal-expired.json +457f0ab49d28ba0c18e3be1e266ed09fa94f1bd7f3105155b88c8dc95fc786c2 proposal-fee-raised.json +c621fc9531f95a04be2aa83d217d0040b19b27254d7bce483dbf6243a558b81b proposal-tampered-output.json +6f2135c940a38af710e137d2da0ff42147bd6d9a2bf18bd3fc6973bef044b3bd proposal-unbound.json +50f1f1cc5df58bfc1126bc9279a96fc1a47c0d0a192fa6239988d8f4d828129b proposal-valid.json +dddb4624c6f290948be9769735b13a445d7e4d201c85bf8e044438d808f91016 proposal-value-mismatch.json +7ce496a463c09dec560070875c12e10781b14a31f944bfb1905c07e6cd751832 proposal-wrong-expected.json +f75a09d8ec62da397010202e053d192a429710ecb566faf29d8d1381f07ee8aa signer-seeds.json +``` + +`ui/test/msauth-fixtures.test.mjs`: + +```js +// The private-multisig vectors every app's tests read (iOS bundles the directory, Android adds it to +// its test resources, wallet-core reads it): fullnode's crates/randprotocol-msig/tests/fixtures, +// byte for byte. The hashes live in ONE list, ui/test/fixtures/msauth.sha256 (`shasum -a 256` +// format), which a re-vendor rewrites; wallet-core's `the_fixture_copies_are_the_vendored_vectors` +// holds the copies to the submodule. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFileSync, readdirSync } from 'node:fs'; + +const DIR = new URL('./fixtures/msauth/', import.meta.url); +const SUMS = readFileSync(new URL('./fixtures/msauth.sha256', import.meta.url), 'utf8') + .split('\n').filter(Boolean).map((l) => l.split(/\s+/)).map(([hash, name]) => [name, hash]); + +test('every msauth fixture is listed once, and hashes to its listed SHA-256', () => { + assert.deepEqual(readdirSync(DIR).sort(), SUMS.map(([n]) => n).sort()); + for (const [name, want] of SUMS) { + assert.equal(createHash('sha256').update(readFileSync(new URL(name, DIR))).digest('hex'), want, name); + } +}); +``` + +Run `node --test ui/test/msauth-fixtures.test.mjs` → PASS. + +- [ ] **Step 2: Failing Rust tests.** Create `core/crates/wallet-core/src/msauth.rs` with only the test module (the functions do not exist yet): + +```rust +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../ui/test/fixtures/msauth/"); + const VENDORED: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../vendor/fullnode/crates/randprotocol-msig/tests/fixtures/"); + pub(super) fn fx(name: &str) -> String { + std::fs::read_to_string(format!("{DIR}{name}.json")).unwrap() + } + pub(super) fn expected() -> Value { + serde_json::from_str(&fx("expected")).unwrap() + } + + /// The valid proposal's real payee: the address its informational summary names + /// ("pay 10 RAND to rand1…", fullnode's generator). Read from there, never from the app's view. + pub(super) fn payee() -> String { + let p: Value = serde_json::from_str(&fx("proposal-valid")).unwrap(); + p["summary"][0].as_str().unwrap().rsplit(' ').next().unwrap().to_string() + } + + /// [confirm: 0e] the fixture name and that the crate reports the substituted output unmatched + /// (rather than refusing outright — if it refuses, assert the refusal names the output instead). + #[test] + fn a_substituted_recipient_is_unmatched() { + let v = check(&fx("proposal-substituted-recipient"), &fx("policy"), &[payee()]).unwrap(); + assert_eq!(v["recipients_checked"], false); + assert!(v["outputs"].as_array().unwrap().iter().any(|o| o["needs_payee"] == true && o["matches"].is_null())); + } + + #[test] + fn the_valid_proposal_matches_its_payee_and_nothing_given_checks_nothing() { + let v = check(&fx("proposal-valid"), &fx("policy"), &[payee()]).unwrap(); + assert_eq!(v["recipients_checked"], true); + assert!(v["outputs"][2]["matches"].is_string(), "{}", v["outputs"][2]); + assert!(v["lines"][2].as_str().unwrap().contains("matches ")); + let none = check(&fx("proposal-valid"), &fx("policy"), &[]).unwrap(); + assert_eq!(none["recipients_checked"], false); + assert!(none["lines"].as_array().unwrap().iter().any(|l| l.as_str().unwrap().contains("no expected recipient given: the payee was NOT checked"))); + assert!(check(&fx("proposal-valid"), &fx("policy"), &["rand1nope".into()]).unwrap_err().contains("expected recipient")); + } + + #[test] + fn the_fixture_copies_are_the_vendored_vectors() { + let names = |d: &str| -> Vec { + let mut v: Vec = std::fs::read_dir(d).unwrap().map(|e| e.unwrap().file_name().into_string().unwrap()).collect(); + v.sort(); + v + }; + assert_eq!(names(DIR), names(VENDORED), "the same files: re-copy after a re-pin (Task 3 Step 1)"); + for n in names(VENDORED) { + assert_eq!(std::fs::read(format!("{DIR}{n}")).unwrap(), std::fs::read(format!("{VENDORED}{n}")).unwrap(), "{n}"); + } + } + + #[test] + fn check_returns_the_expected_summary_with_amounts_as_strings() { + let v = check(&fx("proposal-valid"), &fx("policy"), &[payee()]).unwrap(); + let want = &expected()["summary"]; + for (i, o) in want["outputs"].as_array().unwrap().iter().enumerate() { + let got = &v["outputs"][i]; + assert_eq!(got["owner_fingerprint"], o["owner_fingerprint"]); + assert_eq!(got["to_account"], o["to_account"]); + assert_eq!(got["amount"], json!(o["amount"].as_u64().unwrap().to_string()), "amounts are strings"); + assert_eq!((&got["asset"], &got["memo"]), (&o["asset"], &o["memo"])); + } + assert_eq!(v["fee"], "1000000"); + assert_eq!(v["burns"], json!({ "burn_a": "0", "burn_asset": 0, "burn_r": "0" })); + assert_eq!(v["expires_at"], 2024); + assert_eq!(v["signing_message"], expected()["signing_message"]); + assert_eq!(v["policy_fingerprint"], "NFYW-K8E9-E5K9-AAK3"); + assert_eq!((&v["chain_id"], &v["binding_domain"], &v["genesis"]), (&json!(7), &json!(0), &json!("0".repeat(64)))); + let lines = randprotocol_msig::check_proposal_expecting(&fx("proposal-valid"), &fx("policy"), Some(&[parse_address(&payee()).unwrap()])).unwrap().lines(); // [confirm: 0e] + assert_eq!(v["lines"], json!(lines), "fullnode's lines, verbatim"); + for l in &lines[..4] { + assert!(l.contains("owner key verified"), "{l}"); // [confirm: 0e] the final caveat/match wording + } + assert!(lines.iter().any(|l| l.contains("chain 7")), "the chain-id/genesis line [confirm: 0e]"); + assert!(lines[3].contains("this account 4333dd507bcc4286"), "change is labelled: {}", lines[3]); + assert!(lines.iter().all(|l| !l.contains("rand1")), "no address from the proposal's summary"); + } + + #[test] + fn each_bad_proposal_is_refused_by_name() { + for (name, words) in [ + ("proposal-tampered-output", "does not open as this account's"), + ("proposal-unbound", "does not hash to the proposal's binding"), + ("proposal-wrong-expected", "expected digest is not its bundle's"), + ("proposal-fee-raised", "the witness's value does not match the outputs: asset 0"), + ("proposal-value-mismatch", "the witness's value does not match"), + ("proposal-expired", "the proposal expires at"), + ] { + let e = check(&fx(name), &fx("policy"), &[]).unwrap_err(); + assert!(e.contains(words), "{name}: {e}"); + } + } + + #[test] + fn a_bom_and_whitespace_around_a_file_are_tolerated() { + check(&format!("\u{feff}{}\r\n", fx("proposal-valid")), &format!("\u{feff} {}\n", fx("policy")), &[format!(" {}\n", payee())]).unwrap(); + verify_share(&fx("proposal-valid"), &fx("policy"), &format!("{}\n", expected()["share_0"].as_str().unwrap())).unwrap(); + } + + #[test] + fn verify_share_takes_the_expected_shares_and_refuses_others() { + let (prop, pol) = (fx("proposal-valid"), fx("policy")); + for k in ["share_0", "share_2"] { + assert_eq!(verify_share(&prop, &pol, expected()[k].as_str().unwrap()).unwrap(), Value::Bool(true)); + } + let sig = expected()["share_2"].as_str().unwrap().split_once(':').unwrap().1.to_string(); + assert!(verify_share(&prop, &pol, &format!("1:{sig}")).unwrap_err().contains("signer 1's signature does not verify")); + assert!(verify_share(&prop, &pol, &format!("7:{sig}")).unwrap_err().contains("signer 7 is not in this policy")); + assert!(verify_share(&prop, &pol, "2:zz").unwrap_err().contains("not hex")); + } + + #[test] + fn policy_info_names_the_account_every_signer_compares() { + let v = policy_info(&fx("policy")).unwrap(); + assert_eq!((&v["fingerprint"], &v["threshold"], &v["signers"]), (&json!("NFYW-K8E9-E5K9-AAK3"), &json!(2), &json!(3))); + let prop: Value = serde_json::from_str(&fx("proposal-valid")).unwrap(); + assert_eq!(v["policy_hash"], prop["policy"], "the hash a proposal names"); + assert!(v["address"].as_str().unwrap().starts_with("rand1")); + assert!(policy_info("{\"version\":1,\"threshold\":0,\"signers\":[],\"msk\":\"00\"}").is_err()); + } + + #[test] + fn the_dispatch_arms_answer_through_call() { + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "expected_recipients": [payee()] }); + let r: Value = serde_json::from_str(&crate::call("msauth_check", &p.to_string())).unwrap(); + assert_eq!(r["ok"], true, "{r}"); + let r: Value = serde_json::from_str(&crate::call("msauth_policy_info", &json!({ "policy": fx("policy") }).to_string())).unwrap(); + assert_eq!(r["value"]["threshold"], 2); + let s = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "share": expected()["share_0"] }); + let r: Value = serde_json::from_str(&crate::call("msauth_verify_share", &s.to_string())).unwrap(); + assert_eq!(r["value"], true); + } +} +``` + +Add `mod msauth;` near the top of `lib.rs` (after the `use` block). Run `cd core && cargo test --release -p wallet-core msauth` → FAIL: `cannot find function check`. + +- [ ] **Step 3: Implement** (top of `msauth.rs`, above the tests): + +```rust +//! Private multisig signing (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md §2): the +//! `msauth_*` methods of [`crate::dispatch`], over fullnode's `randprotocol-msig` — the same checks +//! the `rand multisig-private` CLI makes. Pure: no I/O. A policy carries the account's `msk` and a +//! proposal its witness; neither is ever written into an error. +use crate::Result; +use randprotocol_core::notes::word8_to_hex; +use randprotocol_msig::{self as msig, Policy, Proposal}; +use serde_json::{json, Value}; + +fn err(e: msig::MsigError) -> String { + e.to_string() +} + +/// A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. +pub fn clean(s: &str) -> &str { + s.trim_start_matches('\u{feff}').trim() +} + +fn parse_policy(policy: &str) -> Result { + let p: Policy = serde_json::from_str(clean(policy)).map_err(|e| format!("the policy does not parse: {e}"))?; + p.check().map_err(err)?; + Ok(p) +} + +/// An output a signer must name the payee of: not change, and paying something (a zero-amount +/// output is a dummy slot) — [confirm: 0e] that the crate needs no payee for a dummy. +fn needs_payee(o: &msig::OutputSummary) -> bool { + !o.to_account && o.amount != 0 +} + +fn parse_address(a: &str) -> Result { + randprotocol_core::notes::ShieldedAddress::parse(a.trim()).map_err(|e| format!("expected recipient {}: {e}", a.trim())) +} + +/// `check_proposal_expecting` [confirm: 0e], re-shaped to this crate's wire rule (amounts as +/// decimal strings), with fullnode's `lines()` verbatim, each output's `matches` (the expected +/// recipient's address fingerprint, or null) and `needs_payee`, `recipients_checked` (every output +/// that needs a payee matched one — the apps refuse to sign otherwise), and the chain the proposal +/// names (`chain_id`, `genesis`, `binding_domain`) for the caller's comparison with its node. +pub fn check(proposal: &str, policy: &str, expected_recipients: &[String]) -> Result { + let (proposal, policy) = (clean(proposal), clean(policy)); + let expected = expected_recipients.iter().map(|a| parse_address(a)).collect::>>()?; + let given = (!expected.is_empty()).then_some(&expected[..]); + let s = msig::check_proposal_expecting(proposal, policy, given).map_err(err)?; // [confirm: 0e] + let p: Proposal = serde_json::from_str(proposal).map_err(|e| format!("the proposal does not parse: {e}"))?; + let outputs: Vec = s + .outputs + .iter() + .map(|o| json!({ + "owner_fingerprint": o.owner_fingerprint, "to_account": o.to_account, "amount": o.amount.to_string(), + "asset": o.asset, "memo": o.memo, + "matches": o.matches, // [confirm: 0e] the field's name and type (Option) + "needs_payee": needs_payee(o), + })) + .collect(); + let recipients_checked = s.outputs.iter().filter(|o| needs_payee(o)).all(|o| o.matches.is_some()); + Ok(json!({ + "outputs": outputs, + "recipients_checked": recipients_checked, + "fee": s.fee.to_string(), + "burns": { "burn_a": s.burns.burn_a.to_string(), "burn_asset": s.burns.burn_asset, "burn_r": s.burns.burn_r.to_string() }, + "expires_at": s.expires_at, + "signing_message": hex::encode(s.signing_message), + "policy_fingerprint": s.policy_fingerprint, + "lines": s.lines(), + "chain_id": p.chain_id, + "genesis": p.genesis, + "binding_domain": p.binding_domain, + })) +} + +/// `verify_share`: `true`, or the refusal. +pub fn verify_share(proposal: &str, policy: &str, share: &str) -> Result { + msig::verify_share(clean(proposal), clean(policy), clean(share)).map_err(err)?; + Ok(Value::Bool(true)) +} + +/// What a signer compares out of band before saving a profile. +pub fn policy_info(policy: &str) -> Result { + let p = parse_policy(policy)?; + Ok(json!({ + "fingerprint": p.fingerprint(), + "threshold": p.threshold, + "signers": p.signers.len(), + "address": p.address().to_string(), + "policy_hash": word8_to_hex(&p.policy_hash()), + })) +} +``` + +`lib.rs` `dispatch`, before `other =>`: + +```rust + // Private multisig signing (msauth.rs). + "msauth_check" => { + let expected: Vec = match params.get("expected_recipients") { + None | Some(Value::Null) => vec![], + Some(v) => serde_json::from_value(v.clone()).map_err(|_| "expected_recipients must be a list of addresses".to_string())?, + }; + msauth::check(str_param(params, "proposal")?, str_param(params, "policy")?, &expected) + } + "msauth_verify_share" => msauth::verify_share(str_param(params, "proposal")?, str_param(params, "policy")?, str_param(params, "share")?), + "msauth_policy_info" => msauth::policy_info(str_param(params, "policy")?), +``` + +Add to the `dispatch` doc list: + +```rust +/// - `msauth_check` `{proposal, policy, expected_recipients?}` → the checked summary (each output's +/// `matches`/`needs_payee`, `recipients_checked`) (fullnode `check_proposal`, amounts as +/// strings, `lines` verbatim, the proposal's `chain_id`/`genesis`/`binding_domain`). No RPC: the +/// caller compares `expires_at` with its node's head (refuse at `head + 16 >= expires_at`) +/// - `msauth_verify_share` `{proposal, policy, share}` → `true` when `share` (`i:hex`) verifies +/// - `msauth_policy_info` `{policy}` → `{fingerprint, threshold, signers, address, policy_hash}` +``` + +- [ ] **Step 4: Run** `cd core && cargo test --release -p wallet-core msauth` → all PASS. +- [ ] **Step 5: Commit** `git add ui/test/fixtures/msauth ui/test/msauth-fixtures.test.mjs core/crates/wallet-core/src` — message `core: msauth_check, msauth_verify_share, msauth_policy_info over randprotocol-msig; the ten e34117be vectors under ui/test/fixtures/msauth` + trailer. + +--- + +### Task 4: Core — `msauth_signer_import`, `msauth_sign`; the real wasm on the vectors + +**Files:** +- Modify: `core/crates/wallet-core/src/msauth.rs`, `core/crates/wallet-core/src/lib.rs` (two arms + doc), `web/wallet/test/core.integration.test.mjs` (new test) + +**Interfaces:** +- Produces: `msauth::signer_import(input: &str, policy: Option<&str>) -> Result` → `{seed: "<64 hex>", public_key: "", address: "", index: |null}`; `msauth::sign(proposal, policy, seed_hex) -> Result` → `"i:hex"`. Dispatch `msauth_signer_import {input, policy?}`, `msauth_sign {proposal, policy, seed}`. Sentences: `SIGNER_KEY_SHAPE = "a signer key is a rand-node keygen file or 64 hex characters"`, `"a signer seed is 64 hex characters"`, `"the key file's public_key is not the one its seed derives"`, `"the key file's address is not the one its seed derives"`, `"this key is not one of the policy's signers"` (fullnode's wording). + +- [ ] **Step 1: Failing tests** (append to `msauth.rs` tests): + +```rust + fn seeds() -> (Vec, String) { + let v: Value = serde_json::from_str(&fx("signer-seeds")).unwrap(); + (v["members"].as_array().unwrap().iter().map(|s| s.as_str().unwrap().to_string()).collect(), v["non_member"].as_str().unwrap().to_string()) + } + + #[test] + fn members_0_and_2_sign_the_expected_shares() { + let (m, _) = seeds(); + for (i, want) in [(0usize, "share_0"), (2, "share_2")] { + let share = sign(&fx("proposal-valid"), &fx("policy"), &m[i]).unwrap(); + assert_eq!(share, expected()[want], "deterministic Dilithium2 reproduces the vector"); + } + } + + #[test] + fn an_outsider_a_bad_proposal_or_a_bad_seed_signs_nothing() { + let (m, outsider) = seeds(); + assert!(sign(&fx("proposal-valid"), &fx("policy"), &outsider).unwrap_err().contains("not one of the policy's signers")); + assert!(sign(&fx("proposal-unbound"), &fx("policy"), &m[0]).unwrap_err().contains("binding")); + let e = sign(&fx("proposal-valid"), &fx("policy"), "zz").unwrap_err(); + assert_eq!(e, "a signer seed is 64 hex characters", "never echoes the seed"); + } + + #[test] + fn import_reads_a_keygen_file_or_bare_hex_and_finds_the_position() { + let (m, outsider) = seeds(); + let kp = randprotocol_core::Keypair::from_seed(hex::decode(&m[2]).unwrap().try_into().unwrap()).unwrap(); + let file = json!({ "seed": m[2], "address": kp.address().to_base58(), "public_key": kp.public_key().to_hex() }).to_string(); + let v = signer_import(&file, Some(&fx("policy"))).unwrap(); + assert_eq!((&v["seed"], &v["index"], &v["public_key"]), (&json!(m[2]), &json!(2), &json!(kp.public_key().to_hex()))); + assert_eq!(signer_import(&m[0], Some(&fx("policy"))).unwrap()["index"], 0); + assert_eq!(signer_import(&m[0], None).unwrap()["index"], Value::Null); + assert_eq!(signer_import(&outsider, Some(&fx("policy"))).unwrap_err(), "this key is not one of the policy's signers"); + let lying = json!({ "seed": m[2], "address": kp.address().to_base58(), "public_key": "00".repeat(1312) }).to_string(); + assert_eq!(signer_import(&lying, None).unwrap_err(), "the key file's public_key is not the one its seed derives"); + assert_eq!(signer_import("{\"spend_key\":\"aa\"}", None).unwrap_err(), SIGNER_KEY_SHAPE); + assert_eq!(signer_import("nope", None).unwrap_err(), SIGNER_KEY_SHAPE); + } + + #[test] + fn sign_answers_through_call() { + let (m, _) = seeds(); + let p = json!({ "proposal": fx("proposal-valid"), "policy": fx("policy"), "seed": m[0] }); + let r: Value = serde_json::from_str(&crate::call("msauth_sign", &p.to_string())).unwrap(); + assert_eq!(r["value"], expected()["share_0"]); + } +``` + +Run `cd core && cargo test --release -p wallet-core msauth` → FAIL (`sign`, `signer_import` undefined). + +- [ ] **Step 2: Implement** (in `msauth.rs`, above the tests): + +```rust +use randprotocol_core::Keypair; +use zeroize::Zeroizing; + +pub const SIGNER_KEY_SHAPE: &str = "a signer key is a rand-node keygen file or 64 hex characters"; + +/// A seed in a buffer that is wiped when dropped. The error never repeats the input. +fn seed_from_hex(s: &str) -> Result> { + let mut seed = Zeroizing::new([0u8; 32]); + hex::decode_to_slice(s.trim(), &mut seed[..]).map_err(|_| "a signer seed is 64 hex characters".to_string())?; + Ok(seed) +} + +/// A `rand-node keygen` file (`{seed, address, public_key}`) or a bare 64-hex seed: the seed, the +/// key it derives, and — given the policy — this key's position in it (refused if absent). A file +/// whose `public_key` or `address` is not its seed's is refused. +pub fn signer_import(input: &str, policy: Option<&str>) -> Result { + let input = clean(input); + let (seed, claimed_pk, claimed_address) = if input.starts_with('{') { + let v: Value = serde_json::from_str(input).map_err(|_| SIGNER_KEY_SHAPE.to_string())?; + let s = v["seed"].as_str().ok_or(SIGNER_KEY_SHAPE)?; + (seed_from_hex(s).map_err(|_| SIGNER_KEY_SHAPE.to_string())?, v["public_key"].as_str().map(str::to_owned), v["address"].as_str().map(str::to_owned)) + } else { + (seed_from_hex(input).map_err(|_| SIGNER_KEY_SHAPE.to_string())?, None, None) + }; + let kp = Keypair::from_seed(*seed).map_err(|e| format!("the signer seed: {e}"))?; + let public_key = kp.public_key().to_hex(); + if claimed_pk.is_some_and(|c| !c.trim().eq_ignore_ascii_case(&public_key)) { + return crate::bad("the key file's public_key is not the one its seed derives"); + } + let address = kp.address().to_base58(); + if claimed_address.is_some_and(|a| a.trim() != address) { + return crate::bad("the key file's address is not the one its seed derives"); + } + let index = match policy { + None => Value::Null, + Some(pol) => { + let pks = parse_policy(pol)?.pks().map_err(err)?; + let i = pks.iter().position(|k| k[..] == kp.public_key().as_bytes()[..]).ok_or("this key is not one of the policy's signers")?; + json!(i) + } + }; + Ok(json!({ "seed": hex::encode(&seed[..]), "public_key": public_key, "address": address, "index": index })) +} + +/// `sign`: the share `"i:hex"`. Every check `check` makes runs first (fullnode's `sign` re-runs them). +pub fn sign(proposal: &str, policy: &str, seed_hex: &str) -> Result { + let seed = seed_from_hex(seed_hex)?; + let (i, sig) = msig::sign(clean(proposal), clean(policy), &seed).map_err(err)?; + Ok(Value::String(format!("{i}:{}", hex::encode(sig)))) +} +``` + +Dispatch arms: + +```rust + "msauth_signer_import" => msauth::signer_import(str_param(params, "input")?, params.get("policy").and_then(Value::as_str)), + "msauth_sign" => msauth::sign(str_param(params, "proposal")?, str_param(params, "policy")?, str_param(params, "seed")?), +``` + +Doc lines: + +```rust +/// - `msauth_signer_import` `{input, policy?}` → `{seed, public_key, address, index}`: a `rand-node +/// keygen` file or 64 hex; a file whose `public_key`/`address` is not its seed's is refused; with +/// `policy`, a key outside it is refused ("this key is not one of the policy's signers") +/// - `msauth_sign` `{proposal, policy, seed}` → `"i:hex"`: every `msauth_check` check, then the +/// deterministic Dilithium2 share. `seed` is wiped here after use; the caller wipes its own copy +``` + +- [ ] **Step 3: Run** `cd core && cargo test --release -p wallet-core msauth` → PASS; then `cd core && cargo test --release` → all PASS. +- [ ] **Step 4: Real wasm test.** Append to `web/wallet/test/core.integration.test.mjs` (it already loads the real wasm and skips without it; reuse its core-loading helper — the variable it calls `call`/`core` there): + +```js +test('the wasm core checks and signs fullnode\'s private-multisig vectors exactly', { skip }, async () => { + const fx = (n) => readFileSync(new URL(`../../../ui/test/fixtures/msauth/${n}.json`, import.meta.url), 'utf8'); + const expected = JSON.parse(fx('expected')); + const seeds = JSON.parse(fx('signer-seeds')); + const payee = JSON.parse(fx('proposal-valid')).summary[0].split(' ').pop(); + const s = await core.call('msauth_check', { proposal: fx('proposal-valid'), policy: fx('policy'), expected_recipients: [payee] }); + assert.equal(s.recipients_checked, true); + assert.equal(s.policy_fingerprint, 'NFYW-K8E9-E5K9-AAK3'); + assert.equal(s.outputs[2].amount, '10000000000'); + assert.ok(s.lines.slice(0, 4).every((l) => l.includes('owner key verified'))); + assert.equal(await core.call('msauth_sign', { proposal: fx('proposal-valid'), policy: fx('policy'), seed: seeds.members[2] }), expected.share_2); + assert.equal(await core.call('msauth_verify_share', { proposal: fx('proposal-valid'), policy: fx('policy'), share: expected.share_0 }), true); + await assert.rejects(core.call('msauth_check', { proposal: fx('proposal-unbound'), policy: fx('policy') }), /does not hash to the proposal's binding/); +}); +``` + +Run `core/scripts/build-wasm.sh && node --test web/wallet/test/core.integration.test.mjs` → PASS (df check first). +- [ ] **Step 5: Commit** `core: msauth_signer_import and msauth_sign (seed in Zeroizing); the wasm signs the e34117be vectors exactly` + trailer. + +--- + +### Task 5: JS core layer — `coreApi` wrappers, byte vault records, msig sentences + +**Files:** +- Modify: `ui/engine/wallet.js` (`coreApi`), `ui/engine/crypto.js`, `ui/engine/core-errors.js`, `ui/test/core-errors.test.mjs` +- Create: `ui/test/crypto-bytes.test.mjs` + +**Interfaces:** +- Produces: `coreApi(core).msauthCheck(proposal, policy, expectedRecipients = [])`, `.msauthSign(proposal, policy, seedHex)`, `.msauthVerifyShare(proposal, policy, share)`, `.msauthSignerImport(input, policy?)`, `.msauthPolicyInfo(policy)`; `crypto.js` `encryptBytes(password, Uint8Array) -> vault`, `decryptBytes(password, vault) -> Uint8Array` (throws `wrong password`); `encryptSecret` becomes `encryptBytes(password, enc.encode(plaintext))`. + +- [ ] **Step 1: Failing tests.** `ui/test/crypto-bytes.test.mjs`: + +```js +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { encryptBytes, decryptBytes, encryptSecret, decryptSecret } from '../engine/crypto.js'; + +test('a byte record opens to the same bytes, and only with its password', async () => { + const seed = Uint8Array.from({ length: 32 }, (_, i) => i); + const rec = await encryptBytes('pw-long-enough', seed); + assert.deepEqual(await decryptBytes('pw-long-enough', rec), seed); + await assert.rejects(decryptBytes('another-password', rec), /wrong password/); + assert.equal(await decryptSecret('pw-long-enough', await encryptSecret('pw-long-enough', 'text')), 'text', 'string records unchanged'); +}); +``` + +In `ui/test/core-errors.test.mjs` add: + +```js +test('the private-multisig refusals are translated, values kept', async (t) => { + await setLocale('de', { dictionary: { + 'output {slot} does not open as this account\'s: refusing to sign value it cannot see': 'DE: Ausgabe {slot} öffnet nicht', + 'this key is not one of the policy\'s signers': 'DE: kein Unterzeichner', + } }); + t.after(() => setLocale('en')); + assert.equal(translateCoreError('output 2 does not open as this account\'s: refusing to sign value it cannot see'), 'DE: Ausgabe 2 öffnet nicht'); + assert.equal(translateCoreError('this key is not one of the policy\'s signers'), 'DE: kein Unterzeichner'); +}); + +test('coreApi names the five msauth methods', async () => { + const seen = []; + const api = coreApi({ async call(m, p) { seen.push([m, p]); return true; } }); + await api.msauthCheck('P', 'Q', ['rand1a']); + await api.msauthSign('P', 'Q', 'S'); + await api.msauthVerifyShare('P', 'Q', '0:aa'); + await api.msauthSignerImport('K', 'Q'); + await api.msauthSignerImport('K'); + await api.msauthPolicyInfo('Q'); + assert.deepEqual(seen, [ + ['msauth_check', { proposal: 'P', policy: 'Q', expected_recipients: ['rand1a'] }], ['msauth_sign', { proposal: 'P', policy: 'Q', seed: 'S' }], + ['msauth_verify_share', { proposal: 'P', policy: 'Q', share: '0:aa' }], ['msauth_signer_import', { input: 'K', policy: 'Q' }], + ['msauth_signer_import', { input: 'K' }], ['msauth_policy_info', { policy: 'Q' }], + ]); +}); +``` + +Run `node --test ui/test/crypto-bytes.test.mjs ui/test/core-errors.test.mjs` → FAIL. + +- [ ] **Step 2: Implement.** `crypto.js`: + +```js +/** Returns a vault object safe to persist, over raw bytes (a signer seed). */ +export async function encryptBytes(password, bytes) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(password, salt, ITERATIONS, ['encrypt']); + const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, bytes)); + return { v: VAULT_VERSION, kdf: 'pbkdf2-sha256', iter: ITERATIONS, salt: b64(salt), iv: b64(iv), ct: b64(ct) }; +} + +/** Returns a vault object safe to persist. */ +export async function encryptSecret(password, plaintext) { + return encryptBytes(password, enc.encode(plaintext)); +} + +/** + * Opens a byte record into a fresh `Uint8Array` the caller must `fill(0)` when done — the one + * buffer JavaScript can wipe (a string cannot be). Same refusals as `decryptSecret`. + */ +export async function decryptBytes(password, vault) { + const iterations = checkVault(vault); + const key = await deriveKey(password, unb64(vault.salt), iterations, ['decrypt']); + try { + return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: unb64(vault.iv) }, key, unb64(vault.ct))); + } catch { + throw new Error('wrong password'); + } +} + +export async function decryptSecret(password, vault) { + const bytes = await decryptBytes(password, vault); + return dec.decode(bytes); +} +``` + +(Replace the old `encryptSecret`/`decryptSecret` bodies; keep their doc comments.) + +`wallet.js` `coreApi` return object, after `proverFingerprint`: + +```js + // Private multisig signing (wallet-core msauth.rs): check, sign, verify a share, import a + // signer key, describe a policy. `seed` is 64 hex; never log a call's params. + msauthCheck: (proposal, policy, expected_recipients = []) => call('msauth_check', { proposal, policy, expected_recipients }), + msauthSign: (proposal, policy, seed) => call('msauth_sign', { proposal, policy, seed }), + msauthVerifyShare: (proposal, policy, share) => call('msauth_verify_share', { proposal, policy, share }), + msauthSignerImport: (input, policy) => call('msauth_signer_import', policy ? { input, policy } : { input }), + msauthPolicyInfo: (policy) => call('msauth_policy_info', { policy }), +``` + +`core-errors.js` — `EXACT` additions: + +```js + // ---- private multisig signing (`msauth_*`, fullnode randprotocol-msig) ---- + ['the transaction does not hash to the proposal\'s binding', () => t('the transaction does not hash to the proposal\'s binding')], + ['the proposal\'s signing_message is not its binding\'s', () => t('the proposal\'s signing_message is not its binding\'s')], + ['the proposal\'s auth_commit is not this policy\'s account', () => t('the proposal\'s auth_commit is not this policy\'s account')], + ['the proposal\'s expected digest is not its bundle\'s', () => t('the proposal\'s expected digest is not its bundle\'s')], + ['the transaction carries no bundle', () => t('the transaction carries no bundle')], + ['the transaction carries an action; a private multisig proposal is a payment from its bundle alone', + () => t('the transaction carries an action; a private multisig proposal is a payment from its bundle alone')], + ['the witness\'s value cannot be checked: its nk or salt is not this proposal\'s', () => t('the witness\'s value cannot be checked: its nk or salt is not this proposal\'s')], + ['this key is not one of the policy\'s signers', () => t('this key is not one of the policy\'s signers')], + ['a share is index:hex', () => t('a share is index:hex')], + ['a signer key is a rand-node keygen file or 64 hex characters', () => t('a signer key is a rand-node keygen file or 64 hex characters')], + ['a signer seed is 64 hex characters', () => t('a signer seed is 64 hex characters')], + ['expected_recipients must be a list of addresses', () => t('expected_recipients must be a list of addresses')], + ['the key file\'s public_key is not the one its seed derives', () => t('the key file\'s public_key is not the one its seed derives')], + ['the key file\'s address is not the one its seed derives', () => t('the key file\'s address is not the one its seed derives')], +``` + +`PATTERNED` additions (before the `// ---- proving on this device ----` block): + +```js + // ---- private multisig signing ---- + [/^this proposal is for another policy: it names ([0-9a-f]+), this policy is ([0-9a-f]+)$/, + (m) => t('this proposal is for another policy: it names {theirs}, this policy is {ours}', { theirs: m[1], ours: m[2] })], + [/^output (\d+) does not open as this account's: refusing to sign value it cannot see$/, + (m) => t('output {slot} does not open as this account\'s: refusing to sign value it cannot see', { slot: m[1] })], + [/^the witness's value does not match the transaction: input (\d+) does not nullify to the bundle's nullifier$/, + (m) => t('the witness\'s value does not match the transaction: input {input} does not nullify to the bundle\'s nullifier', { input: m[1] })], + [/^the witness's value does not match the outputs: asset (\d+) has (\d+) in and (\d+) out with the fee and burns$/, + (m) => t('the witness\'s value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns', { asset: m[1], held: m[2], spent: m[3] })], + [/^the proposal expires at (\d+), outside its bundle's window \((\d+), (\d+)\]$/, + (m) => t('the proposal expires at {expires}, outside its bundle\'s window ({from}, {to}]', { expires: m[1], from: m[2], to: m[3] })], + [/^the transaction is for chain (\d+), the proposal says chain (\d+)$/, + (m) => t('the transaction is for chain {tx}, the proposal says chain {proposal}', { tx: m[1], proposal: m[2] })], + [/^signer (\d+)'s signature does not verify over this proposal$/, (m) => t('signer {index}\'s signature does not verify over this proposal', { index: m[1] })], + [/^signer (\d+) is not in this policy \(it has (\d+) signers\)$/, (m) => t('signer {index} is not in this policy (it has {count} signers)', { index: m[1], count: m[2] })], + [/^expected recipient (\S+): ([\s\S]+)$/, (m, inner) => t('expected recipient {address}: {reason}', { address: m[1], reason: inner(m[2]) })], + [/^the policy does not parse: ([\s\S]+)$/, (m) => t('the policy does not parse: {reason}', { reason: m[1] })], + [/^the proposal does not parse: ([\s\S]+)$/, (m) => t('the proposal does not parse: {reason}', { reason: m[1] })], +``` + +Then `node ui/scripts/extract-strings.mjs` (regenerates `en.js`; the locales are filled in Task 9 — until then `i18n.test.mjs` fails on the dictionaries only, which Task 9 fixes; run the two new test files here). + +- [ ] **Step 3: Run** `node --test ui/test/crypto-bytes.test.mjs ui/test/core-errors.test.mjs ui/test/backend-wasm.test.mjs ui/test/backend-native.test.mjs` → PASS. +- [ ] **Step 4: Commit** `ui: coreApi's msauth methods, byte vault records, the msig refusals in core-errors` + trailer. + +--- + +### Task 6: Engine — signer profiles (`list`, `preview`, `add`, `remove`) in the vault + +**Files:** +- Create: `ui/engine/signers.js`, `ui/test/signers-engine.test.mjs` +- Modify: `ui/engine/backend-shared.js` (`passwordAttempt`, `openVault` over it, `startSession`, `unlock`, backend object), `ui/backend.js` (contract doc for optional `signers`), `ui/test/fake-backend.mjs` (`signers` group) + +**Interfaces:** +- Consumes: Task 5's `coreApi` methods, `encryptSecret`, `decryptSecret`, `encryptBytes`. +- Produces: `makeSigners(deps) -> { group, openOnUnlock(password) }` where `deps = { c, storage, requireUnlocked, setSession(record), openVault(password), passwordAttempt(open), requireVerifiedChain(), getSettings(), currentRpcUrl() }`. `group` (= `backend.signers`): `list() -> [{id, label, index, fingerprint, threshold, signers, address}]`; `preview({policy, key}) -> {fingerprint, threshold, signers, address, policyHash, index, publicKey}`; `add({label, policy, key, password}) -> list`; `remove(id, password) -> list`. Storage keys `SIGNER_POLICIES_KEY = 'signerPolicies'` (one `encryptSecret` record of `JSON [{id, label, policy, index, policyHash}]`), `SIGNER_SEEDS_KEY = 'signerSeeds'` (`{[id]: encryptBytes record}`). Session record `unlocked.signers` = that decrypted array. Exports `cleanText(s)`, `EXPIRY_MARGIN = 16`, `SignerRefusal`. + +- [ ] **Step 1: Failing tests.** `ui/test/signers-engine.test.mjs`: + +```js +// The engine's signer profiles (spec 2026-10-11-msauth-signing-design.md §3): kept in the vault, +// policies opened at unlock, seeds only under the password, nothing in plain storage. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { makeWasmBackend } from '../engine/backend-wasm.js'; +import { PASSWORD, mapStorage, stubCore, stubFetch, stubPlatform, GENESIS } from './backend-fixtures.mjs'; + +export const POLICY = '{"version":1,"threshold":2,"signers":["aa","bb","cc"],"msk":"' + '11'.repeat(32) + '"}'; +export const KEY = '{"seed":"' + '22'.repeat(32) + '","address":"A","public_key":"bb"}'; +export const SEED_HEX = '22'.repeat(32); +export function msauthCore(extra = {}) { + return stubCore({ + msauth_policy_info: () => ({ fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(60), policy_hash: '33'.repeat(32) }), + msauth_signer_import: ({ input, policy }) => { + if (!String(input).includes(SEED_HEX)) throw new Error('this key is not one of the policy\'s signers'); + return { seed: SEED_HEX, public_key: 'bb', address: 'A', index: policy ? 1 : null }; + }, + ...extra, + }); +} +export async function unlockedWithSigner(opts = {}) { + const storage = opts.storage || mapStorage(); + const core = opts.core || msauthCore(); + const fetch = opts.fetch || stubFetch(); + const backend = makeWasmBackend({ core, storage, platform: stubPlatform(), fetch, locks: null, broadcast: null }); + await backend.wallet.create(PASSWORD); + const [row] = await backend.signers.add({ label: 'treasury', policy: POLICY, key: KEY, password: PASSWORD }); + return { backend, storage, core, fetch, row }; +} + +test('adding a signer keeps policy and seed only in vault records', async () => { + const { storage, row } = await unlockedWithSigner(); + assert.deepEqual({ ...row, id: 'x' }, { id: 'x', label: 'treasury', index: 1, fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(60) }); + const everything = JSON.stringify([...storage.local.entries()]); + assert.ok(!everything.includes(SEED_HEX), 'no seed in plain storage'); + assert.ok(!everything.includes('11'.repeat(32)), 'no msk in plain storage'); + assert.ok(!everything.includes('treasury'), 'no label in plain storage'); + assert.ok(storage.local.get('signerPolicies').ct && storage.local.get('signerSeeds')[row.id].ct); + const session = JSON.stringify([...storage.sessionMap.entries()]); + assert.ok(!session.includes(SEED_HEX), 'the seed is never in the session'); +}); + +test('profiles survive a lock and come back with the normal unlock', async () => { + const { backend, row } = await unlockedWithSigner(); + await backend.wallet.lock(); + await assert.rejects(backend.signers.list()); + await backend.wallet.unlock(PASSWORD); + assert.deepEqual((await backend.signers.list()).map((r) => r.id), [row.id]); +}); + +test('add refuses a wrong password, a key outside the policy and a duplicate', async () => { + const { backend } = await unlockedWithSigner(); + await assert.rejects(backend.signers.add({ label: 'x', policy: POLICY, key: KEY, password: 'wrong-password-here' }), /already saved as treasury/); + await assert.rejects(backend.signers.add({ label: 'y', policy: POLICY, key: '{"seed":"' + '44'.repeat(32) + '"}', password: PASSWORD }), /not one of the policy's signers/); + await assert.rejects(backend.signers.add({ label: '', policy: POLICY, key: KEY, password: PASSWORD }), /1-64 characters/); + const fresh = await unlockedWithSigner({ core: msauthCore() }).then(async ({ backend: b, row }) => { await b.signers.remove(row.id, PASSWORD); return b; }); + await assert.rejects(fresh.signers.add({ label: 'z', policy: POLICY, key: KEY, password: 'wrong-password-here' }), /wrong password/); +}); + +test('remove deletes both halves, and only with the password', async () => { + const { backend, storage, row } = await unlockedWithSigner(); + await assert.rejects(backend.signers.remove(row.id, 'wrong-password-here'), /wrong password/); + assert.deepEqual(await backend.signers.remove(row.id, PASSWORD), []); + assert.equal(storage.local.get('signerSeeds')[row.id], undefined); +}); + +test('a BOM-prefixed policy or key file is read like any other', async () => { + const { backend } = await unlockedWithSigner(); + const p = await backend.signers.preview({ policy: `${POLICY}\n`, key: `${KEY}\r\n` }); + assert.equal(p.index, 1); +}); + +test('a wipe removes every profile', async () => { + const { backend, storage } = await unlockedWithSigner(); + await backend.wallet.wipe(); + assert.equal(storage.local.get('signerPolicies'), undefined); +}); +``` + +Run `node --test ui/test/signers-engine.test.mjs` → FAIL (`backend.signers` undefined). + +- [ ] **Step 2: `ui/engine/signers.js`** (the profile half; Task 7 adds `check`/`sign`/`verifyShare` to the same `group`): + +```js +// Private multisig signer profiles (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md +// §3, §4) for the shared engine: web, extension, desktop. +// +// A profile is {label, policy, seed}; both halves are secret (the seed signs, the policy's `msk` +// sees every note of the account). Storage holds two vault records: `signerPolicies`, ONE +// `encryptSecret` record of every profile's {id, label, policy, index, policyHash}, opened at unlock +// into the session beside the spend key; and `signerSeeds`, one `encryptBytes` record per seed, +// opened only by `sign()` with the password typed for that signature — never at unlock, never kept. +import { encryptSecret, decryptSecret, encryptBytes, decryptBytes } from './crypto.js'; +import { t } from '../i18n.js'; + +export const SIGNER_POLICIES_KEY = 'signerPolicies'; +export const SIGNER_SEEDS_KEY = 'signerSeeds'; +/** Blocks a proposal needs before its window closes: the CLI's `EXPIRY_MARGIN` (fullnode d5b2e652). */ +export const EXPIRY_MARGIN = 16; + +export class SignerRefusal extends Error { + constructor(code, message, detail = {}) { + super(message); + this.name = 'SignerRefusal'; + this.code = code; + this.definite = true; + Object.assign(this, detail); + } +} + +/** A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. */ +export function cleanText(s) { + return String(s ?? '').replace(/^/, '').trim(); +} + +export const hexOf = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); +export function bytesOf(hex) { + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i += 1) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); + return out; +} +const randomId = () => hexOf(crypto.getRandomValues(new Uint8Array(8))); + +function validProfiles(v) { + if (!Array.isArray(v)) return []; + return v.filter((p) => p && typeof p.id === 'string' && typeof p.label === 'string' && typeof p.policy === 'string' + && Number.isInteger(p.index) && typeof p.policyHash === 'string'); +} + +export function makeSigners(deps) { + const { c, storage, requireUnlocked, setSession, openVault } = deps; + + async function profiles() { + const u = await requireUnlocked(); + return validProfiles(u.signers); + } + async function profileOf(id) { + const p = (await profiles()).find((x) => x.id === id); + if (!p) throw new Error(t('No such signer profile.')); + return p; + } + async function requirePassword(password) { + if ((await openVault(password)) === null) throw new Error('wrong password'); + } + async function writeProfiles(password, list) { + await storage.set(SIGNER_POLICIES_KEY, await encryptSecret(password, JSON.stringify(list))); + await setSession({ ...(await requireUnlocked()), signers: list }); + } + async function row(p) { + const info = await c.msauthPolicyInfo(p.policy); + return { id: p.id, label: p.label, index: p.index, fingerprint: info.fingerprint, threshold: info.threshold, signers: info.signers, address: info.address }; + } + async function list() { + return Promise.all((await profiles()).map(row)); + } + + const group = { + list, + async preview({ policy, key }) { + const pol = cleanText(policy); + const info = await c.msauthPolicyInfo(pol); + const signer = await c.msauthSignerImport(cleanText(key), pol); + return { fingerprint: info.fingerprint, threshold: info.threshold, signers: info.signers, address: info.address, policyHash: info.policy_hash, index: signer.index, publicKey: signer.public_key }; + }, + async add({ label, policy, key, password }) { + const name = String(label ?? '').trim(); + if (!name || [...name].length > 64) throw new Error(t('a signer label is 1-64 characters')); + const pol = cleanText(policy); + const info = await c.msauthPolicyInfo(pol); + const signer = await c.msauthSignerImport(cleanText(key), pol); + const current = await profiles(); + const same = current.find((p) => p.policyHash === info.policy_hash && p.index === signer.index); + if (same) throw new Error(t('this signer is already saved as {label}', { label: same.label })); + await requirePassword(password); + const id = randomId(); + const seed = bytesOf(signer.seed); + try { + const seeds = (await storage.get(SIGNER_SEEDS_KEY)) || {}; + seeds[id] = await encryptBytes(password, seed); + await storage.set(SIGNER_SEEDS_KEY, seeds); + } finally { + seed.fill(0); + } + await writeProfiles(password, [...current, { id, label: name, policy: pol, index: signer.index, policyHash: info.policy_hash }]); + return list(); + }, + async remove(id, password) { + await profileOf(id); + await requirePassword(password); + const seeds = (await storage.get(SIGNER_SEEDS_KEY)) || {}; + delete seeds[id]; + await storage.set(SIGNER_SEEDS_KEY, seeds); + await writeProfiles(password, (await profiles()).filter((p) => p.id !== id)); + return list(); + }, + }; + + /** The profiles, opened with the password that just opened the vault; `[]` if none or unreadable. */ + async function openOnUnlock(password) { + const rec = await storage.get(SIGNER_POLICIES_KEY); + if (!rec) return []; + try { return validProfiles(JSON.parse(await decryptSecret(password, rec))); } catch { return []; } + } + + return { group, openOnUnlock, profileOf, deps, decryptBytes }; +} +``` + +- [ ] **Step 3: Wire into `backend-shared.js`.** + - Import: `import { makeSigners } from './signers.js';` + - Replace `openVault` with a shared attempt and the vault read: + +```js + /** + * One password attempt through the shared throttle: queued, delayed by the failure count, counted + * before the KDF runs, cleared on success. `open()` resolves the plaintext or throws; a record + * error (`isVaultRecordError`) is rethrown and not counted as a wrong password. Used by the + * unlock and by every signer signature (signers.js). + */ + function passwordAttempt(open) { + return enqueueAttempt(async () => { + const rec = await failureRecord(); + await sleep(unlockDelayMs(rec.count)); + await bumpFailures(rec); + let value; + try { + value = await open(); + } catch (err) { + if (isVaultRecordError(err)) throw err; + return null; + } + await clearFailures(); + return value; + }); + } + + async function openVault(password) { + const vault = await storage.get(K.vault); + if (!vault) throw new Error(t('no wallet on this device')); + checkVault(vault); // VaultVersionError / VaultDamagedError — not an attempt + return passwordAttempt(() => decryptSecret(password, vault)); + } +``` + + (Keep `openVault`'s doc comment; the `no wallet` and `checkVault` checks still run before any count.) + - `startSession(info, pairing, signerProfiles = [])`: add `record.signers = signerProfiles;` after `record.prover`. + - After `const engine = makeWallet(...)`: + +```js + const signers = makeSigners({ + c, storage, requireUnlocked, openVault, passwordAttempt, requireVerifiedChain, getSettings, currentRpcUrl, + setSession: (record) => storage.session.set(K.unlocked, record), + }); +``` + + - `wallet.unlock`: `await startSession(info, await openProverPairing(password), await signers.openOnUnlock(password));` + - Backend object: `const backend = { wallet, sync, assets, send, faucet, rpc, settings, platform, address, contacts, prover, signers: signers.group, dispose };` + - `ui/backend.js` optional-groups doc: add + +```js + * - `signers?` — private multisig signer profiles (spec 2026-10-11-msauth-signing-design.md): + * `list()`, `preview({policy, key})`, `add({label, policy, key, password})`, `remove(id, password)`, + * `check(id, proposal, expectedRecipients)` → `{summary, signable, refusal?, head?, blocksLeft?}`, + * `sign(id, proposal, expectedRecipients, password)` → `{share, index}`, `verifyShare(id, proposal, share)` → `{index}`. + * Screens offer Settings → Multisig signers only where it exists. +``` + + - `ui/test/fake-backend.mjs`: add `signers: []` to `state` and a group (Task 8's screen tests drive it): + +```js + const signersDefs = { + list: () => state.signers.map(({ id, label, index }) => ({ id, label, index, fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(40) })), + preview: () => ({ fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(40), policyHash: '33'.repeat(32), index: 1, publicKey: 'bb' }), + add: ({ label, password }) => { + if (password !== state.wallet.password) throw new Error('wrong password'); + state.signers.push({ id: `s${state.signers.length + 1}`, label, index: 1 }); + return signersDefs.list(); + }, + remove: (id) => { state.signers = state.signers.filter((s) => s.id !== id); return signersDefs.list(); }, + check: () => state.signerCheck, + sign: (id, proposal, expected, password) => { + if (password !== state.wallet.password) throw new Error('wrong password'); + state.signCalls = (state.signCalls || 0) + 1; + return { share: '1:' + 'ab'.repeat(8), index: 1 }; + }, + verifyShare: (id, proposal, share) => ({ index: Number(String(share).split(':')[0]) }), + }; +``` + + with `signers: buildGroup('signers', signersDefs, overrides.signers, calls),` in the backend object, and `state.signerCheck` defaulting to `{ summary: FAKE_SUMMARY, signable: true, head: 100, blocksLeft: 1924 }` where + +```js +export const FAKE_SUMMARY = Object.freeze({ + outputs: [ + { owner_fingerprint: 'cb5c9747f9a2ef35', to_account: false, amount: '0', asset: 0, memo: '', matches: null, needs_payee: false }, + { owner_fingerprint: '2c94b28813d4ce65', to_account: false, amount: '0', asset: 0, memo: '', matches: null, needs_payee: false }, + { owner_fingerprint: '35bad5bd21a6f0f7', to_account: false, amount: '10000000000', asset: 0, memo: '', matches: 'ABCD-EFGH-JKMN-PQRS', needs_payee: true }, + { owner_fingerprint: '4333dd507bcc4286', to_account: true, amount: '14999000000', asset: 0, memo: '', matches: null, needs_payee: false }, + ], recipients_checked: true, fee: '1000000', burns: { burn_a: '0', burn_asset: 0, burn_r: '0' }, expires_at: 2024, + signing_message: '00', policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', chain_id: 21, genesis: '00'.repeat(32), binding_domain: 1, + lines: [ + '0: → 0 RAND to cb5c9747f9a2ef35 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '1: → 0 RAND to 2c94b28813d4ce65 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '2: → 10 RAND to 35bad5bd21a6f0f7 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '3: → 14.999 RAND to this account 4333dd507bcc4286 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + 'fee 0.001 RAND', 'burn 0 of asset 0, 0 RAND', + ], +}); +``` + +- [ ] **Step 4: Run** `node --test ui/test/signers-engine.test.mjs ui/test/backend-wasm.test.mjs ui/test/backend-native.test.mjs ui/test/backend.test.mjs` → PASS (the unlock-throttle cases in `backend-cases.mjs` still pass over `passwordAttempt`). +- [ ] **Step 5: Commit** `ui/engine: signer profiles in the vault — policies opened at unlock, seeds under the password only; passwordAttempt shared with the unlock` + trailer. + +--- + +### Task 7: Engine — `check`, `sign`, `verifyShare` (head, chain, per-signature password) + +**Files:** +- Modify: `ui/engine/signers.js`, `ui/test/signers-engine.test.mjs` + +**Interfaces:** +- Consumes: Task 6's `makeSigners` deps (`requireVerifiedChain() -> {client, url, identity: {chainId, genesis}}`, `client.head() -> {height}`, `getSettings().chainId`, `currentRpcUrl()`, `passwordAttempt`). +- Produces: `verdict({summary, head, chainId, genesis, url}) -> {signable: true, head, blocksLeft} | {signable: false, refusal: {code: 'unchecked-recipient'|'no-node'|'wrong-chain'|'wrong-genesis'|'expired', message, node?}}` (exported); `group.check(id, proposal, expectedRecipients = [])`, `group.sign(id, proposal, expectedRecipients, password) -> {share, index}` (throws `SignerRefusal` with `.code`), `group.verifyShare(id, proposal, share) -> {index}`. + +- [ ] **Step 1: Failing tests** (append): + +```js +import { verdict, EXPIRY_MARGIN } from '../engine/signers.js'; + +const SUMMARY = { expires_at: 200, chain_id: 13, genesis: GENESIS, binding_domain: 1, lines: ['x'], policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', recipients_checked: true, + outputs: [{ to_account: false, amount: '5', needs_payee: true, matches: 'FP' }] }; +const signingCore = (extra = {}) => msauthCore({ + msauth_check: () => ({ ...SUMMARY }), + msauth_sign: ({ seed }) => { if (seed !== SEED_HEX) throw new Error('bad seed'); return '1:' + 'ab'.repeat(8); }, + msauth_verify_share: ({ share }) => { if (!share.startsWith('0:')) throw new Error('signer 1\'s signature does not verify over this proposal'); return true; }, + ...extra, +}); +const atHead = (height, table = {}) => stubFetch({ rand_getHead: () => ({ height, hash: 'ff'.repeat(32), view: 1 }), ...table }); + +test('an unmatched or unchecked payee refuses signing, before anything else', async () => { + const unchecked = { ...SUMMARY, recipients_checked: false, outputs: [{ to_account: false, amount: '5', needs_payee: true, matches: null }] }; + assert.equal(verdict({ summary: unchecked, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'unchecked-recipient'); + assert.equal(verdict({ summary: unchecked, head: null, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'unchecked-recipient'); + const seen = []; + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: ({ expected_recipients }) => { seen.push(expected_recipients); return unchecked; } }), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee'], PASSWORD), (e) => e.code === 'unchecked-recipient'); + assert.deepEqual(seen, [['rand1payee']], 'the payees reach the core'); +}); + +test('the expiry rule is the CLI\'s: refuse at head + 16 >= expires_at', () => { + assert.equal(EXPIRY_MARGIN, 16); + assert.equal(verdict({ summary: SUMMARY, head: 183, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true); + assert.equal(verdict({ summary: SUMMARY, head: 184, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'expired'); +}); + +test('a proposal for another chain or genesis is refused', async () => { + assert.equal(verdict({ summary: { ...SUMMARY, chain_id: 21 }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'wrong-chain'); + assert.equal(verdict({ summary: { ...SUMMARY, genesis: 'bb'.repeat(32) }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'wrong-genesis'); + assert.equal(verdict({ summary: { ...SUMMARY, binding_domain: 0, genesis: 'bb'.repeat(32) }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true, 'a chain-id-bound proposal binds no genesis'); + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => ({ ...SUMMARY, chain_id: 21 }) }), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee'], PASSWORD), (e) => e.code === 'wrong-chain'); +}); + +test('with no node the proposal is shown but not signed, naming the node', async () => { + const dead = async () => { throw new TypeError('fetch failed'); }; + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: dead }); + const r = await backend.signers.check(row.id, 'P', ['rand1payee']); + assert.equal(r.signable, false); + assert.equal(r.refusal.code, 'no-node'); + assert.match(r.refusal.message, /rpc\.randprotocol\.org/); + assert.deepEqual(r.summary.lines, ['x'], 'the review still shows what was checked'); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee'], PASSWORD), (e) => e.code === 'no-node'); +}); + +test('a node that cannot give a head is no node', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: stubFetch({ rand_getHead: () => { throw new Error('busy'); } }) }); + assert.equal((await backend.signers.check(row.id, 'P', ['rand1payee'])).refusal.code, 'no-node'); +}); + +test('every signature asks for the password and caches nothing', async () => { + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + const before = JSON.stringify([...storage.sessionMap.entries()]); + assert.deepEqual(await backend.signers.sign(row.id, 'P', ['rand1payee'], PASSWORD), { share: '1:' + 'ab'.repeat(8), index: 1 }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee'], ''), /wrong password/, 'the second signature asks again'); + assert.equal(JSON.stringify([...storage.sessionMap.entries()]), before, 'the session gained nothing'); + assert.ok(!JSON.stringify([...storage.local.entries()]).includes(SEED_HEX)); + assert.ok(!JSON.stringify([...storage.local.entries()]).includes('"P"'), 'the proposal is never stored'); +}); + +test('a wrong password at signing is throttled like an unlock', async () => { + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee'], 'wrong-password-here'), /wrong password/); + assert.equal(storage.local.get('unlockFailures').count, 1); + await backend.signers.sign(row.id, 'P', ['rand1payee'], PASSWORD); + assert.equal(storage.local.get('unlockFailures'), undefined, 'a right password clears the count'); +}); + +test('verifyShare names the position and trims a saved file\'s newline', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + assert.deepEqual(await backend.signers.verifyShare(row.id, 'P', '0:abcd\n'), { index: 0 }); + await assert.rejects(backend.signers.verifyShare(row.id, 'P', '1:abcd'), /does not verify/); +}); +``` + +Run → FAIL (`verdict` not exported, `check` missing). + +- [ ] **Step 2: Implement** in `signers.js`: + +```js +/** Whether a checked proposal may be signed now, by the CLI's rule, on this wallet's chain. */ +export function verdict({ summary, head, chainId, genesis, url }) { + // Spec §4 step 3: never sign on an unchecked payee (a coordinator could pay its own key). + if (summary.recipients_checked !== true) { + return { signable: false, refusal: { code: 'unchecked-recipient', message: t('Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.') } }; + } + if (head == null) { + return { signable: false, refusal: { code: 'no-node', node: url, message: t('No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.', { url: url || '—' }) } }; + } + if (Number(summary.chain_id) !== Number(chainId)) { + return { signable: false, refusal: { code: 'wrong-chain', message: t('This proposal is for chain {theirs}; this wallet is on chain {ours}.', { theirs: String(summary.chain_id), ours: String(chainId) }) } }; + } + if (Number(summary.binding_domain) >= 1 && String(summary.genesis).toLowerCase() !== String(genesis || '').toLowerCase()) { + return { signable: false, refusal: { code: 'wrong-genesis', message: t('This proposal is bound to another genesis than the one this wallet\'s node serves.') } }; + } + if (head + EXPIRY_MARGIN >= Number(summary.expires_at)) { + return { signable: false, refusal: { code: 'expired', message: t('This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.', { expires: String(summary.expires_at), head: String(head) }) } }; + } + return { signable: true, head, blocksLeft: Number(summary.expires_at) - head }; +} +``` + +Inside `makeSigners`, destructure `passwordAttempt, requireVerifiedChain, getSettings, currentRpcUrl` from `deps` and add: + +```js + /** The head from a node that passed this wallet's chain check; any failure is "no node". */ + async function judge(summary) { + const settings = await getSettings(); + let url = ''; + try { url = await currentRpcUrl(); } catch { /* no endpoint configured */ } + try { + const { client, url: u, identity } = await requireVerifiedChain(); + const h = await client.head(); + const height = Number(h && h.height); + if (!Number.isSafeInteger(height) || height < 0) throw new Error('no height'); + return verdict({ summary, head: height, chainId: settings.chainId, genesis: identity && identity.genesis, url: u }); + } catch { + return verdict({ summary, head: null, url }); + } + } +``` + +and, at module level, `const payees = (list) => (Array.isArray(list) ? list : []).map(cleanText).filter(Boolean);`, and to `group`: + +```js + // The proposal is a parameter, never stored: it carries the account's witness (§3). + async check(id, proposal, expectedRecipients = []) { + const p = await profileOf(id); + const summary = await c.msauthCheck(cleanText(proposal), p.policy, payees(expectedRecipients)); + return { summary, ...(await judge(summary)) }; + }, + async sign(id, proposal, expectedRecipients, password) { + const p = await profileOf(id); + const text = cleanText(proposal); + const v = await judge(await c.msauthCheck(text, p.policy, payees(expectedRecipients))); + if (!v.signable) throw new SignerRefusal(v.refusal.code, v.refusal.message, { node: v.refusal.node }); + const rec = ((await storage.get(SIGNER_SEEDS_KEY)) || {})[id]; + if (!rec) throw new Error(t('This signer profile has no key on this device: remove it and add it again.')); + const seed = await passwordAttempt(() => decryptBytes(password, rec)); + if (!seed) throw new Error('wrong password'); + try { + // The hex copy below cannot be wiped (a JS string); it is unreachable after this call, and + // wallet-core holds its own copy in Zeroizing. The bytes are wiped here. + const share = await c.msauthSign(text, p.policy, hexOf(seed)); + return { share, index: Number(String(share).split(':')[0]) }; + } finally { + seed.fill(0); + } + }, + async verifyShare(id, proposal, share) { + const p = await profileOf(id); + const s = cleanText(share); + await c.msauthVerifyShare(cleanText(proposal), p.policy, s); + return { index: Number(s.split(':')[0].trim()) }; + }, +``` + +(`decryptBytes` is imported at the top already; drop it from the returned object.) + +- [ ] **Step 3: Run** `node --test ui/test/signers-engine.test.mjs` → PASS. +- [ ] **Step 4: Commit** `ui/engine: signers check/sign/verifyShare — the CLI's expiry margin against a verified head, no node no signature, the password per signature` + trailer. + +--- + +### Task 8: Screens — `#signers`, `#signer/`, the file picker, saving a share + +**Files:** +- Create: `ui/screens/signers.js`, `ui/test/signers-screens.test.mjs` +- Modify: `ui/app.js` (import the screen module beside `contacts.js`), `ui/screens/settings.js` (entry), `ui/test/i18n.test.mjs` (`SCREENS` += `['signers', …]`, `['signer/s1', …]`), `web/wallet/main.js` and `extension/shared/lib/platform.js` (`platform.saveText`), `desktop/ui-shell/backend-tauri.js`, `desktop/src-tauri/src/commands.rs`, `desktop/src-tauri/src/main.rs` (command registration), `ui/backend.js` (doc `platform.saveText?`) + +**Interfaces:** +- Consumes: `backend.signers` (Task 6/7), `ctx.go`, `ctx.mode`, `ctx.isCurrent`, `platform.copy`, `platform.share?`, `platform.openSidebar?`. +- Produces: exports `lastFourOf(fp)`, `confirmsFingerprint(fp, typed)`, `MAX_FILE_BYTES = 1048576`; `platform.saveText?({name, text}) -> Promise`; Tauri command `save_text_to_downloads(name: String, text: String) -> Result` over `save_text_in(dir: &Path, name: &str, text: &str) -> Result`. + +- [ ] **Step 1: Failing screen tests.** `ui/test/signers-screens.test.mjs`: + +```js +import test from 'node:test'; +import assert from 'node:assert/strict'; +import './dom-env.mjs'; +import { unlockedBackend } from './fake-backend.mjs'; +import { mountApp } from './helpers.mjs'; +import { confirmsFingerprint, lastFourOf } from '../screens/signers.js'; + +const tick = () => new Promise((r) => setTimeout(r, 0)); +const signerBackend = (over = {}) => { + const b = unlockedBackend(over); + return b; +}; + +test('confirmation is the fingerprint\'s last four, without case or dashes', () => { + assert.equal(lastFourOf('NFYW-K8E9-E5K9-AAK3'), 'aak3'); + assert.ok(confirmsFingerprint('NFYW-K8E9-E5K9-AAK3', ' aak3 ')); + assert.ok(!confirmsFingerprint('NFYW-K8E9-E5K9-AAK3', 'AAK')); + assert.ok(!confirmsFingerprint('', '')); +}); + +test('settings offers the signers screen only where the backend has the group', async (t) => { + const { app, root } = await mountApp(t, signerBackend(), { hash: '#settings' }); + await app.idle(); + assert.ok(root.querySelector('[data-go="signers"]')); +}); + +test('a proposal\'s lines are shown verbatim, as text, with the caveat', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const lines = [...root.querySelectorAll('[data-role="line"]')].map((n) => n.textContent); + assert.equal(lines.length, 6); + assert.ok(lines.slice(0, 4).every((l) => l.endsWith('(owner key verified; delivery key NOT verifiable from this proposal)'))); +}); + +test('sign stays disabled until the last four are typed, and asks for the password each time', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const sign = root.querySelector('[data-role="sign"]'); + assert.equal(sign.disabled, true); + const confirm = root.querySelector('input[data-role="confirm"]'); + confirm.value = 'aak3'; confirm.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(sign.disabled, false); + root.querySelector('input[data-role="password"]').value = 'unlocked-password-1'; + sign.click(); + await app.idle(); await tick(); + assert.equal(root.querySelector('[data-role="share"]').value, '1:' + 'ab'.repeat(8)); + assert.equal(root.querySelector('input[data-role="password"]').value, '', 'the password field is emptied'); +}); + +test('every paying output asks for its payee, and the typed addresses reach check and sign', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const seen = []; + const original = b.signers.check; + b.signers.check = async (id, proposal, payees) => { seen.push(payees); return original(id, proposal, payees); }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const inputs = root.querySelectorAll('input[data-role="payee"]'); + assert.equal(inputs.length, 1, 'only output 2 pays someone: dummies and change ask nothing'); + inputs[0].value = 'rand1payee'; + root.querySelector('[data-role="check-payees"]').click(); + await app.idle(); await tick(); + assert.deepEqual(seen.at(-1), ['rand1payee']); +}); + +test('a refused check names the node and offers Retry, and nothing can be signed', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + b.signers.check = async () => ({ summary: { outputs: [], recipients_checked: true, lines: ['fee 0.001 RAND'], policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', expires_at: 2024, fee: '1000000', burns: { burn_a: '0', burn_asset: 0, burn_r: '0' } }, + signable: false, refusal: { code: 'no-node', node: 'https://rpc.example', message: 'No node answered at https://rpc.example, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.' } }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + assert.match(root.querySelector('[data-role="refusal"]').textContent, /rpc\.example/); + assert.ok(root.querySelector('[data-role="retry"]')); + assert.equal(root.querySelector('[data-role="sign"]'), null); +}); + +test('the popup offers the side panel instead of a file picker', async (t) => { + const b = signerBackend(); + b.platform.openSidebar = async () => {}; + const { app, root } = await mountApp(t, b, { hash: '#signers', mode: 'popup' }); + await app.idle(); + assert.equal(root.querySelector('input[type=file]'), null); + assert.ok(root.querySelector('[data-role="to-sidebar"]')); +}); + +test('a profile label from storage is text, never markup', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: '', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signers' }); + await app.idle(); + assert.equal(root.querySelector('img'), null); +}); +``` + +(If `mountApp` names its mode option differently, read `ui/test/helpers.mjs` and use its name.) Run → FAIL. + +- [ ] **Step 2: `ui/screens/signers.js`.** + +```js +// Private multisig signing (spec 2026-10-11-msauth-signing-design.md §4): `#signers` lists the +// signer profiles and adds one; `#signer/` checks a proposal, shows exactly what the core +// checked, signs it after the fingerprint's last four characters and the password, and checks a +// share. Every value from a file, a paste, storage or the core reaches the page as text. The +// proposal lives in this screen's closure only and is dropped when it unmounts. +import { h, raw } from '../lib/dom.js'; +import { icons } from '../lib/icons.js'; +import { registerScreen } from '../app.js'; +import { formatUnits } from '../lib/format.js'; +import { t } from '../i18n.js'; + +export const MAX_FILE_BYTES = 1 << 20; +export function lastFourOf(fingerprint) { + return String(fingerprint || '').replace(/-/g, '').slice(-4).toLowerCase(); +} +export function confirmsFingerprint(fingerprint, typed) { + const want = lastFourOf(fingerprint); + return want.length === 4 && String(typed || '').replace(/[-\s]/g, '').toLowerCase() === want; +} +async function readPicked(input) { + const file = input.files && input.files[0]; + if (!file) return null; + if (file.size > MAX_FILE_BYTES) throw new Error(t('That file is larger than a proposal, a policy or a key file can be.')); + return file.text(); +} +const topbar = (title, back) => h`
${title}
`; +function fileField(ctx, { role, label }) { + const popup = ctx.mode === 'popup'; + const picker = popup ? '' : raw(h``); + return h`
${picker}
`; +} +function sidebarOffer(ctx) { + return ctx.mode === 'popup' && typeof ctx.backend.platform.openSidebar === 'function' + ? h`` : ''; +} +function wireFiles(ctx, root, onError) { + for (const input of root.querySelectorAll('input[type=file][data-file]')) { + input.addEventListener('change', async () => { + try { + const text = await readPicked(input); + if (text != null && ctx.isCurrent()) root.querySelector(`textarea[data-role="${input.dataset.file}"]`).value = text; + } catch (err) { onError(err.message); } + input.value = ''; + }); + } + const side = root.querySelector('[data-role="to-sidebar"]'); + if (side) side.addEventListener('click', () => { Promise.resolve(ctx.backend.platform.openSidebar()).catch(() => {}); }); +} +function showError(el, message) { + el.textContent = message || ''; + if (message) el.removeAttribute('hidden'); else el.setAttribute('hidden', ''); +} + +registerScreen('signers', { + render: () => h`

${t('Multisig signers')}

${raw(topbar(t('Multisig signers'), 'settings'))}
`, + async after(ctx, root) { + const group = ctx.backend.signers; + const body = root.querySelector('[data-role="body"]'); + if (!group || typeof group.list !== 'function') { + body.innerHTML = h`
${t('Multisig signing is not available in this app.')}
`; + return; + } + body.innerHTML = h` +
+

${t('Add a signer')}

+
+ ${raw(sidebarOffer(ctx))} +
+ ${raw(fileField(ctx, { role: 'policy', label: t('Policy file') }))} + ${raw(fileField(ctx, { role: 'key', label: t('Signer key file, or its 64-hex seed') }))} + + +
+
+

${t('A signer profile is kept in this wallet\'s encrypted vault. Every signature asks for your password again.')}

`; + const errorEl = body.querySelector('[data-role="error"]'); + const out = body.querySelector('[data-role="preview-out"]'); + const field = (r) => body.querySelector(`[data-role="${r}"]`); + wireFiles(ctx, body, (m) => showError(errorEl, m)); + for (const r of ['label', 'policy', 'key']) field(r).addEventListener('input', () => { out.textContent = ''; }); + + async function paintList() { + const listEl = body.querySelector('[data-role="list"]'); + const rows = await group.list(); + if (!ctx.isCurrent()) return; + if (rows.length === 0) { + listEl.innerHTML = h`
${t('No signer profiles yet')}${t('Add the policy file and your signer key to sign for a private multisig account.')}
`; + return; + } + listEl.innerHTML = h`
    ${raw(rows.map((r) => h`
  • `).join(''))}
`; + listEl.querySelectorAll('li').forEach((li, i) => { + li.querySelector('[data-role="row-label"]').textContent = rows[i].label; + li.querySelector('[data-role="row-fp"]').textContent = rows[i].fingerprint; + }); + } + + field('preview').addEventListener('click', async () => { + showError(errorEl, ''); + try { + const p = await group.preview({ policy: field('policy').value, key: field('key').value }); + if (!ctx.isCurrent()) return; + out.innerHTML = h`
+ ${t('Account fingerprint — compare it with every other signer before you save')} + + ${t('{threshold} of {count} · this key is signer {index}', { threshold: String(p.threshold), count: String(p.signers), index: String(p.index) })} +
+
`; + out.querySelector('[data-role="pv-fp"]').textContent = p.fingerprint; + out.querySelector('[data-role="pv-address"]').textContent = p.address; + out.querySelector('[data-role="save"]').addEventListener('click', async () => { + const pw = out.querySelector('[data-role="add-password"]'); + try { + await group.add({ label: field('label').value, policy: field('policy').value, key: field('key').value, password: pw.value }); + for (const r of ['label', 'policy', 'key']) field(r).value = ''; + out.textContent = ''; + await paintList(); + } catch (err) { showError(errorEl, err.message); } finally { pw.value = ''; } + }); + } catch (err) { showError(errorEl, err.message); } + }); + await paintList(); + }, +}); + +registerScreen('signer', { + render: () => h`

${t('Sign a proposal')}

${raw(topbar(t('Sign a proposal'), 'signers'))}
`, + async after(ctx, root, id) { + const group = ctx.backend.signers; + const body = root.querySelector('[data-role="body"]'); + const profile = (await group.list()).find((p) => p.id === id); + if (!ctx.isCurrent()) return; + if (!profile) { body.textContent = t('No such signer profile.'); return; } + let proposal = null; // this screen's only copy; never stored + let payees = []; // the addresses the signer typed, in the order of the outputs that need one + const inHand = new Set(); + body.innerHTML = h` +
+ ${t('{threshold} of {count} · signer {index}', { threshold: String(profile.threshold), count: String(profile.signers), index: String(profile.index) })}
+
${raw(sidebarOffer(ctx))}${raw(fileField(ctx, { role: 'proposal', label: t('Proposal (proposal.json)') }))} + +
+
+

${t('Check a share')}

+
+

+
+
`; + body.querySelector('[data-role="p-label"]').textContent = profile.label; + body.querySelector('[data-role="p-fp"]').textContent = profile.fingerprint; + const errorEl = body.querySelector('[data-role="error"]'); + const review = body.querySelector('[data-role="review"]'); + wireFiles(ctx, body, (m) => showError(errorEl, m)); + + function paintReview(r) { + const s = r.summary; + review.innerHTML = h`
+ ${t('What this proposal does, as checked against your policy')} +
${raw(s.lines.map(() => '
').join(''))}
+ ${t('Fee: {fee} RAND · expires at height {height}', { fee: formatUnits(s.fee), height: String(s.expires_at) })} + ${t('Enter the address you mean to pay for each payment below. An output that does not match it cannot be signed.')} +
${raw(s.outputs.filter((o) => o.needs_payee).map((o, i) => h`
`).join(''))} +
+
`; + review.querySelectorAll('[data-role="line"]').forEach((n, i) => { n.textContent = s.lines[i]; }); + review.querySelectorAll('input[data-role="payee"]').forEach((n, i) => { n.value = payees[i] || ''; }); + review.querySelector('[data-role="check-payees"]').addEventListener('click', () => { + payees = [...review.querySelectorAll('input[data-role="payee"]')].map((n) => n.value.trim()); + runCheck(); + }); + const act = review.querySelector('[data-role="act"]'); + if (!r.signable) { + act.innerHTML = h``; + act.querySelector('[data-role="refusal"]').textContent = r.refusal.message; + act.querySelector('[data-role="retry"]').addEventListener('click', runCheck); + return; + } + act.innerHTML = h` + ${t('{blocks} blocks left to prove it.', { blocks: String(r.blocksLeft) })} +
+
+ +
`; + const confirm = act.querySelector('[data-role="confirm"]'); + const sign = act.querySelector('[data-role="sign"]'); + confirm.addEventListener('input', () => { sign.disabled = !confirmsFingerprint(s.policy_fingerprint, confirm.value); }); + sign.addEventListener('click', async () => { + const pw = act.querySelector('[data-role="password"]'); + const errEl = act.querySelector('[data-role="sign-error"]'); + showError(errEl, ''); + try { + const { share, index } = await group.sign(id, proposal, payees, pw.value); + if (!ctx.isCurrent()) return; + paintShare(act.querySelector('[data-role="share-out"]'), share, index); + } catch (err) { showError(errEl, err.message); } finally { pw.value = ''; } + }); + } + function paintShare(el, share, index) { + const p = ctx.backend.platform; + el.innerHTML = h`
+ + + ${raw(typeof p.saveText === 'function' ? h`` : '')} + ${raw(typeof p.share === 'function' ? h`` : '')}
`; + el.querySelector('[data-role="share"]').value = share; + el.querySelector('[data-role="copy"]').addEventListener('click', () => p.copy(share)); + el.querySelector('[data-role="save-share"]')?.addEventListener('click', () => p.saveText({ name: `share-${index}.txt`, text: `${share}\n` })); + el.querySelector('[data-role="share-sheet"]')?.addEventListener('click', () => p.share({ title: t('Multisig share'), text: share })); + } + async function runCheck() { + showError(errorEl, ''); + const text = body.querySelector('textarea[data-role="proposal"]').value; + if (text !== proposal) payees = []; // a new proposal: its payees are asked again + proposal = text; + try { + const r = await group.check(id, proposal, payees); + if (ctx.isCurrent()) paintReview(r); + } catch (err) { review.textContent = ''; showError(errorEl, err.message); } + } + body.querySelector('[data-role="check"]').addEventListener('click', runCheck); + body.querySelector('[data-role="verify"]').addEventListener('click', async () => { + const outEl = body.querySelector('[data-role="verify-out"]'); + try { + const { index } = await group.verifyShare(id, proposal ?? '', body.querySelector('[data-role="share-in"]').value); + inHand.add(index); + outEl.textContent = t('Signer {index}\'s share verifies. {have} of {threshold} in hand: signers {list}.', { index: String(index), have: String(inHand.size), threshold: String(profile.threshold), list: [...inHand].sort((a, b) => a - b).join(', ') }); + } catch (err) { outEl.textContent = err.message; } + }); + body.querySelector('[data-role="remove"]').addEventListener('click', async () => { + const pw = body.querySelector('[data-role="remove-password"]'); + try { await group.remove(id, pw.value); ctx.go('#signers'); } catch (err) { showError(errorEl, err.message); } finally { pw.value = ''; } + }); + return () => { proposal = null; }; + }, +}); +``` + +(If `after` cannot return a cleanup in `app.js`, use the screen's existing cleanup hook — read how `send.js` registers one — to null `proposal`.) + +- [ ] **Step 3: Entry and platforms.** + - `ui/app.js`: add `import('./screens/signers.js'),` after `contacts.js` in the screen import list. + - `ui/screens/settings.js`, beside `contactsMarkup`: + +```js +// Offered only where the backend has the (optional) `signers` group. +function signersMarkup() { + return raw(h`

${t('Multisig signers')}

+ `); +} +``` + + and in the render, after the contacts line: `${raw(ctx.backend.signers && typeof ctx.backend.signers.list === 'function' ? signersMarkup() : '')}` (match how `contactsMarkup()` is spliced). + - `web/wallet/main.js` and `extension/shared/lib/platform.js` platform objects: + +```js + // A share (`i:hex`) or other small text as a downloaded file. Not secret. + saveText: ({ name, text }) => { + const url = URL.createObjectURL(new Blob([String(text)], { type: 'text/plain' })); + const a = document.createElement('a'); + a.href = url; a.download = String(name); a.rel = 'noopener'; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }, +``` + + - `desktop/src-tauri/src/commands.rs`: + +```rust +/// A share file in the user's Downloads, never over an existing file. `name` is +/// `share-.txt`-shaped: letters, digits, `.`, `_`, `-`, ending `.txt`. +pub fn save_text_in(dir: &std::path::Path, name: &str, text: &str) -> Result { + let ok = name.ends_with(".txt") && name.len() <= 64 && name.chars().all(|c| c.is_ascii_alphanumeric() || "._-".contains(c)) && !name.starts_with('.'); + if !ok { + return Err(format!("{name:?} is not a file name this app writes")); + } + let path = dir.join(name); + use std::io::Write; + let mut f = std::fs::OpenOptions::new().write(true).create_new(true).open(&path).map_err(|e| format!("{}: {e}", path.display()))?; + f.write_all(text.as_bytes()).map_err(|e| e.to_string())?; + Ok(path) +} + +#[tauri::command] +pub async fn save_text_to_downloads(app: tauri::AppHandle, name: String, text: String) -> Result { + use tauri::Manager; + let dir = app.path().download_dir().map_err(|e| e.to_string())?; + save_text_in(&dir, &name, &text).map(|p| p.display().to_string()) +} +``` + + with a test in its `mod tests`: + +```rust + #[test] + fn save_text_in_writes_once_and_refuses_a_path() { + let dir = std::env::temp_dir().join(format!("rand-save-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let p = save_text_in(&dir, "share-1.txt", "1:ab\n").unwrap(); + assert_eq!(std::fs::read_to_string(&p).unwrap(), "1:ab\n"); + assert!(save_text_in(&dir, "share-1.txt", "x").is_err(), "never overwrites"); + assert!(save_text_in(&dir, "../evil.txt", "x").is_err()); + assert!(save_text_in(&dir, "a.sh", "x").is_err()); + std::fs::remove_dir_all(&dir).unwrap(); + } +``` + + register `commands::save_text_to_downloads` in `main.rs`'s `invoke_handler(tauri::generate_handler![...])`; `desktop/ui-shell/backend-tauri.js` platform: `saveText: ({ name, text }) => invoke('save_text_to_downloads', { name, text }),` (use the file's existing `invoke` import name). If the desktop capability file (`desktop/src-tauri/capabilities/*.json`) lists commands, add it there. + - `ui/test/i18n.test.mjs` `SCREENS`: `['signers', () => unlockedBackend()], ['signer/s1', () => { const b = unlockedBackend(); b.signers.add({ label: 'x', password: 'unlocked-password-1' }); return b; }],`. + - `node ui/scripts/extract-strings.mjs`. +- [ ] **Step 4: Run** `node --test ui/test/signers-screens.test.mjs ui/test/settings.test.mjs ui/test/screens.test.mjs` → PASS; `cd desktop/src-tauri && node ../scripts/stage-ui.mjs && cargo test` → PASS. (`i18n.test.mjs` dictionary cases fail until Task 9; its completeness walk for `signers` must already pass in `en`.) +- [ ] **Step 5: Commit** `ui: Settings → Multisig signers — add a profile from files, check/review/sign a proposal (last four + password), save or share the share, check others' shares; desktop saves to Downloads` + trailer. + +--- + +### Task 9: Shared-UI strings in all 20 locales + +**Files:** +- Modify: `ui/locales/{ar,de,es,fa,fr,hi,id,it,ja,ko,ms,pl,ps,pt,ru,ta,ur,zh,zh-hk}.js` (`en.js` is generated) + +- [ ] **Step 1: Failing test.** `node --test ui/test/i18n.test.mjs` → FAIL: `: keys differ from en.js` for all 19 (the keys Tasks 5–8 added). +- [ ] **Step 2: Translate.** List the new keys: `git diff main -- ui/locales/en.js | grep '^+ "'`. Add each to every dictionary, in the file's alphabetical position (the order `extract-strings.mjs` renders), keeping every `{hole}`, the glossary (RAND, rand-node, `index:hex`, `public_key`, `signing_message`, `auth_commit`, `nk`), and the file's register. Use these terms throughout (signer / proposal / share): + ar موقّع / اقتراح / حصة التوقيع · de Unterzeichner / Vorschlag / Signaturanteil · es firmante / propuesta / parte de firma · fa امضاکننده / پیشنهاد / سهم امضا · fr signataire / proposition / part de signature · hi हस्ताक्षरकर्ता / प्रस्ताव / हस्ताक्षर अंश · id penanda tangan / proposal / bagian tanda tangan · it firmatario / proposta / quota di firma · ja 署名者 / 提案 / 署名シェア · ko 서명자 / 제안 / 서명 조각 · ms penandatangan / cadangan / bahagian tandatangan · pl sygnatariusz / propozycja / udział podpisu · ps لاسلیک کوونکی / وړاندیز / د لاسلیک برخه · pt signatário / proposta / parte da assinatura · ru подписант / предложение / доля подписи · ta கையொப்பமிடுபவர் / முன்மொழிவு / கையொப்பப் பங்கு · ur دستخط کنندہ / تجویز / دستخطی حصہ · zh 签名人 / 提案 / 签名份额 · zh-hk 簽署人 / 提案 / 簽署份額. + The fullnode `lines()` text is not a key anywhere (it is shown verbatim). +- [ ] **Step 3: Run** `node --test "ui/test/**/*.test.mjs"` → all PASS (including the pseudo-language completeness walk over `#signers` and `#signer/s1`). +- [ ] **Step 4: Commit** `ui/locales: multisig signing in all 20 languages` + trailer. + +--- + +### Task 10: iOS — core wrappers, the signer store (Keychain, `.userPresence`), the signing rules + +**Files:** +- Create: `ios/RandWallet/Core/Msauth.swift` (models + `MsauthRules`), `ios/RandWallet/Storage/SignerStore.swift`, `ios/RandWalletTests/MsauthTests.swift`, `ios/RandWalletTests/FakeSignerSecrets.swift` +- Modify: `ios/RandWallet/Core/RandCore.swift` (five wrappers), `ios/RandWallet/Core/CoreErrors.swift` (the Task 5 sentences), `ios/project.yml` (fixtures as test resources), `ios/RandWallet/Services/WalletService.swift` (`forgetWallet` also `SignerStore.shared.removeAll()`) + +**Interfaces:** +- Produces: `RandCore.msauthCheck(proposal:policy:expectedRecipients:) -> MsauthSummary`, `.msauthSign(proposal:policy:seed: Data) -> String`, `.msauthVerifyShare(proposal:policy:share:)`, `.msauthSignerImport(_:policy:) -> MsauthSigner`, `.msauthPolicyInfo(_:) -> MsauthPolicyInfo`; `MsauthRules.expiryMargin = 16`, `MsauthRules.verdict(summary:head:chainId:genesis:url:) -> MsauthVerdict` (`.signable(blocksLeft:)`, `.uncheckedRecipient`, `.noNode(url:)`, `.wrongChain(proposal:wallet:)`, `.wrongGenesis`, `.expired(head:expiresAt:)`), `MsauthRules.confirms(fingerprint:typed:) -> Bool`, `MsauthRules.clean(_:)`; `protocol SignerSecrets`; `KeychainSignerSecrets`; `final class SignerStore: ObservableObject` with `profiles: [SignerProfile]`, `add(label:policy:key:) throws -> SignerProfile`, `remove(_ id:)`, `removeAll()`, `policy(_ id:) throws -> String`, `withSeed(_ id:, reason:, _ body: (Data) throws -> T) throws -> T`. + +- [ ] **Step 1: Test resources.** `ios/project.yml` `RandWalletTests.sources` add: + +```yaml + # The private-multisig vectors (fullnode e34117be), the files ui/test/msauth-fixtures.test.mjs pins. + - path: ../ui/test/fixtures/msauth + buildPhase: resources +``` + +Run `cd ios && xcodegen generate`. + +- [ ] **Step 2: Failing tests.** `ios/RandWalletTests/FakeSignerSecrets.swift`: + +```swift +@testable import RandWallet +import Foundation + +/// The Keychain stand-in: records, per item, whether it was stored behind user presence and how +/// often it was read with a prompt. The simulator has no passcode, so `.userPresence` cannot be +/// created there; a device check is in the plan (Task 10 Step 8). +final class FakeSignerSecrets: SignerSecrets { + var items: [String: (data: Data, presence: Bool)] = [:] + var promptedReads: [String] = [] + var canRequireUserPresence = true + func save(_ data: Data, account: String, requireUserPresence: Bool) throws { items[account] = (data, requireUserPresence) } + func load(account: String, prompt: String?) throws -> Data? { + if prompt != nil { promptedReads.append(account) } + return items[account]?.data + } + func delete(account: String) { items[account] = nil } +} +``` + +`ios/RandWalletTests/MsauthTests.swift`: + +```swift +import XCTest +@testable import RandWallet + +final class MsauthTests: XCTestCase { + private func fx(_ name: String) throws -> String { + let url = try XCTUnwrap(Bundle(for: MsauthTests.self).url(forResource: name, withExtension: "json")) + return try String(contentsOf: url, encoding: .utf8) + } + private func json(_ name: String) throws -> [String: Any] { + try XCTUnwrap(JSONSerialization.jsonObject(with: Data(try fx(name).utf8)) as? [String: Any]) + } + + private func payee() throws -> String { + ((try json("proposal-valid")["summary"] as! [String])[0]).split(separator: " ").last.map(String.init)! + } + + func testASubstitutedRecipientIsUnmatchedAndRefused() throws { // [confirm: 0e] fixture name + let s = try RandCore.msauthCheck(proposal: try fx("proposal-substituted-recipient"), policy: try fx("policy"), expectedRecipients: [try payee()]) + XCTAssertFalse(s.recipientsChecked) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 1, chainId: s.chainId, genesis: s.genesis, url: "u"), .uncheckedRecipient) + let none = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy")) + XCTAssertEqual(MsauthRules.verdict(summary: none, head: 1, chainId: 7, genesis: nil, url: "u"), .uncheckedRecipient, "no payee given, nothing signed") + } + + func testTheCoreChecksAndSignsTheVectors() throws { + let s = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy"), expectedRecipients: [try payee()]) + XCTAssertTrue(s.recipientsChecked) + XCTAssertEqual(s.policyFingerprint, "NFYW-K8E9-E5K9-AAK3") + XCTAssertEqual(s.outputs[2].amount, "10000000000") + XCTAssertTrue(s.outputs[3].toAccount) + XCTAssertTrue(s.lines.prefix(4).allSatisfy { $0.contains("owner key verified") }) + let seeds = try json("signer-seeds")["members"] as! [String] + let expected = try json("expected") + let share = try RandCore.msauthSign(proposal: try fx("proposal-valid"), policy: try fx("policy"), seed: Data(hex: seeds[2])!) + XCTAssertEqual(share, expected["share_2"] as? String) + XCTAssertNoThrow(try RandCore.msauthVerifyShare(proposal: try fx("proposal-valid"), policy: try fx("policy"), share: expected["share_0"] as! String)) + XCTAssertThrowsError(try RandCore.msauthCheck(proposal: try fx("proposal-unbound"), policy: try fx("policy"))) { e in + XCTAssertTrue((e as! RandCore.CoreError).message.contains("does not hash to the proposal's binding")) + } + } + + func testTheExpiryAndChainRules() throws { + let s = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy"), expectedRecipients: [try payee()]) // chain 7, domain 0, expires 2024 + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 2007, chainId: 7, genesis: nil, url: "u"), .signable(blocksLeft: 17)) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 2008, chainId: 7, genesis: nil, url: "u"), .expired(head: 2008, expiresAt: 2024)) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: nil, chainId: 7, genesis: nil, url: "http://n"), .noNode(url: "http://n")) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 1, chainId: 21, genesis: nil, url: "u"), .wrongChain(proposal: 7, wallet: 21)) + XCTAssertTrue(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: " aak3")) + XCTAssertFalse(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: "aak")) + } + + func testTheStoreKeepsTheSeedBehindPresenceAndReadsItPerSignature() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let seeds = try json("signer-seeds")["members"] as! [String] + let p = try store.add(label: "treasury", policy: try fx("policy"), key: seeds[0]) + XCTAssertEqual(p.index, 0) + XCTAssertEqual(secrets.items["msauth.\(p.id).seed"]?.presence, true) + XCTAssertEqual(secrets.items["msauth.\(p.id).policy"]?.presence, false) + for _ in 0..<2 { _ = try store.withSeed(p.id, reason: "sign") { $0.count } } + XCTAssertEqual(secrets.promptedReads, ["msauth.\(p.id).seed", "msauth.\(p.id).seed"], "one prompted read per signature, nothing cached") + XCTAssertThrowsError(try store.add(label: "again", policy: try fx("policy"), key: seeds[0])) + store.remove(p.id) + XCTAssertNil(secrets.items["msauth.\(p.id).seed"]) + XCTAssertNil(secrets.items["msauth.\(p.id).policy"]) + } + + func testNoPasscodeNoProfile() throws { + let secrets = FakeSignerSecrets(); secrets.canRequireUserPresence = false + let seeds = try json("signer-seeds")["members"] as! [String] + XCTAssertThrowsError(try SignerStore(secrets: secrets).add(label: "x", policy: try fx("policy"), key: seeds[0])) + } +} +``` + +(If `Data(hex:)` does not exist in the app target, add it in `Msauth.swift` as `extension Data { init?(hex: String) }` and a `hexString` property.) Run the iOS test command (Global Constraints) `-only-testing:RandWalletTests/MsauthTests` → FAIL (types missing). + +- [ ] **Step 3: `Msauth.swift`.** + +```swift +import Foundation + +/// `msauth_check`'s reply (wallet-core msauth.rs): amounts are decimal strings; `lines` is +/// fullnode's text, shown verbatim, never translated. +struct MsauthOutput: Decodable, Equatable { + let ownerFingerprint: String; let toAccount: Bool; let amount: String; let asset: UInt32; let memo: String + let matches: String?; let needsPayee: Bool // [confirm: 0e] `matches` + enum CodingKeys: String, CodingKey { case ownerFingerprint = "owner_fingerprint", toAccount = "to_account", amount, asset, memo, matches, needsPayee = "needs_payee" } +} +struct MsauthBurns: Decodable, Equatable { + let burnA: String; let burnAsset: UInt32; let burnR: String + enum CodingKeys: String, CodingKey { case burnA = "burn_a", burnAsset = "burn_asset", burnR = "burn_r" } +} +struct MsauthSummary: Decodable, Equatable { + let outputs: [MsauthOutput]; let fee: String; let burns: MsauthBurns; let expiresAt: UInt64 + let signingMessage: String; let policyFingerprint: String; let lines: [String] + let chainId: UInt64; let genesis: String; let bindingDomain: UInt32; let recipientsChecked: Bool + enum CodingKeys: String, CodingKey { + case outputs, fee, burns, lines, genesis + case recipientsChecked = "recipients_checked" + case expiresAt = "expires_at", signingMessage = "signing_message", policyFingerprint = "policy_fingerprint" + case chainId = "chain_id", bindingDomain = "binding_domain" + } +} +struct MsauthSigner: Decodable { let seed: String; let publicKey: String; let address: String; let index: Int? + enum CodingKeys: String, CodingKey { case seed, address, index, publicKey = "public_key" } } +struct MsauthPolicyInfo: Decodable { let fingerprint: String; let threshold: Int; let signers: Int; let address: String; let policyHash: String + enum CodingKeys: String, CodingKey { case fingerprint, threshold, signers, address, policyHash = "policy_hash" } } + +enum MsauthVerdict: Equatable { + case signable(blocksLeft: UInt64), uncheckedRecipient, noNode(url: String), wrongChain(proposal: UInt64, wallet: UInt64), wrongGenesis, expired(head: UInt64, expiresAt: UInt64) +} + +/// The signing rules every app shares (spec §4): the CLI's 16-block margin, no node no signature, +/// the proposal's chain and genesis against this wallet's node, the fingerprint's last four. +enum MsauthRules { + static let expiryMargin: UInt64 = 16 + static func clean(_ s: String) -> String { + var t = s; if t.hasPrefix("\u{FEFF}") { t.removeFirst() } + return t.trimmingCharacters(in: .whitespacesAndNewlines) + } + static func verdict(summary s: MsauthSummary, head: UInt64?, chainId: UInt64, genesis: String?, url: String) -> MsauthVerdict { + guard s.recipientsChecked else { return .uncheckedRecipient } + guard let head else { return .noNode(url: url) } + if s.chainId != chainId { return .wrongChain(proposal: s.chainId, wallet: chainId) } + if s.bindingDomain >= 1 && s.genesis.lowercased() != (genesis ?? "").lowercased() { return .wrongGenesis } + if head + expiryMargin >= s.expiresAt { return .expired(head: head, expiresAt: s.expiresAt) } + return .signable(blocksLeft: s.expiresAt - head) + } + static func confirms(fingerprint: String, typed: String) -> Bool { + let want = String(fingerprint.replacingOccurrences(of: "-", with: "").suffix(4)).lowercased() + let got = typed.replacingOccurrences(of: "-", with: "").trimmingCharacters(in: .whitespaces).lowercased() + return want.count == 4 && got == want + } + /// Head, chain id and genesis from `rpc`; any failure is `.noNode`. + static func check(proposal: String, policy: String, payees: [String], rpc: RpcClient, chainId: UInt64) async throws -> (MsauthSummary, MsauthVerdict) { + let s = try RandCore.msauthCheck(proposal: clean(proposal), policy: policy, expectedRecipients: payees.map(clean).filter { !$0.isEmpty }) + do { + let node = try await rpc.chainId() + guard node == chainId else { return (s, .noNode(url: rpc.url.absoluteString)) } + let head = try await rpc.headHeight() + let genesis = try await rpc.genesisHash() + return (s, verdict(summary: s, head: head, chainId: chainId, genesis: genesis, url: rpc.url.absoluteString)) + } catch { + return (s, .noNode(url: rpc.url.absoluteString)) + } + } +} +``` + +`RandCore.swift` (MARK: private multisig signing): + +```swift + static func msauthCheck(proposal: String, policy: String, expectedRecipients: [String] = []) throws -> MsauthSummary { + try call("msauth_check", ["proposal": proposal, "policy": policy, "expected_recipients": expectedRecipients], as: MsauthSummary.self) + } + /// `seed` is the caller's; it zeroes its own buffer after (SignerStore.withSeed does). + static func msauthSign(proposal: String, policy: String, seed: Data) throws -> String { + guard let s = try call("msauth_sign", ["proposal": proposal, "policy": policy, "seed": seed.hexString]) as? String else { + throw CoreError(message: "msauth_sign returned no share") + } + return s + } + static func msauthVerifyShare(proposal: String, policy: String, share: String) throws { + _ = try call("msauth_verify_share", ["proposal": proposal, "policy": policy, "share": share]) + } + static func msauthSignerImport(_ input: String, policy: String?) throws -> MsauthSigner { + var p: [String: Any] = ["input": input]; if let policy { p["policy"] = policy } + return try call("msauth_signer_import", p, as: MsauthSigner.self) + } + static func msauthPolicyInfo(_ policy: String) throws -> MsauthPolicyInfo { + try call("msauth_policy_info", ["policy": policy], as: MsauthPolicyInfo.self) + } +``` + +- [ ] **Step 4: `SignerStore.swift`.** + +```swift +import Foundation +import Security +import LocalAuthentication + +protocol SignerSecrets { + func save(_ data: Data, account: String, requireUserPresence: Bool) throws + /// `prompt` non-nil: the item is behind user presence and the system asks now. + func load(account: String, prompt: String?) throws -> Data? + func delete(account: String) + var canRequireUserPresence: Bool { get } +} + +/// Keychain items beside the spend key's (`WhenUnlockedThisDeviceOnly`); the seed's item also +/// carries `.userPresence`, so each read asks for Face ID / Touch ID / the passcode. +final class KeychainSignerSecrets: SignerSecrets { + private let service = "org.randprotocol.wallet" + var canRequireUserPresence: Bool { LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) } + func save(_ data: Data, account: String, requireUserPresence: Bool) throws { + let base: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account] + SecItemDelete(base as CFDictionary) + var add = base + add[kSecValueData as String] = data + if requireUserPresence { + guard let ac = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil) else { throw Keychain.KeychainError(status: errSecParam) } + add[kSecAttrAccessControl as String] = ac + } else { + add[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly + } + let status = SecItemAdd(add as CFDictionary, nil) + guard status == errSecSuccess else { throw Keychain.KeychainError(status: status) } + } + func load(account: String, prompt: String?) throws -> Data? { + var q: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, + kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne] + if let prompt { + let ctx = LAContext() // fresh per read: no reuse window between signatures + ctx.localizedReason = prompt + ctx.touchIDAuthenticationAllowableReuseDuration = 0 + q[kSecUseAuthenticationContext as String] = ctx + } + var item: CFTypeRef? + let status = SecItemCopyMatching(q as CFDictionary, &item) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess else { throw Keychain.KeychainError(status: status) } + return item as? Data + } + func delete(account: String) { + SecItemDelete([kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account] as CFDictionary) + } +} + +struct SignerProfile: Codable, Identifiable, Equatable { + let id: String; var label: String; let index: Int; let fingerprint: String + let threshold: Int; let signers: Int; let address: String; let policyHash: String +} + +/// Signer profiles (spec §3): `msauth.index` (the list), `msauth..policy`, `msauth..seed` +/// (behind user presence). The seed is read per signature and zeroed after; never cached. +final class SignerStore: ObservableObject { + static let shared = SignerStore(secrets: KeychainSignerSecrets()) + @Published private(set) var profiles: [SignerProfile] = [] + private let secrets: SignerSecrets + init(secrets: SignerSecrets) { + self.secrets = secrets + if let d = try? secrets.load(account: "msauth.index", prompt: nil), let list = try? JSONDecoder().decode([SignerProfile].self, from: d) { profiles = list } + } + struct StoreError: LocalizedError { let errorDescription: String? } + + func add(label: String, policy: String, key: String) throws -> SignerProfile { + guard secrets.canRequireUserPresence else { + throw StoreError(errorDescription: String(localized: "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.")) + } + let name = label.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty, name.unicodeScalars.count <= 64 else { throw StoreError(errorDescription: String(localized: "a signer label is 1-64 characters")) } + let pol = MsauthRules.clean(policy) + let info = try RandCore.msauthPolicyInfo(pol) + let signer = try RandCore.msauthSignerImport(MsauthRules.clean(key), policy: pol) + guard let index = signer.index else { throw StoreError(errorDescription: "this key is not one of the policy's signers") } + if let same = profiles.first(where: { $0.policyHash == info.policyHash && $0.index == index }) { + throw StoreError(errorDescription: String(localized: "this signer is already saved as \(same.label)")) + } + let id = UUID().uuidString.lowercased() + guard var seed = Data(hex: signer.seed) else { throw StoreError(errorDescription: "a signer seed is 64 hex characters") } + defer { seed.resetBytes(in: 0.. String { + guard let d = try secrets.load(account: "msauth.\(id).policy", prompt: nil) else { throw StoreError(errorDescription: String(localized: "No such signer profile.")) } + return String(decoding: d, as: UTF8.self) + } + /// One prompted read of the seed for `body`; the buffer is zeroed when `body` returns. + func withSeed(_ id: String, reason: String, _ body: (Data) throws -> T) throws -> T { + guard var seed = try secrets.load(account: "msauth.\(id).seed", prompt: reason) else { + throw StoreError(errorDescription: String(localized: "This signer profile has no key on this device: remove it and add it again.")) + } + defer { seed.resetBytes(in: 0.. Bool { + if case .signable = verdict { return MsauthRules.confirms(fingerprint: summary.policyFingerprint, typed: typed) } + return false + } + var refusal: String? { + switch verdict { + case .signable: return nil + case .uncheckedRecipient: return String(localized: "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.") + case .noNode(let url): return String(localized: "No node answered at \(url), so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.") + case .wrongChain(let p, let w): return String(localized: "This proposal is for chain \(String(p)); this wallet is on chain \(String(w)).") + case .wrongGenesis: return String(localized: "This proposal is bound to another genesis than the one this wallet's node serves.") + case .expired(let h, let e): return String(localized: "This proposal expires at height \(String(e)) and the chain is at \(String(h)): too late to sign. Ask for a new proposal.") + } + } +} + +/// Paste or open proposal.json, review what the core checked, type the last four, Face ID, share. +/// The proposal is @State of this view only and is dropped with it. +struct SignProposalView: View { + let profile: SignerProfile + @EnvironmentObject var store: SignerStore + @EnvironmentObject var settings: Settings + @State private var proposal = "" + @State private var model: SignProposalModel? + @State private var typed = "" + @State private var share: String? + @State private var error: String? + @State private var importing = false + @State private var checking = false + @State private var shareToVerify = "" + @State private var payees: [String] = [] // one per output with needsPayee, in order + @State private var inHand = Set() + + var body: some View { + Form { + Section(profile.label) { + Text(profile.fingerprint).font(.mono) + Text("\(profile.threshold) of \(profile.signers) · signer \(profile.index)") + } + Section("Proposal") { + TextEditor(text: $proposal).font(.mono).frame(minHeight: 80).autocorrectionDisabled().textInputAutocapitalization(.never) + Button("Open file") { importing = true } + Button(checking ? "Checking…" : "Check proposal") { Task { await check() } }.disabled(checking || proposal.isEmpty) + if let error { Text(error).foregroundColor(.red) } + } + if let m = model { + Section("What this proposal does, as checked against your policy") { + ForEach(Array(m.lines.enumerated()), id: \.offset) { Text(verbatim: $0.element).font(.mono) } + } + Section("Enter the address you mean to pay for each payment below. An output that does not match it cannot be signed.") { + let paying = m.summary.outputs.filter(\.needsPayee) + ForEach(paying.indices, id: \.self) { i in + TextField("rand1…", text: Binding(get: { i < payees.count ? payees[i] : "" }, + set: { v in while payees.count <= i { payees.append("") }; payees[i] = v })) + .font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + } + Button("Check payees") { Task { await check() } } + } + if let r = m.refusal { + Section { Text(r).foregroundColor(.red); Button("Retry") { Task { await check() } } } + } else { + Section("Type the last four characters of the account fingerprint") { + TextField("", text: $typed).font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + Button("Sign") { sign(m) }.disabled(!m.canSign(typed: typed)) + } + } + } + if let share { + Section("Your share — send it to whoever proves the proposal") { + Text(verbatim: share).font(.mono).textSelection(.enabled).lineLimit(3) + Button("Copy") { UIPasteboard.general.string = share } + ShareLink(item: share) + } + } + Section("Check a share") { + TextField("index:hex", text: $shareToVerify).font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + Button("Check share") { verifyShare() } + if !inHand.isEmpty { Text("\(inHand.count) of \(profile.threshold) in hand: signers \(inHand.sorted().map(String.init).joined(separator: ", "))") } + } + } + .navigationTitle("Sign a proposal") + .fileImporter(isPresented: $importing, allowedContentTypes: [.json, .plainText]) { result in + if case .success(let url) = result, url.startAccessingSecurityScopedResource() { + defer { url.stopAccessingSecurityScopedResource() } + if let d = try? Data(contentsOf: url), d.count <= 1 << 20 { proposal = String(decoding: d, as: UTF8.self) } + else { error = String(localized: "That file is larger than a proposal, a policy or a key file can be.") } + } + } + .onDisappear { proposal = ""; model = nil } + } + + private func check() async { + checking = true; defer { checking = false } + error = nil; model = nil; share = nil + do { + let policy = try store.policy(profile.id) + guard let url = settings.rpcURL else { throw RpcClient.RpcError(code: 0, message: String(localized: "Set a valid RPC URL in Settings")) } + let (s, v) = try await MsauthRules.check(proposal: proposal, policy: policy, payees: payees, rpc: RpcClient(url: url), chainId: UInt64(settings.chainId)) + model = SignProposalModel(summary: s, verdict: v) + } catch { self.error = error.localizedDescription } + } + private func sign(_ m: SignProposalModel) { + Task { + await check() // a fresh head right before signing + guard let fresh = model, fresh.canSign(typed: typed) else { return } + do { + let policy = try store.policy(profile.id) + let text = MsauthRules.clean(proposal) + share = try store.withSeed(profile.id, reason: String(localized: "Sign a multisig proposal")) { seed in + try RandCore.msauthSign(proposal: text, policy: policy, seed: seed) + } + } catch { self.error = error.localizedDescription } + } + } + private func verifyShare() { + do { + let s = MsauthRules.clean(shareToVerify) + try RandCore.msauthVerifyShare(proposal: MsauthRules.clean(proposal), policy: try store.policy(profile.id), share: s) + if let i = Int(s.split(separator: ":").first ?? "") { inHand.insert(i) } + error = nil + } catch { self.error = error.localizedDescription } + } +} +``` + +`SignersView.swift`: a `List` of `store.profiles` (label, fingerprint, "T of N · signer i") with `NavigationLink { SignProposalView(profile:) }` and swipe-to-delete (asks `Keychain.authenticate(reason:)` first, then `store.remove`); an "Add a signer" sheet with a label `TextField`, two `TextEditor`s (policy, key) each with an "Open file" `.fileImporter`, a "Check" button that calls `RandCore.msauthPolicyInfo` + `msauthSignerImport(_:policy:)` and shows fingerprint (large mono), address, "T of N · this key is signer i", the compare-out-of-band sentence, then "Save signer" → `store.add` (errors shown by `localizedDescription`). `SettingsView`: `Section { NavigationLink("Manage multisig signers") { SignersView() } } header: { Text("Multisig signers") }`. `RandWalletApp`: `.environmentObject(SignerStore.shared)`. + +- [ ] **Step 3: Strings.** Build once (`xcodebuild … build`), confirm `git diff ios/RandWallet/Localizable.xcstrings` shows the new keys (add `"key" : { }` entries by hand for any missing), add every new key to each `ios/l10n/.json` using Task 9's term table, then `python3 scripts/i18n/native_check.py ios ` for each of `ar de es fa fr hi id it ja ko ms pl ps pt-BR ru ta ur zh-Hans zh-Hant-HK` → `0 problem(s)`, then `python3 scripts/i18n/native_check.py ios-merge`. +- [ ] **Step 4: Run** the iOS test command → all PASS. +- [ ] **Step 5: Commit** `ios: Settings → Multisig signers — add from files, check, review the core's lines, last four + Face ID, ShareLink; 20 languages` + trailer. + +--- + +### Task 12: Android — core wrappers, signer vault (Keystore per-use key), signing rules + +**Files:** +- Create: `android/app/src/main/java/org/randprotocol/wallet/wallet/MsauthCore.java`, `.../wallet/NativeMsauthCore.java`, `.../wallet/SignFlow.java`, `.../security/SignerProfiles.java`, `.../security/KeystoreSeedCipher.java`, `android/app/src/test/java/org/randprotocol/wallet/wallet/SignFlowTest.java`, `android/app/src/test/java/org/randprotocol/wallet/security/SignerProfilesTest.java` +- Modify: `.../core/Core.java` (five wrappers), `.../security/KeyVault.java` (`KeyValue` adapter; `erase()` also removes profiles and Keystore keys), `.../ui/CoreErrors.java` (Task 5 sentences), `android/app/build.gradle` (test resources) + +**Interfaces:** +- Produces: `Core.msauthCheck(String proposal, String policy, List expectedRecipients) -> JSONObject`, `Core.msauthSign(String proposal, String policy, String seedHex) -> String`, `Core.msauthVerifyShare(...)`, `Core.msauthSignerImport(String input, String policy) -> JSONObject`, `Core.msauthPolicyInfo(String policy) -> JSONObject`; `interface MsauthCore` (same five, `throws Exception`); `SignFlow.EXPIRY_MARGIN = 16`, `enum SignFlow.Verdict { SIGNABLE, UNCHECKED_RECIPIENT, NO_NODE, WRONG_CHAIN, WRONG_GENESIS, EXPIRED }`, `SignFlow.verdict(JSONObject summary, Long head, long chainId, String genesis)`, `SignFlow.confirms(String fingerprint, String typed)`, `SignFlow.clean(String)`, `SignFlow.lines(JSONObject summary) -> List`; `interface SignerProfiles.KeyValue { String get(String); void put(String, String); void remove(String); }`; `SignerProfiles(KeyValue)` with `list() -> List`, `add(Profile, String policy, String sealedSeed)`, `remove(String id)`, `policy(String id)`, `sealedSeed(String id)`, `Profile {id, label, index, policyHash}`; `KeystoreSeedCipher.createKey(id)`, `.encryptCipher(id)`, `.decryptCipher(id, byte[] iv)`, `.deleteKey(id)`, `seal(byte[] iv, byte[] ct) -> "ivB64:ctB64"`, `unseal(String) -> byte[][]`. + +- [ ] **Step 1: Test resources.** `android/app/build.gradle`, inside `android { … }`: + +```groovy + // The private-multisig vectors (fullnode e34117be), held to their SHA-256s by ui/test/msauth-fixtures.test.mjs. + sourceSets { test { resources.srcDirs += ['../../ui/test/fixtures/msauth'] } } +``` + +- [ ] **Step 2: Failing tests.** `SignFlowTest.java`: + +```java +package org.randprotocol.wallet.wallet; + +import static org.junit.Assert.*; + +import org.json.JSONObject; +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class SignFlowTest { + private static JSONObject expectedSummary() throws Exception { + try (InputStream in = SignFlowTest.class.getResourceAsStream("/expected.json")) { + JSONObject s = new JSONObject(new String(in.readAllBytes(), StandardCharsets.UTF_8)).getJSONObject("summary"); + // msauth_check adds these beside fullnode's summary (wallet-core msauth.rs). + return s.put("recipients_checked", true).put("chain_id", 7).put("genesis", "0".repeat(64)).put("binding_domain", 0) + .put("lines", new org.json.JSONArray().put("0: x (owner key verified; delivery key NOT verifiable from this proposal)")); + } + } + + @Test public void theExpiryRuleIsTheClis() throws Exception { + JSONObject s = expectedSummary(); // expires_at 2024 + assertEquals(16, SignFlow.EXPIRY_MARGIN); + assertEquals(SignFlow.Verdict.SIGNABLE, SignFlow.verdict(s, 2007L, 7, null)); + assertEquals(SignFlow.Verdict.EXPIRED, SignFlow.verdict(s, 2008L, 7, null)); + assertEquals(SignFlow.Verdict.NO_NODE, SignFlow.verdict(s, null, 7, null)); + assertEquals(SignFlow.Verdict.WRONG_CHAIN, SignFlow.verdict(s, 1L, 21, null)); + s.put("binding_domain", 1); + assertEquals(SignFlow.Verdict.WRONG_GENESIS, SignFlow.verdict(s, 1L, 7, "ab".repeat(32))); + s.put("recipients_checked", false); + assertEquals("an unchecked payee refuses before anything else", SignFlow.Verdict.UNCHECKED_RECIPIENT, SignFlow.verdict(s, null, 7, null)); + } + + @Test public void confirmationIsTheLastFour() { + assertTrue(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", " aak3")); + assertFalse(SignFlow.confirms("NFYW-K8E9-E5K9-AAK3", "aak")); + assertFalse(SignFlow.confirms("", "")); + } + + @Test public void linesAreTheCoresVerbatimAndFilesAreCleaned() throws Exception { + assertEquals("0: x (owner key verified; delivery key NOT verifiable from this proposal)", SignFlow.lines(expectedSummary()).get(0)); + assertEquals("{}", SignFlow.clean(" {}\r\n")); + } +} +``` + +`SignerProfilesTest.java`: + +```java +package org.randprotocol.wallet.security; + +import static org.junit.Assert.*; + +import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + +public class SignerProfilesTest { + static final class MapKv implements SignerProfiles.KeyValue { + final Map m = new HashMap<>(); + public String get(String k) { return m.get(k); } + public void put(String k, String v) { m.put(k, v); } + public void remove(String k) { m.remove(k); } + } + + @Test public void addListRemoveKeepsBothHalvesUnderTheirNames() throws Exception { + MapKv kv = new MapKv(); + SignerProfiles p = new SignerProfiles(kv); + p.add(new SignerProfiles.Profile("a1", "treasury", 2, "33".repeat(32)), "{policy}", "iv:ct"); + assertEquals(1, p.list().size()); + assertEquals("{policy}", kv.get("msauth.a1.policy")); + assertEquals("iv:ct", kv.get("msauth.a1.seed")); + try { p.add(new SignerProfiles.Profile("b2", "again", 2, "33".repeat(32)), "{policy}", "x:y"); fail("duplicate"); } + catch (IllegalStateException e) { assertTrue(e.getMessage().contains("treasury")); } + p.remove("a1"); + assertNull(kv.get("msauth.a1.policy")); + assertNull(kv.get("msauth.a1.seed")); + assertTrue(p.list().isEmpty()); + } + + @Test public void aSealedSeedRoundTrips() { + byte[][] parts = KeystoreSeedCipher.unseal(KeystoreSeedCipher.seal(new byte[] {1, 2}, new byte[] {3, 4, 5})); + assertArrayEquals(new byte[] {1, 2}, parts[0]); + assertArrayEquals(new byte[] {3, 4, 5}, parts[1]); + } +} +``` + +Run the Android test command → FAIL (classes missing). + +- [ ] **Step 3: Implement.** `SignFlow.java`: + +```java +package org.randprotocol.wallet.wallet; + +import org.json.JSONArray; +import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** The signing rules every app shares (spec 2026-10-11-msauth-signing-design.md §4). Pure: JVM-tested. */ +public final class SignFlow { + private SignFlow() {} + /** The CLI's margin (fullnode d5b2e652 `EXPIRY_MARGIN`): refuse at head + 16 >= expires_at. */ + public static final long EXPIRY_MARGIN = 16; + public enum Verdict { SIGNABLE, UNCHECKED_RECIPIENT, NO_NODE, WRONG_CHAIN, WRONG_GENESIS, EXPIRED } + + public static Verdict verdict(JSONObject s, Long head, long chainId, String genesis) { + // Spec §4 step 3: never sign on an unmatched or unchecked payee. + if (!s.optBoolean("recipients_checked", false)) return Verdict.UNCHECKED_RECIPIENT; + if (head == null) return Verdict.NO_NODE; + if (s.optLong("chain_id", -1) != chainId) return Verdict.WRONG_CHAIN; + if (s.optInt("binding_domain", 0) >= 1 && !s.optString("genesis", "").equalsIgnoreCase(genesis == null ? "" : genesis)) return Verdict.WRONG_GENESIS; + if (head + EXPIRY_MARGIN >= s.optLong("expires_at", 0)) return Verdict.EXPIRED; + return Verdict.SIGNABLE; + } + public static boolean confirms(String fingerprint, String typed) { + String fp = fingerprint == null ? "" : fingerprint.replace("-", ""); + if (fp.length() < 4) return false; + String want = fp.substring(fp.length() - 4).toLowerCase(Locale.ROOT); + return typed != null && typed.replace("-", "").trim().toLowerCase(Locale.ROOT).equals(want); + } + public static String clean(String s) { + String t = s == null ? "" : s; + if (t.startsWith("")) t = t.substring(1); + return t.trim(); + } + /** fullnode's lines, verbatim — never translated. */ + public static List lines(JSONObject s) { + List out = new ArrayList<>(); + JSONArray a = s.optJSONArray("lines"); + for (int i = 0; a != null && i < a.length(); i++) out.add(a.optString(i)); + return out; + } +} +``` + +`SignerProfiles.java`: + +```java +package org.randprotocol.wallet.security; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; + +/** + * Signer profiles beside the spend key (spec §3): {@code msauth.index}, {@code msauth..policy}, + * {@code msauth..seed} (the seed sealed by {@link KeystoreSeedCipher}, "ivB64:ctB64"). + */ +public final class SignerProfiles { + public interface KeyValue { String get(String k); void put(String k, String v); void remove(String k); } + public static final class Profile { + public final String id, label, policyHash; public final int index; + public Profile(String id, String label, int index, String policyHash) { this.id = id; this.label = label; this.index = index; this.policyHash = policyHash; } + JSONObject toJson() throws JSONException { return new JSONObject().put("id", id).put("label", label).put("index", index).put("policyHash", policyHash); } + static Profile of(JSONObject o) { return new Profile(o.optString("id"), o.optString("label"), o.optInt("index"), o.optString("policyHash")); } + } + private final KeyValue kv; + public SignerProfiles(KeyValue kv) { this.kv = kv; } + + public List list() { + List out = new ArrayList<>(); + try { + JSONArray a = new JSONArray(kv.get("msauth.index") == null ? "[]" : kv.get("msauth.index")); + for (int i = 0; i < a.length(); i++) out.add(Profile.of(a.getJSONObject(i))); + } catch (JSONException e) { /* an unreadable index is an empty one */ } + return out; + } + public void add(Profile p, String policy, String sealedSeed) throws JSONException { + List all = list(); + for (Profile q : all) if (q.policyHash.equals(p.policyHash) && q.index == p.index) throw new IllegalStateException("this signer is already saved as " + q.label); + kv.put("msauth." + p.id + ".policy", policy); + kv.put("msauth." + p.id + ".seed", sealedSeed); + all.add(p); + writeIndex(all); + } + public void remove(String id) { + kv.remove("msauth." + id + ".policy"); + kv.remove("msauth." + id + ".seed"); + List all = list(); + all.removeIf(p -> p.id.equals(id)); + try { writeIndex(all); } catch (JSONException e) { throw new IllegalStateException(e); } + } + public String policy(String id) { return kv.get("msauth." + id + ".policy"); } + public String sealedSeed(String id) { return kv.get("msauth." + id + ".seed"); } + private void writeIndex(List all) throws JSONException { + JSONArray a = new JSONArray(); + for (Profile p : all) a.put(p.toJson()); + kv.put("msauth.index", a.toString()); + } +} +``` + +`KeystoreSeedCipher.java`: + +```java +package org.randprotocol.wallet.security; + +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * One Keystore AES-256-GCM key per signer profile, usable only right after the user authenticates + * (spec §3: every signature re-authenticates). API 30+: biometric or device credential, per use + * (timeout 0). API 26–29: per-use auth exists only for biometrics (validity -1), so on those + * devices a profile needs an enrolled biometric. The cipher goes through BiometricPrompt's + * CryptoObject; nothing here prompts. + */ +public final class KeystoreSeedCipher { + private KeystoreSeedCipher() {} + private static final String STORE = "AndroidKeyStore"; + static String alias(String id) { return "msauth." + id; } + + public static void createKey(String id) throws GeneralSecurityException { + KeyGenParameterSpec.Builder b = new KeyGenParameterSpec.Builder(alias(id), KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256).setUserAuthenticationRequired(true); + if (Build.VERSION.SDK_INT >= 30) b.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL); + else b.setUserAuthenticationValidityDurationSeconds(-1); + KeyGenerator g = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE); + g.init(b.build()); + g.generateKey(); + } + private static SecretKey key(String id) throws GeneralSecurityException { + KeyStore ks = KeyStore.getInstance(STORE); + try { ks.load(null); } catch (java.io.IOException e) { throw new GeneralSecurityException(e); } + return (SecretKey) ks.getKey(alias(id), null); + } + public static Cipher encryptCipher(String id) throws GeneralSecurityException { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.ENCRYPT_MODE, key(id)); + return c; + } + public static Cipher decryptCipher(String id, byte[] iv) throws GeneralSecurityException { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.DECRYPT_MODE, key(id), new GCMParameterSpec(128, iv)); + return c; + } + public static void deleteKey(String id) { + try { KeyStore ks = KeyStore.getInstance(STORE); ks.load(null); ks.deleteEntry(alias(id)); } catch (Exception ignored) { } + } + public static String seal(byte[] iv, byte[] ct) { return Base64.getEncoder().encodeToString(iv) + ":" + Base64.getEncoder().encodeToString(ct); } + public static byte[][] unseal(String s) { + String[] p = s.split(":", 2); + return new byte[][] { Base64.getDecoder().decode(p[0]), Base64.getDecoder().decode(p[1]) }; + } +} +``` + +`MsauthCore.java` (interface, the five methods returning `JSONObject`/`String`/`void`, `throws Exception`), `NativeMsauthCore.java` (delegates to `Core`), `Core.java`: + +```java + // ---- private multisig signing (wallet-core msauth.rs) ---- + public static JSONObject msauthCheck(String proposal, String policy, java.util.List expectedRecipients) throws CoreException { + return object("msauth_check", p("proposal", proposal, "policy", policy, "expected_recipients", new org.json.JSONArray(expectedRecipients))); + } + /** {@code seedHex} is the caller's; it wipes its byte[] after (the String cannot be wiped). */ + public static String msauthSign(String proposal, String policy, String seedHex) throws CoreException { + Object v = call("msauth_sign", p("proposal", proposal, "policy", policy, "seed", seedHex)); + if (v instanceof String) return (String) v; + throw new CoreException("msauth_sign returned no share"); + } + public static void msauthVerifyShare(String proposal, String policy, String share) throws CoreException { call("msauth_verify_share", p("proposal", proposal, "policy", policy, "share", share)); } + public static JSONObject msauthSignerImport(String input, String policy) throws CoreException { + return object("msauth_signer_import", policy == null ? p("input", input) : p("input", input, "policy", policy)); + } + public static JSONObject msauthPolicyInfo(String policy) throws CoreException { return object("msauth_policy_info", p("policy", policy)); } +``` + +`KeyVault.java`: add + +```java + /** The signer profiles' storage, in this same encrypted file (spec §3). */ + public SignerProfiles.KeyValue signerStore() { + return new SignerProfiles.KeyValue() { + public String get(String k) { return prefs.getString(k, null); } + public void put(String k, String v) { prefs.edit().putString(k, v).commit(); } + public void remove(String k) { prefs.edit().remove(k).commit(); } + }; + } +``` + +and in `erase()`, before the commit: `for (SignerProfiles.Profile p : new SignerProfiles(signerStore()).list()) { KeystoreSeedCipher.deleteKey(p.id); new SignerProfiles(signerStore()).remove(p.id); } prefs.edit().remove("msauth.index").commit();`. `CoreErrors.java`: Task 5's sentences (EXACT + PATTERNED, the file's pattern; new `R.string.msauth_*` ids added in Task 13's strings — add them to `values/strings.xml` now so this compiles). + +- [ ] **Step 4: Run** the Android test command → PASS (`SignFlowTest`, `SignerProfilesTest`, `CoreErrorsTest`, `StringsParityTest`). +- [ ] **Step 5: Commit** `android: msauth core wrappers, SignFlow rules, signer profiles in KeyVault, a per-use Keystore key per seed` + trailer. +- [ ] **Step 9 (manual, later with Task 13): device check** — API 30+ device with a screen lock: add a profile (prompt), sign twice (prompt each time); an API 26–29 emulator without biometrics: adding a profile is refused with the "needs a fingerprint" sentence. + +--- + +### Task 13: Android — screens and strings + +**Files:** +- Create: `.../ui/SignersActivity.java`, `.../ui/SignProposalActivity.java`, `android/app/src/main/res/layout/activity_signers.xml`, `android/app/src/main/res/layout/activity_sign_proposal.xml` +- Modify: `AndroidManifest.xml` (two activities, `exported=false`), `.../ui/SettingsActivity.java` + `activity_settings.xml` (a "Manage multisig signers" button), `res/values/strings.xml` and the 19 `res/values-*/strings.xml` + +**Interfaces:** +- Consumes: Task 12's `SignFlow`, `SignerProfiles`, `KeystoreSeedCipher`, `Core.msauth*`, `RpcClient(prefs.rpcUrl())` (`chainId()`, `headHeight()`, `genesisHash()`), `Unlock.authenticators()`. + +- [ ] **Step 1: Failing test** — extend `StringsParityTest` coverage automatically: add the new `L10n.t(R.string.msauth_*, "…")` calls in the activities first; `StringsParityTest` then fails until `values/strings.xml` holds them. Run the Android test command → FAIL on the missing resources. +- [ ] **Step 2: Activities.** + - `SignersActivity`: list of `SignerProfiles.list()` rows (label, fingerprint from `Core.msauthPolicyInfo(policy)`, "T of N · signer i"); "Add a signer": label `EditText`, two `EditText`s with "Open file" buttons launching `registerForActivityResult(new ActivityResultContracts.OpenDocument(), …)` with `new String[]{"application/json", "text/plain"}` (refuse > 1 MiB), "Check" → `Core.msauthPolicyInfo` + `Core.msauthSignerImport(key, policy)` → shows fingerprint/address/position/compare sentence → "Save signer": `KeystoreSeedCipher.createKey(id)`, `BiometricPrompt` with `CryptoObject(encryptCipher(id))` (authenticators: `Unlock.authenticators()` on API 30+, `BIOMETRIC_STRONG` + negative button below), on success `doFinal(seedBytes)` → `SignerProfiles.add(profile, policy, KeystoreSeedCipher.seal(cipher.getIV(), ct))`; `Arrays.fill(seedBytes, (byte) 0)` in `finally`. If `BiometricManager.canAuthenticate(...)` is not `BIOMETRIC_SUCCESS`, refuse with `msauth_needs_device_auth` ("Set a screen lock (and on Android 10 or older, a fingerprint) first: a signer key is used only right after you authenticate."). + - `SignProposalActivity` (extra `id`): proposal `EditText` + Open file; "Check proposal" on a background thread: `Core.msauthCheck(SignFlow.clean(text), policy, payees)` (first with no payees; then one `EditText` per output with `needs_payee`, labelled with its amount and owner key, and "Check payees" re-runs the check with the typed addresses; a new proposal clears them; UNCHECKED_RECIPIENT shows "Enter the address of every payee…" and no Sign), then `rpc.chainId()`, `rpc.headHeight()`, `rpc.genesisHash()` (any exception → head `null`), `SignFlow.verdict(...)`; shows `SignFlow.lines(summary)` verbatim (monospace `TextView`s), fee/expiry, the out-of-band sentence; a refusal shows its sentence (NO_NODE names `prefs.rpcUrl()`) with Retry; SIGNABLE shows the last-four `EditText` gating "Sign". Sign: re-run the check, then `BiometricPrompt` with `CryptoObject(decryptCipher(id, iv))` → `doFinal(ct)` → `Core.msauthSign(text, policy, hex(seed))` → `Arrays.fill(seed, 0)` → share shown with Copy and Share (`Intent.ACTION_SEND`, `text/plain`, `Intent.createChooser`). "Check a share": `Core.msauthVerifyShare` → positions in hand vs T. The proposal is a field of the activity only, cleared in `onDestroy`; `FLAG_SECURE` on both windows. + - Settings: button → `startActivity(new Intent(this, SignersActivity.class))`. +- [ ] **Step 3: Strings.** Add every `msauth_*` string to `values/strings.xml` (the English the `L10n.t` literal carries), then each `values-{ar,de,es,fa,fr,hi,in,it,ja,ko,ms,pl,ps,pt-rBR,ru,ta,ur,zh-rCN,zh-rHK}` with Task 9's term table; `python3 scripts/i18n/native_check.py android values-` for each → `0 problem(s)`. +- [ ] **Step 4: Run** the Android test command → PASS; `cd android && JAVA_HOME=… ANDROID_HOME=… ./gradlew assembleDebug` → BUILD SUCCESSFUL (needs the `.so`: `core/scripts/build-android.sh` with `ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/27.2.12479018`; df first). +- [ ] **Step 5: Commit** `android: Settings → Multisig signers — add from files (Keystore key, per-use auth), check, review, last four + BiometricPrompt, ACTION_SEND; 20 languages` + trailer. + +--- + +### Task 14: One end-to-end on a devnet — the CLI prepares, web and iOS sign, the CLI proves, the payout lands + +**Files:** +- Create: `scripts/e2e-msauth.sh`, `web/wallet/test/msauth.devnet.e2e.test.mjs`, `ios/RandWalletTests/LiveMsauthTests.swift` +- Modify: `core/vendor/fullnode` + `CHAIN_BUILD` + the three version asserts (re-pin, below) + +**Interfaces:** +- Consumes: everything above; fullnode's CLI `rand multisig-private new-policy|address|balance|prepare|check|prove|submit` — **not in `e34117be`**: it lands at `2fb491a9`/`d5b2e652` on `feat/private-multisig` (e2e at `187af10b`). + +- [ ] **Step 1: Re-pin** to the private-multisig merge commit on fullnode `main` (or, if not merged yet, the `feat/private-multisig` head, which must contain `d5b2e652`): Task 1 Steps 1–4 with that SHA (`CHAIN_BUILD` = its first 7 characters), and re-copy the fixtures if `the_fixture_copies_are_the_vendored_vectors` fails (then rewrite `ui/test/fixtures/msauth.sha256` with Task 3 Step 1's three lines and say so in the commit). Commit `core: vendor fullnode (private multisig merged; rand multisig-private)`. +- [ ] **Step 2: Build the binaries** (df ≥ 25 GiB): + +```bash +cd core/vendor/fullnode && cp Cargo.lock /private/tmp/claude-501/fullnode-lock.bak +CARGO_TARGET_DIR=$PWD/../../target/e2e cargo build --release -p randprotocol-node -p randprotocol-client +cp /private/tmp/claude-501/fullnode-lock.bak Cargo.lock; cd ../../.. +core/scripts/build-wasm.sh && core/scripts/build-ios.sh +``` + +- [ ] **Step 3: The web signer.** `web/wallet/test/msauth.devnet.e2e.test.mjs`: + +```js +// One private-multisig signature from the web wallet's engine over the real wasm core, against the +// devnet scripts/e2e-msauth.sh runs. Skipped unless MSAUTH_E2E_DIR and MSAUTH_E2E_RPC are set. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { makeWasmBackend } from '../../../ui/engine/backend-wasm.js'; +import { mapStorage } from '../../../ui/test/backend-fixtures.mjs'; + +const CORE_JS = new URL('../../../extension/shared/core/rand_wallet.js', import.meta.url); +const CORE_WASM = new URL('../../../extension/shared/core/rand_wallet_bg.wasm', import.meta.url); +const DIR = process.env.MSAUTH_E2E_DIR || ''; +const RPC = process.env.MSAUTH_E2E_RPC || ''; +const SIGNER = process.env.MSAUTH_E2E_SIGNER ?? '0'; +const PASSWORD = 'an-e2e-password-for-a-real-vault'; +const skip = !DIR || !RPC || !existsSync(CORE_WASM) ? 'run scripts/e2e-msauth.sh' : false; +const read = (n) => readFileSync(join(DIR, n), 'utf8'); + +test('a web wallet signs a private-multisig proposal on a devnet', { skip, timeout: 120_000 }, async () => { + const mod = await import(CORE_JS.href); + mod.initSync({ module: readFileSync(CORE_WASM) }); + const core = { async call(m, p = {}) { const r = JSON.parse(mod.call(m, JSON.stringify(p))); if (!r.ok) throw new Error(r.error); return r.value; } }; + const backend = makeWasmBackend({ core, storage: mapStorage(), fetch, platform: { name: 'node-e2e', openExternal() {}, copy() {} }, locks: null, broadcast: null }); + await backend.wallet.create(PASSWORD); + await backend.settings.set({ rpcUrl: RPC }); + const [row] = await backend.signers.add({ label: 'web signer', policy: read('policy.json'), key: read(`signer${SIGNER}.key.json`), password: PASSWORD }); + assert.equal(row.index, Number(SIGNER)); + const payee = read('payee.txt').trim(); + const unchecked = await backend.signers.check(row.id, read('proposal.json')); + assert.equal(unchecked.refusal.code, 'unchecked-recipient', 'no payee entered, nothing signable'); + const checked = await backend.signers.check(row.id, read('proposal.json'), [payee]); + assert.equal(checked.signable, true, JSON.stringify(checked.refusal)); + const { share, index } = await backend.signers.sign(row.id, read('proposal.json'), [payee], PASSWORD); + assert.equal(index, Number(SIGNER)); + assert.deepEqual(await backend.signers.verifyShare(row.id, read('proposal.json'), share), { index }); + writeFileSync(join(DIR, `share${SIGNER}.txt`), `${share}\n`); +}); +``` + +- [ ] **Step 4: The iOS signer.** `ios/RandWalletTests/LiveMsauthTests.swift`: + +```swift +import XCTest +@testable import RandWallet + +/// One private-multisig signature through RandCore on the simulator, against the devnet +/// scripts/e2e-msauth.sh runs (it passes the files' contents: TEST_RUNNER_RAND_MSAUTH_*). The +/// Keychain's `.userPresence` cannot exist on a passcode-less simulator, so the store runs over +/// FakeSignerSecrets here; the device check is Task 10 Step 8. +@MainActor +final class LiveMsauthTests: XCTestCase { + func testSignsADevnetProposalThroughTheCore() async throws { + let env = ProcessInfo.processInfo.environment + guard let proposal = env["RAND_MSAUTH_PROPOSAL"], let policy = env["RAND_MSAUTH_POLICY"], let key = env["RAND_MSAUTH_KEY"], + let rpc = env["RAND_MSAUTH_RPC"].flatMap(URL.init(string:)) else { + throw XCTSkip("run scripts/e2e-msauth.sh") + } + let store = SignerStore(secrets: FakeSignerSecrets()) + let profile = try store.add(label: "ios signer", policy: policy, key: key) + guard let payee = env["RAND_MSAUTH_PAYEE"] else { throw XCTSkip("run scripts/e2e-msauth.sh") } + let (summary, verdict) = try await MsauthRules.check(proposal: proposal, policy: try store.policy(profile.id), payees: [payee], rpc: RpcClient(url: rpc), chainId: 21) + guard case .signable = verdict else { return XCTFail("\(verdict)") } + XCTAssertTrue(summary.recipientsChecked) + let share = try store.withSeed(profile.id, reason: "e2e") { try RandCore.msauthSign(proposal: MsauthRules.clean(proposal), policy: policy, seed: $0) } + try RandCore.msauthVerifyShare(proposal: proposal, policy: policy, share: share) + print("MSAUTH-SHARE \(share)") + } +} +``` + +- [ ] **Step 5: `scripts/e2e-msauth.sh`.** + +```bash +#!/usr/bin/env bash +# Private-multisig signing end to end (spec 2026-10-11-msauth-signing-design.md §6.5): a 2-of-3 +# account on a one-validator Test-profile devnet (chain 21 shape: split authorisation + the multisig +# auth guest), the CLI prepares, signer 0 signs in the web wallet's engine (real wasm core), signer 2 +# in iOS through RandCore on the simulator, the CLI proves (msauth tier 18 + bundle, on this Mac) +# and submits, and the payee's scan finds the payout. +# +# RAND_NODE_BIN=core/target/e2e/release/rand-node RAND_BIN=core/target/e2e/release/rand scripts/e2e-msauth.sh +set -euo pipefail +cd "$(dirname "$0")/.." +ROOT=$(pwd) +: "${RAND_NODE_BIN:?set RAND_NODE_BIN}" "${RAND_BIN:?set RAND_BIN}" +DIR=${MSAUTH_E2E_DIR:-$(mktemp -d)} +PORT=${MSAUTH_E2E_PORT:-18545} +RPC="http://127.0.0.1:$PORT" +export RAND_MSAUTH_VK="$ROOT/core/vendor/fullnode/crates/randprotocol-zkvm/keys/msauth-vk-test.bin" +export RAND_ALLOW_TEST_FRI_PROFILE=1 +R() { "$RAND_BIN" --rpc "$RPC" "$@"; } +rpc() { curl -sf -X POST -H 'content-type: application/json' -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":[]}" "$RPC"; } +echo "workdir $DIR" + +"$RAND_NODE_BIN" keygen --out "$DIR/node.key.json" +for i in 0 1 2; do "$RAND_NODE_BIN" keygen --out "$DIR/signer$i.key.json"; done +R --key "$DIR/b.key.json" keygen +B=$(R --key "$DIR/b.key.json" address | grep -Eo 'rand1[1-9A-HJ-NP-Za-km-z]+' | head -1) +"$RAND_NODE_BIN" genesis --chain-id 21 --fri-profile test --faucet --validator "$DIR/node.key.json,1000,$B" \ + --gas-price 100 --byte-price 800 --bundle-gas-limit 20479 --bundle-guest v3 --auth-guest --auth-multisig-guest \ + --max-proof-bytes 4194304 --max-block-bytes 20971520 --binding-domain 1 --proof-window-blocks 1024 --out "$DIR/genesis.json" +"$RAND_NODE_BIN" init --datadir "$DIR/data" --genesis "$DIR/genesis.json" +"$RAND_NODE_BIN" run --datadir "$DIR/data" --key "$DIR/node.key.json" --validator --rpc "127.0.0.1:$PORT" \ + --listen /ip4/127.0.0.1/tcp/0 --no-mdns --block-interval-ms 3000 --view-timeout-ms 6000 --min-free-disk-mb 0 > "$DIR/node.log" 2>&1 & +NODE_PID=$! +trap 'kill $NODE_PID 2>/dev/null || true' EXIT +for _ in $(seq 120); do rpc rand_status | grep -q '"msauth_key":"loaded"' && break; sleep 1; done +rpc rand_status | grep -q '"msauth_key":"loaded"' || { tail -40 "$DIR/node.log"; exit 1; } + +pk() { python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["public_key"])' "$1"; } +R multisig-private new-policy --signer "$(pk "$DIR/signer0.key.json")" --signer "$(pk "$DIR/signer1.key.json")" \ + --signer "$(pk "$DIR/signer2.key.json")" --threshold 2 --out "$DIR/policy.json" +ACCT=$(R multisig-private address --policy "$DIR/policy.json" | sed -n 's/^address: //p') +R faucet "$ACCT" --amount 100 +R multisig-private balance --policy "$DIR/policy.json" +R multisig-private prepare --policy "$DIR/policy.json" --to "$B" --amount 10 --out "$DIR/proposal.json" +echo "$B" > "$DIR/payee.txt" # what each signer is told out of band, and types in + +MSAUTH_E2E_DIR="$DIR" MSAUTH_E2E_RPC="$RPC" MSAUTH_E2E_SIGNER=0 node --test web/wallet/test/msauth.devnet.e2e.test.mjs + +( cd ios && TEST_RUNNER_RAND_MSAUTH_PROPOSAL="$(cat "$DIR/proposal.json")" TEST_RUNNER_RAND_MSAUTH_POLICY="$(cat "$DIR/policy.json")" \ + TEST_RUNNER_RAND_MSAUTH_KEY="$(cat "$DIR/signer2.key.json")" TEST_RUNNER_RAND_MSAUTH_RPC="$RPC" TEST_RUNNER_RAND_MSAUTH_PAYEE="$B" \ + xcodebuild -project RandWallet.xcodeproj -scheme RandWallet -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ + test -only-testing:RandWalletTests/LiveMsauthTests ) 2>&1 | tee "$DIR/ios.log" +sed -n 's/.*MSAUTH-SHARE //p' "$DIR/ios.log" | head -1 > "$DIR/share2.txt" +grep -q '^2:' "$DIR/share2.txt" || { echo "iOS printed no share"; exit 1; } + +/usr/bin/time -l "$RAND_BIN" --rpc "$RPC" --cpu multisig-private prove --proposal "$DIR/proposal.json" --policy "$DIR/policy.json" \ + --signature "$(tr -d '\n' < "$DIR/share0.txt")" --signature "$(tr -d '\n' < "$DIR/share2.txt")" --out "$DIR/proposal.tx" 2> "$DIR/prove.time" +grep 'maximum resident set size' "$DIR/prove.time" +R multisig-private submit --tx "$DIR/proposal.tx" +R --key "$DIR/b.key.json" sync +R --key "$DIR/b.key.json" balance | tee "$DIR/b.balance" +grep -Eq '(^|[^0-9.])10(\.0+)? RAND' "$DIR/b.balance" || { echo "the payee does not hold 10 RAND"; exit 1; } +echo "msauth e2e: OK" +``` + +`chmod +x scripts/e2e-msauth.sh`. If `genesis` refuses a flag, read `"$RAND_NODE_BIN" genesis --help` and adjust to the fullnode `187af10b` test genesis (`genesis_private_multisig`: proof window 1024, 4 MiB proofs, 20 MiB blocks, gas section). +- [ ] **Step 6: Run** (nothing else proving; df ≥ 25 GiB): `RAND_NODE_BIN=core/target/e2e/release/rand-node RAND_BIN=core/target/e2e/release/rand scripts/e2e-msauth.sh 2>&1 | tee /private/tmp/claude-501/msauth-e2e.log` → `msauth e2e: OK`. Record the prove's wall time and peak RSS (fullnode's CLI says "at least 48 GB" for tier 18; the Test-profile peak is unmeasured in fullnode's docs — this run measures it; this Mac has 48 GB: if the peak passes ~40 GB, stop and report rather than swap). +- [ ] **Step 7: Commit** `e2e: private multisig end to end — CLI prepares, the web engine and iOS sign through the core, CLI proves and submits, the payout lands (, )` + trailer. + +--- + +## Self-review notes (2026-10-12) + +- Spec coverage: §2 table → Tasks 3–4 (+ wrappers 5, 10, 12); §3 storage and re-auth → 6, 10, 12; §4 steps 1–5 → 6–8, 11, 13; file pickers → 8, 11, 13; error mapping → 5, 10, 12; §6.1 re-pin + prover e2e → 1, 2; §6.3 locales → 9, 11, 13; §6.5 devnet → 14. Deferred by decision: profiles without a wallet (Global Constraints). +- Spec `639cae7` (§4 step 3, payees entered; chain-id/genesis line compared with the node): Tasks 3, 5, 7, 8, 10–14; the crate half is still moving — every dependent name is tagged **[confirm: 0e]**; the re-vendor to the fix-wave commit (Task 14 Step 1, or earlier if 0e lands first) re-copies the fixtures, rewrites `msauth.sha256`, and renames to match. +- Spec facts corrected here: the msig crate signs through `randprotocol_core::Keypair` (no direct `crystals-dilithium` dependency; that is a zkvm dev-dependency); a tampered output is refused as "output N does not open as this account's: refusing to sign value it cannot see"; `core-errors.js` maps English sentences, not codes; `ProposalSummary` amounts are `u64` and are re-shaped to strings; the CLI (`rand multisig-private`) is not in `e34117be` (it lands at `2fb491a9`/`d5b2e652`, e2e `187af10b`); the CLI's own prove line says "at least 48 GB"; JS strings cannot be zeroed; Android per-use device-credential auth with a `CryptoObject` needs API 30 (minSdk is 26). +- Names checked across tasks: `msauth_check|sign|verify_share|signer_import|policy_info`; `coreApi.msauth*`; `makeSigners` → `{group, openOnUnlock}`; `signers.{list,preview,add,remove,check,sign,verifyShare}`; `verdict`, `EXPIRY_MARGIN`, `SignerRefusal`, `cleanText`; iOS `MsauthRules`, `SignerStore`, `SignerSecrets`; Android `SignFlow`, `SignerProfiles`, `KeystoreSeedCipher`. diff --git a/docs/superpowers/specs/2026-10-11-msauth-signing-design.md b/docs/superpowers/specs/2026-10-11-msauth-signing-design.md new file mode 100644 index 0000000..648fe6d --- /dev/null +++ b/docs/superpowers/specs/2026-10-11-msauth-signing-design.md @@ -0,0 +1,141 @@ +# Private multisig signing in every wallet app + +Status: design, 2026-10-11. Implementation waits on fullnode's private multisig (the `msauth` guest, +constraint set 9) reaching fullnode `main`. The user's decision: **every app signs** — web wallet, +extension, desktop, iOS, Android. + +## 1. What a signer does + +A private multisig account is spent by a proposal that T of its N signers sign (fullnode +`docs/superpowers/specs/2026-10-11-private-multisig-phase4-design.md` §6). One person — anyone holding +the policy — runs `rand multisig-private prepare` and passes `prop.json` to the signers. Each signer +checks it, signs it, and sends back one share, `i:`. The preparer collects ≥ T shares and +runs `prove` (tier 18, ~31 GB) and `submit` on a big machine with the CLI. + +The apps do the middle step only: **check, show, sign, share.** They never prepare, prove or submit. + +## 2. The core API (one source of truth) + +Every check is fullnode's, through the new crate `randprotocol-msig` (re-exported as +`randprotocol_client::multisig_private`; agreed with the private-multisig session; deps core, zkvm, +serde, hex, thiserror; pure: no RPC, no filesystem, no OS entropy; fullnode CI checks it for +`wasm32-unknown-unknown`). wallet-core depends on `randprotocol-msig` directly: + +| fullnode | wallet-core `call` method | params → value | +|---|---|---| +| `check_proposal(proposal, policy) -> ProposalSummary` | `msauth_check` | `{proposal, policy}` → summary JSON | +| `sign(proposal, policy, seed) -> (u8, Vec)` | `msauth_sign` | `{proposal, policy, seed}` → `"i:hex"` | +| `verify_share(proposal, policy, share)` | `msauth_verify_share` | `{proposal, policy, share}` → `true` | +| — | `msauth_signer_import` | `{input}` (a `rand-node keygen` JSON or 64 hex) → `{seed, public_key, address}` | +| — | `msauth_policy_info` | `{policy}` → `{fingerprint, threshold, signers, address}` | + +`ProposalSummary`: outputs (recipient, amount, asset, memo), fee, burns, `expires_at`, +`signing_message`, `policy_fingerprint`. `msauth_signer_import` checks the file's `public_key` +against the one derived from `seed` and refuses a mismatch. `msauth_sign` refuses a key that is not +in the policy (fullnode's rule); the app also checks this at import, so a wrong key fails early. + +New arms go in `wallet_core::call` (`core/crates/wallet-core/src/lib.rs`); wasm, FFI and Tauri pick +them up through the existing generic `call` with no binding change. Typed wrappers: +`ui/engine/wallet.js` `coreApi()`, `ios/RandWallet/Core/RandCore.swift`, +`android/.../core/Core.java`. Error codes map in `ui/engine/core-errors.js` and the native error +tables. + +## 3. What the app stores: a signer profile + +A **signer profile** = `{label, policy (JSON), seed}`. Both halves are secret: the seed signs, and +the policy's `msk` is viewing power over the account. A profile is stored exactly where and how the +app stores its spend key today, never in plain storage: + +- **web, extension, desktop** (shared engine): inside the existing vault, encrypted with the same + AES-256-GCM / PBKDF2 key (`ui/engine/crypto.js`, `backend-shared.js`), readable only while + unlocked. The vault gains a `signers` list beside the spend key. + +**Every signature re-authenticates** (user's decision 2026-10-12). Unlocking the app does not +unlock signer seeds: each `msauth_sign` first asks for the password (web, extension, desktop: +the seed is decrypted from the vault for that one call and the buffer is zeroed after) or device +auth (iOS: Keychain item with `.userPresence` access control, read per signature; Android: +a Keystore key with `setUserAuthenticationRequired(true)` and a per-use BiometricPrompt / +device credential). The seed is never cached between signatures. Viewing a profile's policy +(fingerprint, T of N) needs only the normal unlock. +- **iOS**: Keychain items `msauth..seed` and `msauth..policy`, `WhenUnlockedThisDeviceOnly`, + as `spend_key` is (`Storage/Keychain.swift`). +- **Android**: `KeyVault` entries of the same names (EncryptedSharedPreferences, Keystore master key). + +Several profiles are allowed (one person may sign for several accounts). Removing a profile deletes +both halves. A profile does not need a spend key: an app with no wallet can still hold signer +profiles, but the existing unlock (password / device auth) gates them all the same. + +**Proposals are never stored.** A proposal holds the account's witness (note openings, `nk`): it is +read into memory for one signing and dropped. + +## 4. The flow (every app) + +1. **Signers → Add**: import the policy file and the signer key (paste or file). The app shows the + account address, its fingerprint (to compare out of band with the other signers — the design's + own instruction), `T of N`, and this key's position. The profile is saved only after that screen. +2. **Signers → → Sign a proposal**: paste or open `prop.json`. `msauth_check` runs; any + failure is shown by name and nothing can be signed. +3. **Review**: every output (recipient, amount and asset, memo), the fee, any burn, the expiry, + the fingerprint. A recipient is shown as `check_proposal` gives it: a fingerprint of the owner + key only, with the caveat "owner key verified; delivery key NOT verifiable from this proposal" + rendered as-is (a proposal cannot prove the recipient's KEM key, so the app must not show a full + address fingerprint as if it had checked one). + **The signer must enter each payee's address** (2026-10-12, from the private-multisig review: a + proposal alone cannot tell the signer that an output pays the payee they meant, so a dishonest + coordinator could pay its own key with every check passing). Before review, the signer enters + the **intended payments**: one row per payee, address and amount, both required (a payee paid + twice is one row with the total). The app passes them as `address:amount` to + `check_proposal`'s expected-recipients input. The crate (2026-10-12 ruling) matches by set, + refuses a paying output whose owner is in no row, a row no output pays, and a row whose amount + differs from the sum paid to that address. Each output then shows "matches
"; an output with no match, or a recipient left empty, refuses signing (the app + never signs on "no expected recipient given: the payee was NOT checked"). The chain-id and + genesis line is shown and compared with the node's `rand_getHead`/status. Each output carries `to_account` (verified: the + note's owner is the multisig account itself), so the review labels change apart from payments. `check_proposal` has no RPC, so it only checks `expires_at` against the + bundle's own time; **the app compares it with the chain head** (`rand_getHead`, which every + app already reads) and refuses to sign at or past `expires_at - 16`, the CLI `prove`'s margin. + **With no node reachable it refuses to sign** (user's decision 2026-10-12): the expiry cannot be + checked, so the share is not made; the screen says which node it tried and offers retry. + Confirmation types the last four characters of the + fingerprint, the withdraw flow's pattern (`ui/screens/withdraw.js`). +4. **Share**: `msauth_sign` → the share string. Copy, share sheet (iOS `ShareLink`, Android + `ACTION_SEND`), or save as a file. The share is not secret (a signature over a public message). +5. Optional: **check a share** someone else sent (`msauth_verify_share`) — shows which positions are + in hand against T. + +New input for all apps: a **file picker** (web/desktop ``, iOS `.fileImporter`, +Android `ACTION_OPEN_DOCUMENT`), since proposals are tens of KB — awkward to paste on a phone. Paste +stays available. + +## 5. Out of scope + +Preparing, proving and submitting (CLI; proving needs ~31 GB); delegated msauth proving (a fullnode +follow-up, phase-4 spec §6.6); showing the multisig account's balance in the app (needs the +`Wallet::from_viewing_key` scan path; a later step); the CLI's validator-key warning (the apps +cannot tell a consensus key file apart). + +## 6. Order of work + +1. Re-pin `core/vendor/fullnode` to the private multisig merge (constraint set 9; the wallet's + bundle proofs change with it) and re-run the real-node prover e2e on a chain-21 shape — + **this makes clients main chain-21-only; chain 20 fixes go to `release/chain20`.** +2. Core arms + tests, using the fixtures committed with fullnode's `randprotocol-msig` + (`crates/randprotocol-msig/tests/fixtures/`: `policy`, `proposal-valid`, + `proposal-tampered-output`, `proposal-value-mismatch`, `proposal-fee-raised` (refused only by + the per-asset balance check), `proposal-unbound` (refused on the binding), + `proposal-wrong-expected`, `proposal-expired`, + `signer-seeds` (three + members + one outsider), `expected` (summary, signing message, shares for members 0 and 2)). They + are deterministic, so the app tests match exact values, not only pass/fail. The tampered output is + re-bound: it fails as "output does not open as sender", not on the binding. The zkvm re-pin + brings `dilithium-core` gated off wasm32 (its `asm!` does not build there); the msig crate uses + crystals-dilithium directly. Vendor from fullnode `e34117be` (Task 6 complete; 10 fixtures, + SHA-256s checked 2026-10-12) or the merge that carries it. +3. Shared UI (web, extension, desktop) + unit tests + strings in all 20 locales. +4. iOS + XCTest; Android + JVM tests; native string parity. +5. One end-to-end: a 2-of-3 account on a devnet, the CLI prepares, two apps sign (web and one + native through its core), the CLI proves and submits, the payout lands. + +## 7. Release + +Ships with the chain-21 wallet release (set 9), never to chain-20 users. diff --git a/extension/shared/lib/platform.js b/extension/shared/lib/platform.js index b684f2a..bd2f6d2 100644 --- a/extension/shared/lib/platform.js +++ b/extension/shared/lib/platform.js @@ -27,6 +27,14 @@ export function makePlatform({ firefox = IS_FIREFOX, t } = {}) { // A new tab, never this popup's window: an explorer must get no handle on the wallet. openExternal: (url) => { ext.tabs.create({ url: String(url) }); }, copy: (text) => navigator.clipboard.writeText(String(text ?? '')), + // A share (`i:hex`) or other small text as a downloaded file. Not secret. + saveText: ({ name, text }) => { + const url = URL.createObjectURL(new Blob([String(text)], { type: 'text/plain' })); + const a = document.createElement('a'); + a.href = url; a.download = String(name); a.rel = 'noopener'; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }, /** * OPTIONAL in the contract, and the reason it exists: an extension may only reach a host it diff --git a/ios/RandWallet.xcodeproj/project.pbxproj b/ios/RandWallet.xcodeproj/project.pbxproj index 0dbac59..721c2b2 100644 --- a/ios/RandWallet.xcodeproj/project.pbxproj +++ b/ios/RandWallet.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0031217ED8D8CD439DC321F6 /* proposal-expired.json in Resources */ = {isa = PBXBuildFile; fileRef = 30E637DC0C8968BADF8C8789 /* proposal-expired.json */; }; 00548EAF94820025B0EF5227 /* DepartureMono-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6517CA906DB823B64DC2D4AE /* DepartureMono-OFL.txt */; }; 01CC98496ECD20E886B41839 /* AmmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF27F56115E51C8E102D8AB /* AmmTests.swift */; }; 032203A43C3EB86266C74714 /* NoteStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F435046A1ED71DE0910C5DE8 /* NoteStoreTests.swift */; }; @@ -23,45 +24,64 @@ 25CA49B8860133B80AB64F0D /* LockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A60F2C0B701563C8D31F051A /* LockView.swift */; }; 2A2537DD37ADE821664F66EF /* JetBrainsMono-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 05B3F1CE5065BD9C4B1EF033 /* JetBrainsMono-OFL.txt */; }; 2BE16BD319D7839213E157FF /* Inter-OFL.txt in Resources */ = {isa = PBXBuildFile; fileRef = 26E39DD97C0FB4416D70C25E /* Inter-OFL.txt */; }; + 2DCDDEE19B7ABEC096929A6D /* proposal-tampered-output.json in Resources */ = {isa = PBXBuildFile; fileRef = 25EB7CD780BECC4D76143430 /* proposal-tampered-output.json */; }; 2F82B858E4FD08B0BCEB5E5A /* ProverClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F38010836B46706F1EEF2C /* ProverClient.swift */; }; 31CF8C9FD95F271ED9BB6AEE /* RandWalletApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2DD8768BEDCFD16439F3C8 /* RandWalletApp.swift */; }; + 33C1EFB5740A11FD6B9F18DB /* proposal-fee-raised.json in Resources */ = {isa = PBXBuildFile; fileRef = 4E568A1F48EF6BAB25D5FB8A /* proposal-fee-raised.json */; }; 3565A1CB00F34196A7E73AE8 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4480C53C422CBF97386A8CCC /* Settings.swift */; }; + 39287425DE900EEE0D991A18 /* SignerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E9E25597C66480A5F84DD4 /* SignerStore.swift */; }; 39ECD2E0F8DAC40EE6BBE9D3 /* LiveSwapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFC1D783C50E7F748573284 /* LiveSwapTests.swift */; }; 3BEEF00D71CCDD67DE4102BE /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 36C4F0D6B81F1AB901EECC42 /* InfoPlist.xcstrings */; }; 3DA25B42D56E3D8712FA4EDF /* ReceiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579B488EB09271EADE817752 /* ReceiveView.swift */; }; + 3E051DFC1CBDFFAC0EC9FB82 /* proposal-substituted-recipient.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B06B846C15119641FBE87E1 /* proposal-substituted-recipient.json */; }; 416476360AE7F708578387D0 /* ProverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EFF95F847F91E6F0EED5358 /* ProverTests.swift */; }; 43D785D5C212AE422FE8BEE8 /* CoreSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9FD71335C8D0B8513015FE /* CoreSmokeTests.swift */; }; 46A65DB167E5023BCA5DD83C /* TypefaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40B92E6506345D5C81599BF /* TypefaceTests.swift */; }; + 4FCE7819F36C9A8468D0CEF0 /* expected.json in Resources */ = {isa = PBXBuildFile; fileRef = D0AE0D1769FB5401CAE879A9 /* expected.json */; }; 5E809DE4F136AAEAD14CF445 /* InterVariable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C6A590C5B77BAD64AC13D7BB /* InterVariable.ttf */; }; 64554089BC7AAD37655A0162 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548109BF97F5DB7DB99F6CE /* HomeView.swift */; }; 66ACB9955A4ACD4DB0CBD929 /* DepositWalkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D73D5A77FFC38FC54259D79 /* DepositWalkTests.swift */; }; 686393B1820A71E557A7A91C /* SwapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF55C3B3E2EBB1C8325F5E1E /* SwapView.swift */; }; + 69324B64FAC3E19759982AA7 /* Msauth.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C1D597A763D74857FBC37D /* Msauth.swift */; }; + 6A9228C8C3FE7ACEDFA3E9E8 /* proposal-wrong-expected.json in Resources */ = {isa = PBXBuildFile; fileRef = 3F77320D83C8A3E752FA1FFD /* proposal-wrong-expected.json */; }; + 6E48B8F2B23461EE61521E90 /* MsauthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79984AD66809D63D1FFF9B5E /* MsauthTests.swift */; }; 71628504EFADE7B1A8B2E0A7 /* LinkHandoffTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39BB6F1188AB35E970520CF /* LinkHandoffTests.swift */; }; + 71D3604178EBA21C8037BF5D /* SettingsMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBF4772C089EB50D9A35F795 /* SettingsMigrationTests.swift */; }; 73DE625E5E18560ECF227095 /* NoteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 183284CD1DA185232100F09D /* NoteStore.swift */; }; 746BF8D8C153106BFEF2336F /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA40E259F05440DD2DAE0A55 /* Models.swift */; }; 76838E0AF20A51CF247552FA /* ContactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8C97D2A4B42DF67451B26 /* ContactsTests.swift */; }; 7774EF16AEF9271B5FC2A455 /* DepositWalk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAA1C4A31DC664F417BA006 /* DepositWalk.swift */; }; 7F51FCBEB896DBFE88FAC922 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 422B13542A6961C4E86C6C77 /* QRScannerView.swift */; }; + 7FACBE748A5C3EF19DB52ACD /* proposal-value-mismatch.json in Resources */ = {isa = PBXBuildFile; fileRef = 20B04FF141C65024CA27E083 /* proposal-value-mismatch.json */; }; 800C1F2EAA73B476DEF97F7D /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F641C399AF3E2F3F40FA15A /* Contacts.swift */; }; 8F6C36B95B6DDDCD64E799C2 /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356ED987BBE644E0AA7E6C60 /* SendView.swift */; }; 8FBEFEC8F08643962AD3C59B /* ProverPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98FEAC05B5B18B8B98085A5C /* ProverPairingService.swift */; }; 90AE055BC64ACE91BCA68904 /* SendLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228AE584E190F486DBFBDEA4 /* SendLinkTests.swift */; }; + 918C1F9E9C679411B11532D0 /* signer-seeds.json in Resources */ = {isa = PBXBuildFile; fileRef = 2596E204738D5D9ECA9B03C1 /* signer-seeds.json */; }; 923296960E607F866AEE2A14 /* RpcClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 600C46C8B2B89650FE98BDFC /* RpcClient.swift */; }; + 95D365B9F01F58B20A5A37CE /* FakeSignerSecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54A4FC4B625FCA03BD1809F /* FakeSignerSecrets.swift */; }; 9B3828D22658A63EA00256BD /* SendLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09C7A5E73D13000AE0776FF /* SendLink.swift */; }; A94A110DBAFF180E10CC2EF0 /* SOURCES.txt in Resources */ = {isa = PBXBuildFile; fileRef = 142083F0DE48DA5CC65AB1AA /* SOURCES.txt */; }; ACF8927BE6ED236F1EB1BD0A /* InvokeFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C8AADDBA61C258B1C39CD8 /* InvokeFlowTests.swift */; }; B1687316C1DC29FF183854FD /* CoreErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C582C0C2D633061866B4C6D /* CoreErrors.swift */; }; + B22E5A9CC6A2ABA4117D4F1B /* SignProposalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159C1BE693B93053206A69E3 /* SignProposalView.swift */; }; + BAAEDD51953A336B162E8D48 /* amm-vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = 74FF2ECA6ED57507B4930099 /* amm-vectors.json */; }; + BB4E77440A6361ED64BE14C3 /* LiveMsauthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35025A8CB3E22690DF9731F2 /* LiveMsauthTests.swift */; }; + C1BA6A704D0A4F562E6E5A4D /* SignersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB5F6B1A04E1129ED660E56 /* SignersView.swift */; }; C9BD56CBE6E9B189E638A8C9 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = FB4A66E6F62229FCDD173613 /* Localizable.xcstrings */; }; CC2E8838F78355A6E1C149F7 /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11EDD0B6F2E0ABC54D4BFED /* AuthService.swift */; }; CC5B54F04757CD85EAEDF97B /* WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9480BB9E9D8F9C3D2AD31509 /* WalletService.swift */; }; D0CAA1A790008E7424245371 /* RandCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39129BEB2E8214E3A8C0E574 /* RandCore.swift */; }; + D5825C1587E0CF33C8AFDA0C /* proposal-valid.json in Resources */ = {isa = PBXBuildFile; fileRef = 16F062C6CCD3D6D8600B199B /* proposal-valid.json */; }; + DA810BA0429EEF41EC109EBE /* proposal-unbound.json in Resources */ = {isa = PBXBuildFile; fileRef = 3087D3DBD4C9075C548E170D /* proposal-unbound.json */; }; DEB816520B783380A0B1801C /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BC68404249FF81F6A77075 /* ContactsView.swift */; }; E016AEDBB976EB8AD8BE1CBE /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976224CCD1E531D92268C700 /* Keychain.swift */; }; + E1CB9FBE512EB4B97978162F /* policy.json in Resources */ = {isa = PBXBuildFile; fileRef = B796A5F79E0E79024B337B1D /* policy.json */; }; E321477FFFA89C2983DF71DF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A2685E3F5D2750E9DE3FA018 /* PrivacyInfo.xcprivacy */; }; EF4A61C6A533C34F8AE93363 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB8569A9BA22F2E98649199 /* WelcomeView.swift */; }; F0220DB33F7840E59DB47343 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 676FE17810EFAC969314E1CB /* RootView.swift */; }; F19AC0B40F8720FA62EA5EC1 /* Mark.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609740750127F3652929BF8 /* Mark.swift */; }; - FA7FBD630919B2848BAAC8C0 /* amm-vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = 0E541682C28481F7E2953453 /* amm-vectors.json */; }; + F74310C7272C05AF1338805A /* SignScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F22E28C2D23B70ABE9070D /* SignScreenTests.swift */; }; FC6006023AFDB0DE7A54215D /* LiveSendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511FB7F3639F8196E229371D /* LiveSendTests.swift */; }; FE817779952D1666D69915AE /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F81980ED99AC808A372C7D3 /* SettingsView.swift */; }; /* End PBXBuildFile section */ @@ -79,22 +99,31 @@ /* Begin PBXFileReference section */ 05B3F1CE5065BD9C4B1EF033 /* JetBrainsMono-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "JetBrainsMono-OFL.txt"; sourceTree = ""; }; 06C8AADDBA61C258B1C39CD8 /* InvokeFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvokeFlowTests.swift; sourceTree = ""; }; - 0E541682C28481F7E2953453 /* amm-vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "amm-vectors.json"; sourceTree = ""; }; 0EE01B221B5970997D744292 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 142083F0DE48DA5CC65AB1AA /* SOURCES.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = SOURCES.txt; sourceTree = ""; }; + 159C1BE693B93053206A69E3 /* SignProposalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignProposalView.swift; sourceTree = ""; }; + 16F062C6CCD3D6D8600B199B /* proposal-valid.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-valid.json"; sourceTree = ""; }; 183284CD1DA185232100F09D /* NoteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteStore.swift; sourceTree = ""; }; 1B2F56A8ABC5E364C3C1FAD3 /* InvokeFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvokeFlow.swift; sourceTree = ""; }; 1CAA1C4A31DC664F417BA006 /* DepositWalk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepositWalk.swift; sourceTree = ""; }; + 20B04FF141C65024CA27E083 /* proposal-value-mismatch.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-value-mismatch.json"; sourceTree = ""; }; 228AE584E190F486DBFBDEA4 /* SendLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendLinkTests.swift; sourceTree = ""; }; + 2596E204738D5D9ECA9B03C1 /* signer-seeds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "signer-seeds.json"; sourceTree = ""; }; + 25EB7CD780BECC4D76143430 /* proposal-tampered-output.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-tampered-output.json"; sourceTree = ""; }; 26E39DD97C0FB4416D70C25E /* Inter-OFL.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Inter-OFL.txt"; sourceTree = ""; }; 2D2DD8768BEDCFD16439F3C8 /* RandWalletApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandWalletApp.swift; sourceTree = ""; }; 2EFF95F847F91E6F0EED5358 /* ProverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProverTests.swift; sourceTree = ""; }; + 3087D3DBD4C9075C548E170D /* proposal-unbound.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-unbound.json"; sourceTree = ""; }; + 30E637DC0C8968BADF8C8789 /* proposal-expired.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-expired.json"; sourceTree = ""; }; + 35025A8CB3E22690DF9731F2 /* LiveMsauthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMsauthTests.swift; sourceTree = ""; }; 356ED987BBE644E0AA7E6C60 /* SendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendView.swift; sourceTree = ""; }; 36C4F0D6B81F1AB901EECC42 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 39129BEB2E8214E3A8C0E574 /* RandCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandCore.swift; sourceTree = ""; }; 3E9FD71335C8D0B8513015FE /* CoreSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSmokeTests.swift; sourceTree = ""; }; + 3F77320D83C8A3E752FA1FFD /* proposal-wrong-expected.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-wrong-expected.json"; sourceTree = ""; }; 422B13542A6961C4E86C6C77 /* QRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = ""; }; 4480C53C422CBF97386A8CCC /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + 4E568A1F48EF6BAB25D5FB8A /* proposal-fee-raised.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-fee-raised.json"; sourceTree = ""; }; 511FB7F3639F8196E229371D /* LiveSendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveSendTests.swift; sourceTree = ""; }; 51B8C97D2A4B42DF67451B26 /* ContactsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsTests.swift; sourceTree = ""; }; 579B488EB09271EADE817752 /* ReceiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveView.swift; sourceTree = ""; }; @@ -107,8 +136,12 @@ 6C582C0C2D633061866B4C6D /* CoreErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreErrors.swift; sourceTree = ""; }; 6F641C399AF3E2F3F40FA15A /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; 6F81980ED99AC808A372C7D3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 74FF2ECA6ED57507B4930099 /* amm-vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "amm-vectors.json"; sourceTree = ""; }; + 79984AD66809D63D1FFF9B5E /* MsauthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsauthTests.swift; sourceTree = ""; }; + 7B06B846C15119641FBE87E1 /* proposal-substituted-recipient.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "proposal-substituted-recipient.json"; sourceTree = ""; }; 8548109BF97F5DB7DB99F6CE /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 859D1BB14A8B8B0751915C9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 86E9E25597C66480A5F84DD4 /* SignerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignerStore.swift; sourceTree = ""; }; 8EF27F56115E51C8E102D8AB /* AmmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmmTests.swift; sourceTree = ""; }; 90BC68404249FF81F6A77075 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; 9330A3D39298C1863E79BA85 /* Amm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Amm.swift; sourceTree = ""; }; @@ -116,20 +149,27 @@ 96401A24D2E186DDE3D3FE48 /* DepartureMono-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "DepartureMono-Regular.otf"; sourceTree = ""; }; 976224CCD1E531D92268C700 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; 98FEAC05B5B18B8B98085A5C /* ProverPairingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProverPairingService.swift; sourceTree = ""; }; + 9AB5F6B1A04E1129ED660E56 /* SignersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignersView.swift; sourceTree = ""; }; 9FB8569A9BA22F2E98649199 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; A11EDD0B6F2E0ABC54D4BFED /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; A2685E3F5D2750E9DE3FA018 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A60F2C0B701563C8D31F051A /* LockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockView.swift; sourceTree = ""; }; + B796A5F79E0E79024B337B1D /* policy.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = policy.json; sourceTree = ""; }; BA40E259F05440DD2DAE0A55 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; BA51C5F8034D8DD473D82DDA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + BBF4772C089EB50D9A35F795 /* SettingsMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationTests.swift; sourceTree = ""; }; + C54A4FC4B625FCA03BD1809F /* FakeSignerSecrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeSignerSecrets.swift; sourceTree = ""; }; C6A590C5B77BAD64AC13D7BB /* InterVariable.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = InterVariable.ttf; sourceTree = ""; }; CA6DD6FD8AAEBC3E7484D380 /* RandWalletTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = RandWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CC2ABE502E7CB89EA50A54D2 /* JetBrainsMono-Variable.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "JetBrainsMono-Variable.ttf"; sourceTree = ""; }; CF55C3B3E2EBB1C8325F5E1E /* SwapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapView.swift; sourceTree = ""; }; D09C7A5E73D13000AE0776FF /* SendLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendLink.swift; sourceTree = ""; }; + D0AE0D1769FB5401CAE879A9 /* expected.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = expected.json; sourceTree = ""; }; D383A140E56010E9A65AA0C2 /* RandWallet.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = RandWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D8C1D597A763D74857FBC37D /* Msauth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Msauth.swift; sourceTree = ""; }; DE7B6AB7687B725C159B84EF /* Amount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Amount.swift; sourceTree = ""; }; E39BB6F1188AB35E970520CF /* LinkHandoffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkHandoffTests.swift; sourceTree = ""; }; + E6F22E28C2D23B70ABE9070D /* SignScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignScreenTests.swift; sourceTree = ""; }; E9F38010836B46706F1EEF2C /* ProverClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProverClient.swift; sourceTree = ""; }; EFFC1D783C50E7F748573284 /* LiveSwapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveSwapTests.swift; sourceTree = ""; }; F40B92E6506345D5C81599BF /* TypefaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypefaceTests.swift; sourceTree = ""; }; @@ -151,6 +191,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0248590AF9D5BB554475B60D /* msauth */ = { + isa = PBXGroup; + children = ( + D0AE0D1769FB5401CAE879A9 /* expected.json */, + B796A5F79E0E79024B337B1D /* policy.json */, + 30E637DC0C8968BADF8C8789 /* proposal-expired.json */, + 4E568A1F48EF6BAB25D5FB8A /* proposal-fee-raised.json */, + 7B06B846C15119641FBE87E1 /* proposal-substituted-recipient.json */, + 25EB7CD780BECC4D76143430 /* proposal-tampered-output.json */, + 3087D3DBD4C9075C548E170D /* proposal-unbound.json */, + 16F062C6CCD3D6D8600B199B /* proposal-valid.json */, + 20B04FF141C65024CA27E083 /* proposal-value-mismatch.json */, + 3F77320D83C8A3E752FA1FFD /* proposal-wrong-expected.json */, + 2596E204738D5D9ECA9B03C1 /* signer-seeds.json */, + ); + path = msauth; + sourceTree = ""; + }; 02561A443EC549A5F22F2988 /* Services */ = { isa = PBXGroup; children = ( @@ -171,45 +229,21 @@ DE7B6AB7687B725C159B84EF /* Amount.swift */, 6C582C0C2D633061866B4C6D /* CoreErrors.swift */, BA40E259F05440DD2DAE0A55 /* Models.swift */, + D8C1D597A763D74857FBC37D /* Msauth.swift */, 39129BEB2E8214E3A8C0E574 /* RandCore.swift */, D09C7A5E73D13000AE0776FF /* SendLink.swift */, ); path = Core; sourceTree = ""; }; - 0DD951A62E7C78C576425B28 /* ui */ = { + 278307C39A8B7F50557DEB38 /* ui */ = { isa = PBXGroup; children = ( - 4B2D205227D99993871B4F9A /* test */, + 7E383413DAB619B5913ACACC /* test */, ); path = ui; sourceTree = ""; }; - 29D1F4488FDE8711E87FD9BC /* fixtures */ = { - isa = PBXGroup; - children = ( - 0E541682C28481F7E2953453 /* amm-vectors.json */, - ); - path = fixtures; - sourceTree = ""; - }; - 2C8AED69F5FC939BDE530407 /* i18n */ = { - isa = PBXGroup; - children = ( - 0DD951A62E7C78C576425B28 /* ui */, - ); - name = i18n; - path = ..; - sourceTree = ""; - }; - 4B2D205227D99993871B4F9A /* test */ = { - isa = PBXGroup; - children = ( - 29D1F4488FDE8711E87FD9BC /* fixtures */, - ); - path = test; - sourceTree = ""; - }; 4FCA7FE309CA00D6AD54D270 /* RandWallet */ = { isa = PBXGroup; children = ( @@ -244,10 +278,28 @@ 976224CCD1E531D92268C700 /* Keychain.swift */, 183284CD1DA185232100F09D /* NoteStore.swift */, 4480C53C422CBF97386A8CCC /* Settings.swift */, + 86E9E25597C66480A5F84DD4 /* SignerStore.swift */, ); path = Storage; sourceTree = ""; }; + 7E383413DAB619B5913ACACC /* test */ = { + isa = PBXGroup; + children = ( + BB274F04CBA9322D8D170A0C /* fixtures */, + ); + path = test; + sourceTree = ""; + }; + 895921A51B9BEF8B77461DCB /* clients-msauth */ = { + isa = PBXGroup; + children = ( + 278307C39A8B7F50557DEB38 /* ui */, + ); + name = "clients-msauth"; + path = ..; + sourceTree = ""; + }; B55B89E759D89F59F7A38E2D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -256,6 +308,15 @@ name = Frameworks; sourceTree = ""; }; + BB274F04CBA9322D8D170A0C /* fixtures */ = { + isa = PBXGroup; + children = ( + 74FF2ECA6ED57507B4930099 /* amm-vectors.json */, + 0248590AF9D5BB554475B60D /* msauth */, + ); + path = fixtures; + sourceTree = ""; + }; CBBCC067DFAD651E498C43C4 /* RandWalletTests */ = { isa = PBXGroup; children = ( @@ -263,13 +324,18 @@ 51B8C97D2A4B42DF67451B26 /* ContactsTests.swift */, 3E9FD71335C8D0B8513015FE /* CoreSmokeTests.swift */, 5D73D5A77FFC38FC54259D79 /* DepositWalkTests.swift */, + C54A4FC4B625FCA03BD1809F /* FakeSignerSecrets.swift */, 06C8AADDBA61C258B1C39CD8 /* InvokeFlowTests.swift */, E39BB6F1188AB35E970520CF /* LinkHandoffTests.swift */, + 35025A8CB3E22690DF9731F2 /* LiveMsauthTests.swift */, 511FB7F3639F8196E229371D /* LiveSendTests.swift */, EFFC1D783C50E7F748573284 /* LiveSwapTests.swift */, + 79984AD66809D63D1FFF9B5E /* MsauthTests.swift */, F435046A1ED71DE0910C5DE8 /* NoteStoreTests.swift */, 2EFF95F847F91E6F0EED5358 /* ProverTests.swift */, 228AE584E190F486DBFBDEA4 /* SendLinkTests.swift */, + BBF4772C089EB50D9A35F795 /* SettingsMigrationTests.swift */, + E6F22E28C2D23B70ABE9070D /* SignScreenTests.swift */, F40B92E6506345D5C81599BF /* TypefaceTests.swift */, ); path = RandWalletTests; @@ -278,7 +344,7 @@ E151B23A84486190545F87A9 = { isa = PBXGroup; children = ( - 2C8AED69F5FC939BDE530407 /* i18n */, + 895921A51B9BEF8B77461DCB /* clients-msauth */, 4FCA7FE309CA00D6AD54D270 /* RandWallet */, CBBCC067DFAD651E498C43C4 /* RandWalletTests */, B55B89E759D89F59F7A38E2D /* Frameworks */, @@ -309,6 +375,8 @@ 676FE17810EFAC969314E1CB /* RootView.swift */, 356ED987BBE644E0AA7E6C60 /* SendView.swift */, 6F81980ED99AC808A372C7D3 /* SettingsView.swift */, + 9AB5F6B1A04E1129ED660E56 /* SignersView.swift */, + 159C1BE693B93053206A69E3 /* SignProposalView.swift */, CF55C3B3E2EBB1C8325F5E1E /* SwapView.swift */, BA51C5F8034D8DD473D82DDA /* Theme.swift */, 9FB8569A9BA22F2E98649199 /* WelcomeView.swift */, @@ -452,7 +520,18 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - FA7FBD630919B2848BAAC8C0 /* amm-vectors.json in Resources */, + BAAEDD51953A336B162E8D48 /* amm-vectors.json in Resources */, + 4FCE7819F36C9A8468D0CEF0 /* expected.json in Resources */, + E1CB9FBE512EB4B97978162F /* policy.json in Resources */, + 0031217ED8D8CD439DC321F6 /* proposal-expired.json in Resources */, + 33C1EFB5740A11FD6B9F18DB /* proposal-fee-raised.json in Resources */, + 3E051DFC1CBDFFAC0EC9FB82 /* proposal-substituted-recipient.json in Resources */, + 2DCDDEE19B7ABEC096929A6D /* proposal-tampered-output.json in Resources */, + DA810BA0429EEF41EC109EBE /* proposal-unbound.json in Resources */, + D5825C1587E0CF33C8AFDA0C /* proposal-valid.json in Resources */, + 7FACBE748A5C3EF19DB52ACD /* proposal-value-mismatch.json in Resources */, + 6A9228C8C3FE7ACEDFA3E9E8 /* proposal-wrong-expected.json in Resources */, + 918C1F9E9C679411B11532D0 /* signer-seeds.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -478,6 +557,7 @@ 25CA49B8860133B80AB64F0D /* LockView.swift in Sources */, F19AC0B40F8720FA62EA5EC1 /* Mark.swift in Sources */, 746BF8D8C153106BFEF2336F /* Models.swift in Sources */, + 69324B64FAC3E19759982AA7 /* Msauth.swift in Sources */, 73DE625E5E18560ECF227095 /* NoteStore.swift in Sources */, 2F82B858E4FD08B0BCEB5E5A /* ProverClient.swift in Sources */, 8FBEFEC8F08643962AD3C59B /* ProverPairingService.swift in Sources */, @@ -491,6 +571,9 @@ 8F6C36B95B6DDDCD64E799C2 /* SendView.swift in Sources */, 3565A1CB00F34196A7E73AE8 /* Settings.swift in Sources */, FE817779952D1666D69915AE /* SettingsView.swift in Sources */, + B22E5A9CC6A2ABA4117D4F1B /* SignProposalView.swift in Sources */, + 39287425DE900EEE0D991A18 /* SignerStore.swift in Sources */, + C1BA6A704D0A4F562E6E5A4D /* SignersView.swift in Sources */, 686393B1820A71E557A7A91C /* SwapView.swift in Sources */, 0EC3D5DBC6C8237CEAE39E80 /* Theme.swift in Sources */, CC5B54F04757CD85EAEDF97B /* WalletService.swift in Sources */, @@ -506,13 +589,18 @@ 76838E0AF20A51CF247552FA /* ContactsTests.swift in Sources */, 43D785D5C212AE422FE8BEE8 /* CoreSmokeTests.swift in Sources */, 66ACB9955A4ACD4DB0CBD929 /* DepositWalkTests.swift in Sources */, + 95D365B9F01F58B20A5A37CE /* FakeSignerSecrets.swift in Sources */, ACF8927BE6ED236F1EB1BD0A /* InvokeFlowTests.swift in Sources */, 71628504EFADE7B1A8B2E0A7 /* LinkHandoffTests.swift in Sources */, + BB4E77440A6361ED64BE14C3 /* LiveMsauthTests.swift in Sources */, FC6006023AFDB0DE7A54215D /* LiveSendTests.swift in Sources */, 39ECD2E0F8DAC40EE6BBE9D3 /* LiveSwapTests.swift in Sources */, + 6E48B8F2B23461EE61521E90 /* MsauthTests.swift in Sources */, 032203A43C3EB86266C74714 /* NoteStoreTests.swift in Sources */, 416476360AE7F708578387D0 /* ProverTests.swift in Sources */, 90AE055BC64ACE91BCA68904 /* SendLinkTests.swift in Sources */, + 71D3604178EBA21C8037BF5D /* SettingsMigrationTests.swift in Sources */, + F74310C7272C05AF1338805A /* SignScreenTests.swift in Sources */, 46A65DB167E5023BCA5DD83C /* TypefaceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/RandWallet/Core/CoreErrors.swift b/ios/RandWallet/Core/CoreErrors.swift index 1237b6c..1a6fc8a 100644 --- a/ios/RandWallet/Core/CoreErrors.swift +++ b/ios/RandWallet/Core/CoreErrors.swift @@ -64,6 +64,22 @@ enum CoreErrors { // version.prover_history_warning (ProverPairingService.warning) "This prover will be able to read this wallet's whole history — every payment received and sent, before and after today. It cannot spend. To keep your history private, run your own.": { String(localized: "This prover will be able to read this wallet's whole history — every payment received and sent, before and after today. It cannot spend. To keep your history private, run your own.") }, + // msauth_* (private multisig signing, fullnode randprotocol-msig) + "the transaction does not hash to the proposal's binding": { String(localized: "the transaction does not hash to the proposal's binding") }, + "the proposal's signing_message is not its binding's": { String(localized: "the proposal's signing_message is not its binding's") }, + "the proposal's auth_commit is not this policy's account": { String(localized: "the proposal's auth_commit is not this policy's account") }, + "the proposal's expected digest is not its bundle's": { String(localized: "the proposal's expected digest is not its bundle's") }, + "the transaction carries no bundle": { String(localized: "the transaction carries no bundle") }, + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": { String(localized: "the transaction carries an action; a private multisig proposal is a payment from its bundle alone") }, + "the witness's value cannot be checked: its nk or salt is not this proposal's": { String(localized: "the witness's value cannot be checked: its nk or salt is not this proposal's") }, + "this key is not one of the policy's signers": { String(localized: "this key is not one of the policy's signers") }, + "a share is index:hex": { String(localized: "a share is index:hex") }, + "a signer key is a rand-node keygen file or 64 hex characters": { String(localized: "a signer key is a rand-node keygen file or 64 hex characters") }, + "a signer seed is 64 hex characters": { String(localized: "a signer seed is 64 hex characters") }, + "expected_recipients must be a list of addresses": { String(localized: "expected_recipients must be a list of addresses") }, + "the key file's public_key is not the one its seed derives": { String(localized: "the key file's public_key is not the one its seed derives") }, + "the key file's address is not the one its seed derives": { String(localized: "the key file's address is not the one its seed derives") }, + "no expected recipient given: the payee was NOT checked": { String(localized: "no expected recipient given: the payee was NOT checked") }, ] // MARK: sentences with a variable part @@ -125,6 +141,34 @@ enum CoreErrors { Rule(#"this chain names an auth guest but a v1/v2 bundle guest \(([0-9a-fA-F]+)\); the node is misconfigured or lying — refusing to prove"#) { g in String(localized: "this chain names an auth guest but a v1/v2 bundle guest (\(g[0])); the node is misconfigured or lying — refusing to prove") }, + // msauth_* (private multisig signing) + Rule(#"this proposal is for another policy: it names ([0-9a-f]+), this policy is ([0-9a-f]+)"#) { g in + String(localized: "this proposal is for another policy: it names \(g[0]), this policy is \(g[1])") + }, + Rule(#"output (\d+) does not open as this account's: refusing to sign value it cannot see"#) { g in + String(localized: "output \(g[0]) does not open as this account's: refusing to sign value it cannot see") + }, + Rule(#"the witness's value does not match the transaction: input (\d+) does not nullify to the bundle's nullifier"#) { g in + String(localized: "the witness's value does not match the transaction: input \(g[0]) does not nullify to the bundle's nullifier") + }, + Rule(#"the witness's value does not match the outputs: asset (\d+) has (\d+) in and (\d+) out with the fee and burns"#) { g in + String(localized: "the witness's value does not match the outputs: asset \(g[0]) has \(g[1]) in and \(g[2]) out with the fee and burns") + }, + Rule(#"the proposal expires at (\d+), outside its bundle's window \((\d+), (\d+)\]"#) { g in + String(localized: "the proposal expires at \(g[0]), outside its bundle's window (\(g[1]), \(g[2])]") + }, + Rule(#"the transaction is for chain (\d+), the proposal says chain (\d+)"#) { g in + String(localized: "the transaction is for chain \(g[0]), the proposal says chain \(g[1])") + }, + Rule(#"signer (\d+)'s signature does not verify over this proposal"#) { g in + String(localized: "signer \(g[0])'s signature does not verify over this proposal") + }, + Rule(#"signer (\d+) is not in this policy \(it has (\d+) signers\)"#) { g in + String(localized: "signer \(g[0]) is not in this policy (it has \(g[1]) signers)") + }, + Rule(#"expected recipient (\S+): (.+)"#) { g in String(localized: "expected recipient \(g[0]): \(localized(g[1]))") }, + Rule(#"the policy does not parse: (.+)"#) { g in String(localized: "the policy does not parse: \(g[0])") }, + Rule(#"the proposal does not parse: (.+)"#) { g in String(localized: "the proposal does not parse: \(g[0])") }, Rule(#"proving failed: (.+)"#) { g in String(localized: "proving failed: \(g[0])") }, Rule(#"proving the spend authorisation failed: (.+)"#) { g in String(localized: "proving the spend authorisation failed: \(g[0])") }, Rule(#"proving the call failed: (.+)"#) { g in String(localized: "proving the call failed: \(g[0])") }, diff --git a/ios/RandWallet/Core/Msauth.swift b/ios/RandWallet/Core/Msauth.swift new file mode 100644 index 0000000..28a41ef --- /dev/null +++ b/ios/RandWallet/Core/Msauth.swift @@ -0,0 +1,413 @@ +import Foundation + +// Private multisig signing (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md §3, +// §4): the core's `msauth_*` replies, the signing rules every app shares (`MsauthRules`, the same +// decisions as ui/engine/signers.js `verdict`) and the order a signature is made in +// (`MsauthSigning`). + +/// One output of `msauth_check`'s reply (wallet-core msauth.rs). `amount` is a decimal string; +/// `matches` is the crate's `"
( RAND)"` when a named payee matched. +struct MsauthOutput: Equatable { + let ownerFingerprint: String + let toAccount: Bool + let amount: String + let asset: UInt32 + let memo: String + let matches: String? + let needsPayee: Bool +} + +/// One intended-payment entry as the caller passed it and its address's owner fingerprint (nil +/// when the address does not parse). +struct MsauthExpected: Equatable { + let entry: String + let fingerprint: String? +} + +/// `msauth_check`'s reply, kept as the JSON the core gave (`json`) with typed views for a screen. +/// The views are lenient — a field that is missing or of the wrong type reads as nil or is left +/// out — because `MsauthRules.verdict` judges `json` itself and refuses a malformed reply +/// (`.malformedSummary`); nothing here is a reason to sign. `lines` is fullnode's text, shown +/// verbatim, never translated. +struct MsauthSummary { + let json: Any? + + init(json: Any?) { self.json = json } + + private var object: [String: Any] { json as? [String: Any] ?? [:] } + + var recipientsChecked: Bool { MsauthJSON.bool(object["recipients_checked"]) == true } + var chainId: UInt64? { MsauthJSON.uint(object["chain_id"]) } + var expiresAt: UInt64? { MsauthJSON.uint(object["expires_at"]) } + var bindingDomain: UInt64? { MsauthJSON.uint(object["binding_domain"]) } + var genesis: String? { object["genesis"] as? String } + var policyFingerprint: String? { object["policy_fingerprint"] as? String } + var signingMessage: String? { object["signing_message"] as? String } + var fee: String? { object["fee"] as? String } + var lines: [String] { (object["lines"] as? [Any])?.compactMap { $0 as? String } ?? [] } + var outputs: [MsauthOutput] { + ((object["outputs"] as? [Any]) ?? []).compactMap { item in + guard let o = item as? [String: Any], let toAccount = MsauthJSON.bool(o["to_account"]), + let amount = MsauthJSON.amount(o["amount"]) else { return nil } + return MsauthOutput(ownerFingerprint: o["owner_fingerprint"] as? String ?? "", toAccount: toAccount, amount: amount, + asset: MsauthJSON.uint(o["asset"]).flatMap { UInt32(exactly: $0) } ?? 0, + memo: o["memo"] as? String ?? "", matches: o["matches"] as? String, + needsPayee: MsauthJSON.bool(o["needs_payee"]) ?? false) + } + } + var expected: [MsauthExpected] { + ((object["expected"] as? [Any]) ?? []).compactMap { item in + guard let x = item as? [String: Any], let entry = x["entry"] as? String else { return nil } + return MsauthExpected(entry: entry, fingerprint: x["fingerprint"] as? String) + } + } +} + +struct MsauthSigner: Decodable { + let seed: String + let publicKey: String + let address: String + let index: Int? + enum CodingKeys: String, CodingKey { case seed, address, index, publicKey = "public_key" } +} + +struct MsauthPolicyInfo: Decodable, Equatable { + let fingerprint: String + let threshold: Int + let signers: Int + let address: String + let policyHash: String + enum CodingKeys: String, CodingKey { case fingerprint, threshold, signers, address, policyHash = "policy_hash" } +} + +enum MsauthVerdict: Equatable { + case signable(blocksLeft: UInt64) + case malformedSummary + case uncheckedRecipient + case noNode(url: String) + case wrongChain(proposal: UInt64, wallet: UInt64) + case wrongGenesis + case expired(head: UInt64, expiresAt: UInt64) + + var isSignable: Bool { if case .signable = self { return true } else { return false } } + + /// The refusal in the user's language (the same English as ui/engine/signers.js); nil when + /// signable. + var message: String? { + switch self { + case .signable: return nil + case .malformedSummary: + return String(localized: "The proposal check returned an incomplete answer, so this proposal is not signed.") + case .uncheckedRecipient: + return String(localized: "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.") + case .noNode(let url): + let u = url.isEmpty ? "—" : url + return String(localized: "No node answered at \(u), so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.") + case .wrongChain(let proposal, let wallet): + return String(localized: "This proposal is for chain \(String(proposal)); this wallet is on chain \(String(wallet)).") + case .wrongGenesis: + return String(localized: "This proposal is bound to another genesis than the one this wallet's node serves.") + case .expired(let head, let expiresAt): + return String(localized: "This proposal expires at height \(String(expiresAt)) and the chain is at \(String(head)): too late to sign. Ask for a new proposal.") + } + } +} + +/// A proposal that may not be signed: the verdict that refused it. +struct MsauthRefusal: LocalizedError, Equatable { + let verdict: MsauthVerdict + var errorDescription: String? { verdict.message } +} + +/// Reading `JSONSerialization`'s values strictly: a JSON `true` is an `NSNumber` too, so a Bool and +/// a number are told apart by their CoreFoundation type, never by a cast. +enum MsauthJSON { + /// JavaScript's `Number.MAX_SAFE_INTEGER`, the bound ui/engine/signers.js applies. + static let maxSafeInteger: Double = 9_007_199_254_740_991 + + static func isBool(_ v: Any?) -> Bool { + guard let n = v as? NSNumber else { return false } + return CFGetTypeID(n) == CFBooleanGetTypeID() + } + static func bool(_ v: Any?) -> Bool? { + guard isBool(v), let n = v as? NSNumber else { return nil } + return n.boolValue + } + /// A non-negative safe integer: a JSON number, not a Bool, integral, 0...2^53-1. + static func uint(_ v: Any?) -> UInt64? { + guard !isBool(v), !(v is String), let n = v as? NSNumber else { return nil } + let d = n.doubleValue + guard d.isFinite, d >= 0, d <= maxSafeInteger, d.rounded() == d else { return nil } + return UInt64(d) + } + /// An amount: a string of ASCII digits (whitespace around allowed), or a non-negative safe + /// integer. Returned trimmed, as text. + static func amount(_ v: Any?) -> String? { + if let s = v as? String { + let t = s.trimmingCharacters(in: .whitespacesAndNewlines) + return !t.isEmpty && t.unicodeScalars.allSatisfy({ ("0"..."9").contains($0) }) ? t : nil + } + return uint(v).map { String($0) } + } + static func isZeroAmount(_ v: Any?) -> Bool { + if let s = v as? String { + let t = s.trimmingCharacters(in: .whitespacesAndNewlines) + return !t.isEmpty && t.allSatisfy { $0 == "0" } + } + return uint(v) == 0 + } + /// An asset id as ui/engine/signers.js reads it (`Number(o.asset)`): a number or digit string. + static func asset(_ v: Any?) -> UInt64? { + if let s = v as? String { return amount(s).flatMap { UInt64($0) } } + return uint(v) + } + static func isHex(_ s: String) -> Bool { + !s.isEmpty && s.unicodeScalars.allSatisfy { ("0"..."9").contains($0) || ("a"..."f").contains($0) || ("A"..."F").contains($0) } + } +} + +/// The signing rules every app shares (spec §4): the payee named and matched with its amount, the +/// CLI's 16-block margin, no node no signature, the proposal's chain and genesis against this +/// wallet's node, the fingerprint's last four. The same decisions, in the same order, as +/// ui/engine/signers.js `verdict`. +enum MsauthRules { + /// Blocks a proposal needs before its window closes: the CLI's `EXPIRY_MARGIN`. + static let expiryMargin: UInt64 = 16 + + /// A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. + static func clean(_ s: String) -> String { + var t = s + if t.hasPrefix("\u{FEFF}") { t.removeFirst() } + return t.trimmingCharacters(in: .whitespacesAndNewlines) + } + + /// The intended payments as the core is given them: each entry cleaned, none dropped. + static func payees(_ list: [String]) -> [String] { list.map(clean) } + + /// An entry that is empty or has no address before its amount. + static func isBlank(_ entry: String) -> Bool { + let t = clean(entry) + return t.isEmpty || t.hasPrefix(":") + } + + /// An entry without an amount: no `:`, or nothing after the first one. + static func isAmountless(_ entry: String) -> Bool { + guard let i = entry.firstIndex(of: ":") else { return true } + return entry[entry.index(after: i)...].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + /// Of the paying outputs (raw JSON), the owner fingerprints paid a non-RAND asset and those paid + /// RAND. An output whose asset is missing or unreadable counts as RAND (fails closed). + static func payeeAssets(_ paying: [[String: Any]]) -> (token: Set, rand: Set) { + var token = Set(), rand = Set() + for o in paying { + guard let fp = o["owner_fingerprint"] as? String, !fp.isEmpty else { continue } + if let asset = MsauthJSON.asset(o["asset"]), asset != 0 { token.insert(fp) } else { rand.insert(fp) } + } + return (token, rand) + } + + /// Whether a checked proposal may be signed now, on this wallet's chain. `recipients` is the + /// list the check was given (`payees`), `head` nil when no node answered. + static func verdict(summary: MsauthSummary, head: UInt64?, chainId: UInt64, genesis: String?, url: String, + recipients: [String]) -> MsauthVerdict { + guard let s = summary.json as? [String: Any] else { return .malformedSummary } + // Never sign on an unchecked payee (a coordinator could pay its own key). + guard MsauthJSON.bool(s["recipients_checked"]) == true else { return .uncheckedRecipient } + if recipients.contains(where: isBlank) { return .uncheckedRecipient } + // The review shows `lines` verbatim: missing, or anything but an array of strings, is malformed. + guard let lines = s["lines"] as? [Any], lines.allSatisfy({ $0 is String }) else { return .malformedSummary } + guard let outs = s["outputs"] as? [Any] else { return .malformedSummary } + var outputs: [[String: Any]] = [] + for item in outs { + guard let o = item as? [String: Any], MsauthJSON.bool(o["to_account"]) != nil, MsauthJSON.amount(o["amount"]) != nil else { + return .malformedSummary + } + outputs.append(o) + } + let paying = outputs.filter { MsauthJSON.bool($0["to_account"]) == false && !MsauthJSON.isZeroAmount($0["amount"]) } + // No intended payment named at all while something pays out: unchecked, whatever the core said. + if recipients.isEmpty && !paying.isEmpty { return .uncheckedRecipient } + // An amount is pinned on every entry, except an entry whose own owner fingerprint owns a + // paying non-RAND output and NO paying RAND output (the core refuses a pin on a non-RAND + // output). Per row: one small non-RAND output cannot unpin the RAND rows, nor the RAND paid + // to its own payee (1 unit of a token beside an inflated RAND output is refused). + let amountless = recipients.filter(isAmountless) + if !amountless.isEmpty { + guard let exp = s["expected"] as? [Any] else { return .uncheckedRecipient } + let (tokenOwners, randOwners) = payeeAssets(paying) + let allowed = { (entry: String) -> Bool in + exp.contains { item in + guard let x = item as? [String: Any], x["entry"] as? String == entry, + let fp = x["fingerprint"] as? String, !fp.isEmpty else { return false } + return tokenOwners.contains(fp) && !randOwners.contains(fp) + } + } + if !amountless.allSatisfy(allowed) { return .uncheckedRecipient } + } + let unmatched = paying.contains { o in + guard let m = o["matches"] as? String else { return true } + return m.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + if unmatched { return .uncheckedRecipient } + guard let expiresAt = MsauthJSON.uint(s["expires_at"]), let proposalChain = MsauthJSON.uint(s["chain_id"]), + let domain = MsauthJSON.uint(s["binding_domain"]), + let proposalGenesis = s["genesis"] as? String, MsauthJSON.isHex(proposalGenesis) else { + return .malformedSummary + } + guard let head else { return .noNode(url: url) } + if proposalChain != chainId { return .wrongChain(proposal: proposalChain, wallet: chainId) } + if domain >= 1 && proposalGenesis.lowercased() != (genesis ?? "").lowercased() { return .wrongGenesis } + // head + 16 >= expires_at, written so it cannot overflow. + if head >= expiresAt || expiresAt - head <= expiryMargin { + return .expired(head: head, expiresAt: expiresAt) + } + return .signable(blocksLeft: expiresAt - head) + } + + /// The fingerprint's last four characters, typed: case, dashes and every whitespace ignored (as JS). + static func confirms(fingerprint: String, typed: String) -> Bool { + let want = String(fingerprint.replacingOccurrences(of: "-", with: "").suffix(4)).lowercased() + let got = String(typed.unicodeScalars.filter { $0 != "-" && !CharacterSet.whitespacesAndNewlines.contains($0) }.map(Character.init)).lowercased() + return want.count == 4 && got == want + } +} + +// MARK: the order a signature is made in + +/// What `MsauthSigning` asks of the node this wallet uses. `RpcClient` is one. +protocol MsauthNode { + var nodeURL: String { get } + func chainId() async throws -> UInt64 + func headHeight() async throws -> UInt64 + func genesisHash() async throws -> String? +} + +extension RpcClient: MsauthNode { + var nodeURL: String { url.absoluteString } +} + +/// The two core calls a signature depends on; `RandMsauthCore` is the real one. +protocol MsauthCore { + func check(proposal: String, policy: String, expectedRecipients: [String]) throws -> MsauthSummary + func sign(proposal: String, policy: String, seed: Data, expectedRecipients: [String]) throws -> String +} + +struct RandMsauthCore: MsauthCore { + func check(proposal: String, policy: String, expectedRecipients: [String]) throws -> MsauthSummary { + try RandCore.msauthCheck(proposal: proposal, policy: policy, expectedRecipients: expectedRecipients) + } + func sign(proposal: String, policy: String, seed: Data, expectedRecipients: [String]) throws -> String { + try RandCore.msauthSign(proposal: proposal, policy: policy, seed: seed, expectedRecipients: expectedRecipients) + } +} + +struct MsauthCheck { + /// nil when the entries were refused before the core was asked. + let summary: MsauthSummary? + let verdict: MsauthVerdict +} + +struct MsauthShare: Equatable { + let share: String + let index: Int +} + +/// Check, then sign, for one signer profile: the proposal is a parameter and never stored (it +/// carries the account's witness). `node` nil is "no node": the proposal is shown, never signed. +struct MsauthSigning { + let store: SignerStore + let core: MsauthCore + let node: MsauthNode? + /// This wallet's chain (Settings). + let chainId: UInt64 + + /// The node's head, from a node on this wallet's chain; any failure is `.noNode`. + private func judge(_ summary: MsauthSummary, _ payees: [String]) async -> MsauthVerdict { + guard let node else { + return MsauthRules.verdict(summary: summary, head: nil, chainId: chainId, genesis: nil, url: "", recipients: payees) + } + let url = node.nodeURL + do { + guard try await node.chainId() == chainId else { + return MsauthRules.verdict(summary: summary, head: nil, chainId: chainId, genesis: nil, url: url, recipients: payees) + } + let head = try await node.headHeight() + // A height past 2^53-1 is no answer a node gives honestly (the JS rule's safe-integer bound). + guard head <= 9_007_199_254_740_991 else { + return MsauthRules.verdict(summary: summary, head: nil, chainId: chainId, genesis: nil, url: url, recipients: payees) + } + let genesis = try await node.genesisHash() + return MsauthRules.verdict(summary: summary, head: head, chainId: chainId, genesis: genesis, url: url, recipients: payees) + } catch { + return MsauthRules.verdict(summary: summary, head: nil, chainId: chainId, genesis: nil, url: url, recipients: payees) + } + } + + /// The core's check with the intended payments, and the verdict on it now. A blank entry is + /// refused before the core is asked (it would otherwise be dropped or misread). + func check(id: String, proposal: String, expectedRecipients: [String]) async throws -> MsauthCheck { + let policy = try store.policy(id) + let payees = MsauthRules.payees(expectedRecipients) + if payees.contains(where: MsauthRules.isBlank) { return MsauthCheck(summary: nil, verdict: .uncheckedRecipient) } + let summary = try core.check(proposal: MsauthRules.clean(proposal), policy: policy, expectedRecipients: payees) + return MsauthCheck(summary: summary, verdict: await judge(summary, payees)) + } + + /// The share, or the refusal (`MsauthRefusal`, or the core's error). The order: check and + /// judge; read the seed (the system asks for Face ID / Touch ID / the passcode); re-read the + /// head and judge again, since the prompt can take as long as the person likes; sign with the + /// same list the check used, so the core re-runs the payee check on exactly what was judged. + /// The seed buffer is zeroed on every exit (`SignerStore.withSeedAsync`). + func sign(id: String, proposal: String, expectedRecipients: [String], reason: String) async throws -> MsauthShare { + let policy = try store.policy(id) + let text = MsauthRules.clean(proposal) + let payees = MsauthRules.payees(expectedRecipients) + if payees.contains(where: MsauthRules.isBlank) { throw MsauthRefusal(verdict: .uncheckedRecipient) } + let summary = try core.check(proposal: text, policy: policy, expectedRecipients: payees) + let first = await judge(summary, payees) + guard first.isSignable else { throw MsauthRefusal(verdict: first) } + // Leaving the screen cancels this task: stop before the Face ID prompt and before signing. + try Task.checkCancellation() + let share = try await store.withSeedAsync(id, reason: reason) { seed in + let again = await judge(summary, payees) + guard again.isSignable else { throw MsauthRefusal(verdict: again) } + try Task.checkCancellation() + return try core.sign(proposal: text, policy: policy, seed: seed, expectedRecipients: payees) + } + guard let head = share.split(separator: ":").first, let index = Int(head.trimmingCharacters(in: .whitespaces)) else { + throw RandCore.CoreError(message: "msauth_sign returned no share") + } + return MsauthShare(share: share, index: index) + } + + /// Whether another signer's share verifies over this proposal; throws the core's refusal. + func verifyShare(id: String, proposal: String, share: String) throws { + try RandCore.msauthVerifyShare(proposal: MsauthRules.clean(proposal), policy: try store.policy(id), share: MsauthRules.clean(share)) + } +} + +extension Data { + /// Bytes from hex (an even count of hex digits), or nil. + init?(hex: String) { + let scalars = Array(hex.unicodeScalars) + guard scalars.count % 2 == 0 else { return nil } + var out = Data(capacity: scalars.count / 2) + var i = 0 + while i < scalars.count { + guard let hi = Data.nibble(scalars[i]), let lo = Data.nibble(scalars[i + 1]) else { return nil } + out.append(hi << 4 | lo) + i += 2 + } + self = out + } + private static func nibble(_ c: Unicode.Scalar) -> UInt8? { + switch c.value { + case 48...57: return UInt8(c.value - 48) + case 97...102: return UInt8(c.value - 87) + case 65...70: return UInt8(c.value - 55) + default: return nil + } + } + var hexString: String { map { String(format: "%02x", $0) }.joined() } +} diff --git a/ios/RandWallet/Core/RandCore.swift b/ios/RandWallet/Core/RandCore.swift index 6f2d4f6..3f591a0 100644 --- a/ios/RandWallet/Core/RandCore.swift +++ b/ios/RandWallet/Core/RandCore.swift @@ -152,6 +152,41 @@ enum RandCore { try call("finish_proof", ["pending": pending, "reply_hex": replyHex], as: InvokeResult.self) } + // MARK: private multisig signing (wallet-core msauth.rs) + + /// The proposal checked against the policy and the intended payments (`rand1…:AMOUNT` each, + /// AMOUNT in RAND display text; address alone only for a non-RAND payment). Empty means no + /// payee named: the reply then says `recipients_checked: false` and must not be signed. + /// `MsauthRules.verdict` judges the reply; nothing here decides whether to sign. + static func msauthCheck(proposal: String, policy: String, expectedRecipients: [String] = []) throws -> MsauthSummary { + MsauthSummary(json: try call("msauth_check", ["proposal": proposal, "policy": policy, "expected_recipients": expectedRecipients])) + } + /// The share `"i:hex"`. The core re-runs every check with `expectedRecipients` (pass the list + /// the check used) and refuses an empty list when an output pays someone. `seed` is sent as a + /// hex String inside the JSON parameters, which cannot be wiped; the core wipes its own copy, + /// and the caller wipes `seed` (SignerStore.withSeed does). + static func msauthSign(proposal: String, policy: String, seed: Data, expectedRecipients: [String]) throws -> String { + let params: [String: Any] = ["proposal": proposal, "policy": policy, "seed": seed.hexString, "expected_recipients": expectedRecipients] + guard let share = try call("msauth_sign", params) as? String else { + throw CoreError(message: "msauth_sign returned no share") + } + return share + } + /// Returns when `share` (`i:hex`) verifies over the proposal; throws the core's refusal. + static func msauthVerifyShare(proposal: String, policy: String, share: String) throws { + _ = try call("msauth_verify_share", ["proposal": proposal, "policy": policy, "share": share]) + } + /// A `rand-node keygen` file or 64 hex: its seed, key, address and — with `policy` — its + /// position there (a key outside the policy is refused). The reply carries the seed. + static func msauthSignerImport(_ input: String, policy: String?) throws -> MsauthSigner { + var params: [String: Any] = ["input": input] + if let policy { params["policy"] = policy } + return try call("msauth_signer_import", params, as: MsauthSigner.self) + } + static func msauthPolicyInfo(_ policy: String) throws -> MsauthPolicyInfo { + try call("msauth_policy_info", ["policy": policy], as: MsauthPolicyInfo.self) + } + static func formatAmount(units: String) throws -> String { try call("format_amount", ["units": units]) as? String ?? units } diff --git a/ios/RandWallet/Localizable.xcstrings b/ios/RandWallet/Localizable.xcstrings index 16ac855..9ad05f5 100644 --- a/ios/RandWallet/Localizable.xcstrings +++ b/ios/RandWallet/Localizable.xcstrings @@ -53775,6 +53775,11414 @@ } } } + }, + "the transaction does not hash to the proposal's binding" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the transaction does not hash to the proposal's binding" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجزئة المعاملة ليست ارتباط الاقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Hash der Transaktion ist nicht die Bindung des Vorschlags" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el hash de la transacción no es la vinculación de la propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "هش تراکنش همان binding پیشنهاد نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "le hash de la transaction n'est pas la liaison de la proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लेन-देन का हैश प्रस्ताव की बाइंडिंग का नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "hash transaksi bukan binding proposal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transazione non ha come hash il binding della proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランザクションのハッシュが提案のバインディングと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "트랜잭션의 해시가 제안의 바인딩과 다릅니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "hash transaksi bukan binding cadangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "skrót transakcji nie jest powiązaniem propozycji" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د راکړې ورکړې hash د وړاندیز binding نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o hash da transação não é o vínculo da proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "хеш транзакции — не привязка предложения" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பரிவர்த்தனையின் ஹாஷ் முன்மொழிவின் binding அல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "لین دین کا ہیش تجویز کی بائنڈنگ کا نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易的哈希不是提案的绑定" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易的雜湊不是提案的綁定" + } + } + } + }, + "the proposal's signing_message is not its binding's" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the proposal's signing_message is not its binding's" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة signing_message في الاقتراح ليست قيمة ارتباطه" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die signing_message des Vorschlags ist nicht die seiner Bindung" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el signing_message de la propuesta no es el de su vinculación" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "signing_message پیشنهاد همان signing_message مربوط به binding آن نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "le signing_message de la proposition n'est pas celui de sa liaison" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव का signing_message उसकी बाइंडिंग का नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "signing_message proposal bukan milik binding-nya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il signing_message della proposta non è quello del suo binding" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案の signing_message が、そのバインディングのものと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안의 signing_message가 바인딩의 것과 다릅니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "signing_message cadangan bukan milik binding-nya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "signing_message propozycji nie jest signing_message jej powiązania" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د وړاندیز signing_message د هغه د binding نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o signing_message da proposta não é o do seu vínculo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "signing_message предложения — не signing_message его привязки" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவின் signing_message அதன் binding-இனுடையது அல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز کا signing_message اس کی بائنڈنگ کا نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的 signing_message 与其绑定的不一致" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的 signing_message 與其綁定的不一致" + } + } + } + }, + "the proposal's auth_commit is not this policy's account" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the proposal's auth_commit is not this policy's account" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة auth_commit في الاقتراح ليست حساب هذه السياسة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "das auth_commit des Vorschlags ist nicht das Konto dieser Richtlinie" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el auth_commit de la propuesta no es la cuenta de esta política" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "auth_commit پیشنهاد حساب این سیاست نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'auth_commit de la proposition n'est pas le compte de cette politique" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव का auth_commit इस पॉलिसी का खाता नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "auth_commit proposal bukan akun kebijakan ini" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'auth_commit della proposta non è l'account di questa policy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案の auth_commit が、このポリシーのアカウントと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안의 auth_commit이 이 정책의 계정이 아닙니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "auth_commit cadangan bukan akaun dasar ini" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "auth_commit propozycji nie jest kontem tej polityki" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د وړاندیز auth_commit د دې پالیسۍ حساب نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o auth_commit da proposta não é a conta desta política" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "auth_commit предложения — не аккаунт этой политики" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவின் auth_commit இந்தக் கொள்கையின் கணக்கு அல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز کا auth_commit اس پالیسی کا اکاؤنٹ نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的 auth_commit 不是此策略的账户" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的 auth_commit 不是此政策的帳戶" + } + } + } + }, + "the proposal's expected digest is not its bundle's" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the proposal's expected digest is not its bundle's" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الملخّص (digest) المتوقع للاقتراح ليس ملخّص حزمته" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der erwartete Digest des Vorschlags ist nicht der seines Bundles" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el digest esperado de la propuesta no es el de su bundle" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "digest مورد انتظار پیشنهاد همان digest مربوط به bundle آن نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "le digest attendu de la proposition n'est pas celui de son lot" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव का अपेक्षित डाइजेस्ट उसके बंडल का नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "digest yang diharapkan proposal bukan milik bundle-nya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il digest atteso della proposta non è quello del suo bundle" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案の想定ダイジェストが、そのバンドルのものと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안의 예상 다이제스트가 번들의 것과 다릅니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "digest dijangka cadangan bukan milik bundle-nya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "oczekiwany digest propozycji nie jest digestem jej bundle" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د وړاندیز تمه شوی digest د هغه د bundle نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o digest esperado da proposta não é o do seu bundle" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ожидаемый digest предложения — не digest его bundle" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவின் எதிர்பார்க்கப்படும் digest அதன் bundle-இனுடையது அல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز کا متوقع ڈائجسٹ اس کے بنڈل کا نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的预期摘要与其捆绑包的不一致" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案的預期摘要與其捆綁包的不一致" + } + } + } + }, + "the transaction carries no bundle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the transaction carries no bundle" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا تحمل المعاملة أي حزمة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Transaktion enthält kein Bundle" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transacción no lleva ningún bundle" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تراکنش هیچ bundle ای ندارد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transaction ne porte aucun lot" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लेन-देन में कोई बंडल नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi tidak membawa bundle" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transazione non porta alcun bundle" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランザクションにバンドルが含まれていません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "트랜잭션에 번들이 없습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi tidak membawa bundle" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "transakcja nie zawiera bundle" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "راکړه ورکړه هیڅ bundle نه لري" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a transação não carrega nenhum bundle" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "транзакция не несёт bundle" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பரிவர்த்தனையில் bundle எதுவும் இல்லை" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "لین دین میں کوئی بنڈل نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易不含捆绑包" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易不含捆綁包" + } + } + } + }, + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the transaction carries an action; a private multisig proposal is a payment from its bundle alone" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحمل المعاملة إجراءً؛ والاقتراح الخاص متعدد التوقيع هو دفعة من حزمته وحدها" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Transaktion enthält eine Aktion; ein privater Multisig-Vorschlag ist allein eine Zahlung aus seinem Bundle" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transacción lleva una acción; una propuesta multifirma privada es un pago solo desde su bundle" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تراکنش یک اقدام دارد؛ پیشنهاد چندامضایی خصوصی فقط پرداختی از bundle خودش است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transaction porte une action ; une proposition multisig privée est un paiement depuis son seul lot" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लेन-देन में एक एक्शन है; निजी मल्टीसिग प्रस्ताव केवल अपने बंडल से किया गया भुगतान होता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi membawa sebuah aksi; proposal multisig privat adalah pembayaran dari bundle-nya saja" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transazione porta un'azione; una proposta multisig privata è un pagamento dal solo suo bundle" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランザクションにアクションが含まれています。プライベートなマルチシグ提案は、そのバンドルだけからの支払いです" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "트랜잭션에 액션이 포함되어 있습니다. 비공개 멀티시그 제안은 번들만으로 이루어진 결제입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi membawa satu tindakan; cadangan multisig persendirian ialah pembayaran daripada bundle-nya sahaja" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "transakcja zawiera akcję; prywatna propozycja multisig to płatność wyłącznie z jej bundle" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "راکړه ورکړه یو عمل لري؛ خصوصي ملټي‌سېګ وړاندیز یوازې د خپل bundle څخه تادیه ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a transação carrega uma ação; uma proposta multiassinatura privada é um pagamento apenas do seu bundle" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "транзакция несёт действие; приватное мультиподписное предложение — это платёж только из его bundle" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பரிவர்த்தனை ஒரு செயலைக் கொண்டுள்ளது; தனிப்பட்ட மல்டிசிக் முன்மொழிவு என்பது அதன் bundle-இலிருந்து மட்டும் செய்யும் கட்டணம்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "لین دین میں ایک عمل ہے؛ نجی ملٹی سگ تجویز صرف اپنے بنڈل سے ادائیگی ہوتی ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易带有一个动作;私密多签提案只是仅来自其捆绑包的付款" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易帶有一個動作;私密多重簽署提案只是僅來自其捆綁包的付款" + } + } + } + }, + "the witness's value cannot be checked: its nk or salt is not this proposal's" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the witness's value cannot be checked: its nk or salt is not this proposal's" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن التحقق من قيمة الشاهد (witness): قيمة nk أو الملح فيه ليست لهذا الاقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Wert des Witness lässt sich nicht prüfen: sein nk oder Salt gehört nicht zu diesem Vorschlag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "no se puede comprobar el valor del witness: su nk o su salt no son los de esta propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار witness قابل بررسی نیست: nk یا salt آن مال این پیشنهاد نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la valeur du witness ne peut pas être vérifiée : son nk ou son sel n'est pas celui de cette proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "विटनेस का मान जाँचा नहीं जा सकता: उसका nk या सॉल्ट इस प्रस्ताव का नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak dapat diperiksa: nk atau salt-nya bukan milik proposal ini" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il valore del witness non si può verificare: il suo nk o il suo salt non sono quelli di questa proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウィットネスの値を確認できません: その nk またはソルトがこの提案のものではありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "증인(witness) 값을 확인할 수 없습니다: nk 또는 솔트가 이 제안의 것이 아닙니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak dapat disemak: nk atau salt-nya bukan milik cadangan ini" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wartości witness nie można sprawdzić: jego nk lub salt nie pochodzi z tej propozycji" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د witness ارزښت نشي څېړل کېدای: د هغه nk یا salt د دې وړاندیز نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o valor do witness não pode ser verificado: seu nk ou salt não é o desta proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "значение witness проверить нельзя: его nk или salt — не от этого предложения" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "witness-இன் மதிப்பைச் சரிபார்க்க முடியவில்லை: அதன் nk அல்லது salt இந்த முன்மொழிவினுடையது அல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ویٹنس کی قدر جانچی نہیں جا سکتی: اس کا nk یا سالٹ اس تجویز کا نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法核对见证数据的金额:其 nk 或盐值不属于此提案" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法核對見證資料的金額:其 nk 或鹽值不屬於此提案" + } + } + } + }, + "this key is not one of the policy's signers" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "this key is not one of the policy's signers" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا المفتاح ليس من موقّعي السياسة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "dieser Schlüssel gehört nicht zu den Unterzeichnern der Richtlinie" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "esta clave no es de ninguno de los firmantes de la política" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این کلید یکی از امضاکنندگان سیاست نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "cette clé n'est pas l'un des signataires de la politique" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह की पॉलिसी के हस्ताक्षरकर्ताओं में से नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "kunci ini bukan salah satu penanda tangan kebijakan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "questa chiave non è uno dei firmatari della policy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このキーはポリシーの署名者ではありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 키는 정책의 서명자가 아닙니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "kunci ini bukan salah satu penandatangan dasar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ten klucz nie należy do sygnatariuszy polityki" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا کیلي د پالیسۍ له لاسلیک کوونکو څخه نه ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "esta chave não é de nenhum dos signatários da política" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "этот ключ не входит в число подписантов политики" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த விசை கொள்கையின் கையொப்பமிடுபவர்களில் ஒன்றல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ کی پالیسی کے دستخط کنندگان میں سے نہیں ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此密钥不属于该策略的签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此金鑰不屬於該政策的簽署人" + } + } + } + }, + "a share is index:hex" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "a share is index:hex" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حصة التوقيع بصيغة index:hex" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "ein Signaturanteil ist index:hex" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "una parte de firma es index:hex" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "سهم امضا به شکل index:hex است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "une part de signature est index:hex" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षर अंश index:hex रूप में होता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "sebuah bagian tanda tangan berformat index:hex" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "una quota di firma è index:hex" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名シェアは index:hex 形式です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명 조각은 index:hex 형식입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "bahagian tandatangan ialah index:hex" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "udział podpisu ma postać index:hex" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک برخه index:hex ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "uma parte da assinatura é index:hex" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "доля подписи имеет вид index:hex" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பப் பங்கு index:hex வடிவில் இருக்கும்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخطی حصہ index:hex ہوتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名份额的格式为 index:hex" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署份額的格式為 index:hex" + } + } + } + }, + "a signer key is a rand-node keygen file or 64 hex characters" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "a signer key is a rand-node keygen file or 64 hex characters" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مفتاح الموقّع هو ملف keygen من rand-node أو 64 حرفًا سداسيًا عشريًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "ein Unterzeichner-Schlüssel ist eine rand-node-Keygen-Datei oder 64 Hex-Zeichen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "una clave de firmante es un archivo keygen de rand-node o 64 caracteres hexadecimales" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "کلید امضاکننده یک فایل keygen از rand-node یا ۶۴ نویسه هگز است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "une clé de signataire est un fichier keygen de rand-node ou 64 caractères hexadécimaux" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता की एक rand-node keygen फ़ाइल या 64 हेक्स अक्षर होती है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "kunci penanda tangan adalah berkas keygen rand-node atau 64 karakter hex" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "una chiave di firmatario è un file keygen di rand-node o 64 caratteri esadecimali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者キーは rand-node の keygen ファイル、または 64 桁の16進文字です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 키는 rand-node keygen 파일 또는 64자리 16진수 문자입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "kunci penandatangan ialah fail keygen rand-node atau 64 aksara hex" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "klucz sygnatariusza to plik keygen rand-node lub 64 znaki szesnastkowe" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي کیلي د rand-node keygen فایل یا ۶۴ هېکس توري دي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "uma chave de signatário é um arquivo keygen do rand-node ou 64 caracteres hexadecimais" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ключ подписанта — это файл keygen от rand-node или 64 шестнадцатеричных символа" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் விசை என்பது rand-node keygen கோப்பு அல்லது 64 ஹெக்ஸ் எழுத்துகள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کی ایک rand-node keygen فائل یا 64 ہیکس حروف ہوتی ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人密钥是 rand-node 的 keygen 文件或 64 位十六进制字符" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人金鑰是 rand-node 的 keygen 檔案或 64 位十六進制字元" + } + } + } + }, + "a signer seed is 64 hex characters" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "a signer seed is 64 hex characters" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بذرة الموقّع هي 64 حرفًا سداسيًا عشريًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "ein Unterzeichner-Seed besteht aus 64 Hex-Zeichen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "una semilla de firmante son 64 caracteres hexadecimales" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "seed امضاکننده ۶۴ نویسه هگز است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "une graine de signataire compte 64 caractères hexadécimaux" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता का सीड 64 हेक्स अक्षरों का होता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "seed penanda tangan terdiri dari 64 karakter hex" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "un seed di firmatario è di 64 caratteri esadecimali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者のシードは 64 桁の16進文字です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 시드는 64자리 16진수 문자입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "seed penandatangan ialah 64 aksara hex" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "seed sygnatariusza to 64 znaki szesnastkowe" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي seed ۶۴ هېکس توري دي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "uma seed de signatário tem 64 caracteres hexadecimais" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "seed подписанта — 64 шестнадцатеричных символа" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் சீட் 64 ஹெக்ஸ் எழுத்துகள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کا سیڈ 64 ہیکس حروف کا ہوتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人种子为 64 位十六进制字符" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人種子為 64 位十六進制字元" + } + } + } + }, + "expected_recipients must be a list of addresses" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients must be a list of addresses" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يجب أن يكون expected_recipients قائمة عناوين" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients muss eine Liste von Adressen sein" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients debe ser una lista de direcciones" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients باید فهرستی از آدرس‌ها باشد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients doit être une liste d'adresses" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients पतों की सूची होनी चाहिए" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients harus berupa daftar alamat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients deve essere un elenco di indirizzi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients はアドレスのリストでなければなりません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients는 주소 목록이어야 합니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients mestilah senarai alamat" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients musi być listą adresów" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients باید د پتو لیست وي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients deve ser uma lista de endereços" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients должен быть списком адресов" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients முகவரிகளின் பட்டியலாக இருக்க வேண்டும்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients پتوں کی فہرست ہونی چاہیے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients 必须是地址列表" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected_recipients 必須是地址清單" + } + } + } + }, + "the key file's public_key is not the one its seed derives" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the key file's public_key is not the one its seed derives" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة public_key في ملف المفتاح ليست التي تشتقها بذرته" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der public_key der Schlüsseldatei ist nicht der, den ihr Seed ableitet" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el public_key del archivo de clave no es el que deriva su semilla" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "public_key فایل کلید همان نیست که seed آن مشتق می‌کند" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "le public_key du fichier de clé n'est pas celui que dérive sa graine" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "की फ़ाइल की public_key वह नहीं है जो उसका सीड व्युत्पन्न करता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "public_key berkas kunci bukan yang diturunkan seed-nya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il public_key del file di chiave non è quello che il suo seed deriva" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーファイルの public_key が、そのシードから導出されるものと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "키 파일의 public_key가 시드에서 도출된 값과 다릅니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "public_key fail kunci bukan yang diterbitkan oleh seed-nya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "public_key z pliku klucza nie jest tym, który wyprowadza jego seed" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د کیلي فایل public_key هغه نه دی چې seed یې راباسي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o public_key do arquivo de chave não é o que sua seed deriva" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "public_key в файле ключа не тот, что выводится из его seed" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "விசைக் கோப்பின் public_key அதன் சீட் வருவிப்பதல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "کی فائل کی public_key وہ نہیں جو اس کا سیڈ اخذ کرتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密钥文件中的 public_key 与其种子派生出的不一致" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "金鑰檔案內的 public_key 與其種子衍生出的不一致" + } + } + } + }, + "the key file's address is not the one its seed derives" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the key file's address is not the one its seed derives" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عنوان ملف المفتاح ليس العنوان الذي تشتقه بذرته" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Adresse der Schlüsseldatei ist nicht die, die ihr Seed ableitet" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la dirección del archivo de clave no es la que deriva su semilla" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آدرس فایل کلید همان نیست که seed آن مشتق می‌کند" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'adresse du fichier de clé n'est pas celle que dérive sa graine" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "की फ़ाइल का पता वह नहीं है जो उसका सीड व्युत्पन्न करता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "alamat berkas kunci bukan yang diturunkan seed-nya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'indirizzo del file di chiave non è quello che il suo seed deriva" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キーファイルのアドレスが、そのシードから導出されるものと一致しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "키 파일의 주소가 시드에서 도출된 주소와 다릅니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "alamat fail kunci bukan yang diterbitkan oleh seed-nya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "adres z pliku klucza nie jest tym, który wyprowadza jego seed" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د کیلي فایل پته هغه نه ده چې seed یې راباسي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o endereço do arquivo de chave não é o que sua seed deriva" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "адрес в файле ключа не тот, что выводится из его seed" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "விசைக் கோப்பின் முகவரி அதன் சீட் வருவிப்பதல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "کی فائل کا پتہ وہ نہیں جو اس کا سیڈ اخذ کرتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密钥文件中的地址与其种子派生出的地址不一致" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "金鑰檔案內的地址與其種子衍生出的地址不一致" + } + } + } + }, + "no expected recipient given: the payee was NOT checked" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "no expected recipient given: the payee was NOT checked" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يُحدَّد مستلم متوقع: لم يُتحقق من المستلم (NOT checked)" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "kein erwarteter Empfänger angegeben: der Zahlungsempfänger wurde NICHT geprüft" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "no se indicó ningún destinatario esperado: el beneficiario NO se comprobó" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "هیچ دریافت‌کننده مورد انتظاری داده نشد: دریافت‌کننده بررسی نشد (NOT checked)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "aucun destinataire attendu n'a été indiqué : le bénéficiaire n'a PAS été vérifié" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई अपेक्षित प्राप्तकर्ता नहीं दिया गया: प्राप्तकर्ता की जाँच नहीं हुई (NOT checked)" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "tidak ada penerima yang diharapkan: penerima pembayaran TIDAK diperiksa" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "nessun destinatario atteso indicato: il beneficiario NON è stato verificato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "想定される受取人が指定されていません: 受取人は確認されていません (NOT checked)" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "예상 수취인이 지정되지 않았습니다: 수취인을 확인하지 않았습니다 (NOT checked)" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "tiada penerima dijangka diberikan: penerima bayaran TIDAK disemak" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "nie podano oczekiwanego odbiorcy: odbiorca NIE został sprawdzony" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "هیڅ تمه شوی ترلاسه کوونکی نه دی ورکړل شوی: ترلاسه کوونکی ونه څېړل شو (NOT checked)" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "nenhum destinatário esperado informado: o beneficiário NÃO foi verificado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ожидаемый получатель не указан: получатель НЕ проверялся" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "எதிர்பார்க்கப்படும் பெறுநர் கொடுக்கப்படவில்லை: பெறுநர் சரிபார்க்கப்படவில்லை (NOT checked)" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "کوئی متوقع وصول کنندہ نہیں دیا گیا: وصول کنندہ کی جانچ نہیں ہوئی (NOT checked)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未给出预期收款人:收款人未经核对(NOT checked)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "未提供預期收款人:收款人未經核對(NOT checked)" + } + } + } + }, + "this proposal is for another policy: it names %@, this policy is %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "this proposal is for another policy: it names %@, this policy is %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا الاقتراح لسياسة أخرى: يذكر %@، وهذه السياسة هي %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "dieser Vorschlag gehört zu einer anderen Richtlinie: er nennt %@, diese Richtlinie ist %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "esta propuesta es de otra política: nombra %@, esta política es %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیشنهاد برای سیاست دیگری است: %@ را نام می‌برد، این سیاست %@ است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "cette proposition est pour une autre politique : elle nomme %@, cette politique est %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्रस्ताव किसी और पॉलिसी का है: इसमें %@ है, जबकि यह पॉलिसी %@ है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "proposal ini untuk kebijakan lain: menyebut %@, kebijakan ini adalah %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "questa proposta è per un'altra policy: nomina %@, questa policy è %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この提案は別のポリシー用です: %@ を指していますが、このポリシーは %@ です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 제안은 다른 정책용입니다: %@을(를) 가리키며 이 정책은 %@입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "cadangan ini untuk dasar lain: ia menyebut %@, dasar ini ialah %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ta propozycja dotyczy innej polityki: wskazuje %@, ta polityka to %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا وړاندیز د بلې پالیسۍ لپاره دی: %@ یادوي، دا پالیسي %@ ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "esta proposta é de outra política: nomeia %@, esta política é %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "это предложение для другой политики: в нём указано %@, эта политика — %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த முன்மொழிவு வேறொரு கொள்கைக்கானது: அது %@ என்கிறது, இந்தக் கொள்கை %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ تجویز کسی اور پالیسی کی ہے: اس میں %@ ہے، جبکہ یہ پالیسی %@ ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案属于另一个策略:它指向 %@,而此策略是 %@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案屬於另一個政策:它指向 %@,而此政策是 %@" + } + } + } + }, + "output %@ does not open as this account's: refusing to sign value it cannot see" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "output %@ does not open as this account's: refusing to sign value it cannot see" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المخرج %@ لا يُفتح على أنه يخص هذا الحساب: رفض توقيع قيمة لا يمكن رؤيتها" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausgabe %@ lässt sich nicht als die dieses Kontos öffnen: Signatur eines Werts, den sie nicht sehen kann, wird verweigert" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la salida %@ no se abre como de esta cuenta: se rechaza firmar un valor que no puede ver" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "خروجی %@ به‌عنوان خروجی این حساب باز نمی‌شود: امضای مقداری که دیده نمی‌شود رد می‌شود" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la sortie %@ ne s'ouvre pas comme celle de ce compte : refus de signer une valeur qu'elle ne peut pas voir" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आउटपुट %@ इस खाते के रूप में नहीं खुलता: जो मान दिखता नहीं उस पर हस्ताक्षर से इनकार" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "output %@ tidak terbuka sebagai milik akun ini: menolak menandatangani nilai yang tidak dapat dilihatnya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'output %@ non si apre come di questo account: rifiuto di firmare un valore che non può vedere" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "出力 %@ はこのアカウントのものとして開けません: 見えない値への署名を拒否します" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "출력 %@이(가) 이 계정의 것으로 열리지 않습니다: 볼 수 없는 값에는 서명을 거부합니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "output %@ tidak terbuka sebagai milik akaun ini: enggan menandatangani nilai yang tidak dapat dilihatnya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wyjście %@ nie otwiera się jako należące do tego konta: odmowa podpisania wartości, której nie widzi" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "محصول %@ د دې حساب په توګه نه خلاصېږي: د هغه ارزښت لاسلیک ردېږي چې نشي لیدل کېدای" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a saída %@ não abre como desta conta: recusa-se a assinar um valor que não consegue ver" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "выход %@ не открывается как принадлежащий этому аккаунту: отказ подписывать значение, которое нельзя увидеть" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "வெளியீடு %@ இந்தக் கணக்கினுடையதாகத் திறக்கவில்லை: பார்க்க முடியாத மதிப்புக்குக் கையொப்பமிட மறுக்கப்படுகிறது" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "آؤٹ پٹ %@ اس اکاؤنٹ کے طور پر نہیں کھلتا: جو قدر نظر نہیں آتی اس پر دستخط سے انکار" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输出 %@ 无法作为此账户的输出打开:拒绝对看不到的金额签名" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸出 %@ 無法作為此帳戶的輸出開啟:拒絕簽署看不到的金額" + } + } + } + }, + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة الشاهد (witness) لا تطابق المعاملة: المدخل %@ لا يُبطَل إلى nullifier الحزمة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Wert des Witness passt nicht zur Transaktion: Eingang %@ ergibt nicht den Nullifier des Bundles" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el valor del witness no coincide con la transacción: la entrada %@ no genera el nullifier del bundle" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار witness با تراکنش نمی‌خواند: ورودی %@ به nullifier مربوط به bundle نمی‌رسد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la valeur du witness ne correspond pas à la transaction : l'entrée %@ n'annule pas vers le nullifier du lot" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "विटनेस का मान लेन-देन से मेल नहीं खाता: इनपुट %@ बंडल के नलिफ़ायर तक नहीं पहुँचता" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak cocok dengan transaksi: input %@ tidak menghasilkan nullifier bundle" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il valore del witness non corrisponde alla transazione: l'input %@ non produce il nullifier del bundle" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウィットネスの値がトランザクションと一致しません: 入力 %@ はバンドルのヌリファイアに対応しません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "증인(witness) 값이 트랜잭션과 일치하지 않습니다: 입력 %@이(가) 번들의 널리파이어로 이어지지 않습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak sepadan dengan transaksi: input %@ tidak menjadi nullifier bundle" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wartość witness nie zgadza się z transakcją: wejście %@ nie daje nullifiera bundle" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د witness ارزښت له راکړې ورکړې سره نه برابرېږي: ننوتنه %@ د bundle nullifier ته نه رسېږي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o valor do witness não confere com a transação: a entrada %@ não gera o nullifier do bundle" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "значение witness не совпадает с транзакцией: вход %@ не даёт nullifier bundle" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "witness-இன் மதிப்பு பரிவர்த்தனையுடன் பொருந்தவில்லை: உள்ளீடு %@ bundle-இன் nullifier-க்கு வரவில்லை" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ویٹنس کی قدر لین دین سے مطابقت نہیں رکھتی: ان پٹ %@ بنڈل کے نلیفائر تک نہیں پہنچتا" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "见证数据的金额与交易不一致:输入 %@ 未对应到捆绑包的 nullifier" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "見證資料的金額與交易不一致:輸入 %@ 未對應到捆綁包的 nullifier" + } + } + } + }, + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قيمة الشاهد (witness) لا تطابق المخرجات: للأصل %@ مدخل %@ ومخرج %@ مع الرسوم والحرق" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Wert des Witness passt nicht zu den Ausgaben: Asset %@ hat %@ Eingang und %@ Ausgang mit Gebühr und Burns" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el valor del witness no coincide con las salidas: el activo %@ tiene %@ de entrada y %@ de salida con la comisión y las quemas" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار witness با خروجی‌ها نمی‌خواند: دارایی %@ ورودی %@ و خروجی %@ با کارمزد و سوزاندن‌ها دارد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la valeur du witness ne correspond pas aux sorties : l'actif %@ a %@ en entrée et %@ en sortie avec les frais et les brûlages" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "विटनेस का मान आउटपुट से मेल नहीं खाता: एसेट %@ में %@ इनपुट और शुल्क व बर्न सहित %@ आउटपुट है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak cocok dengan output: aset %@ memiliki %@ masuk dan %@ keluar beserta biaya dan pembakaran" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il valore del witness non corrisponde agli output: l'asset %@ ha %@ in ingresso e %@ in uscita con la commissione e i burn" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ウィットネスの値が出力と一致しません: アセット %@ は、入力 %@ に対し、手数料とバーンを含めた出力が %@ です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "증인(witness) 값이 출력과 일치하지 않습니다: 자산 %@의 입력은 %@, 수수료와 소각을 포함한 출력은 %@입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "nilai witness tidak sepadan dengan output: aset %@ mempunyai %@ masuk dan %@ keluar dengan yuran dan pembakaran" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wartość witness nie zgadza się z wyjściami: aktywo %@ ma %@ na wejściu i %@ na wyjściu z opłatą i spaleniami" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د witness ارزښت له محصولاتو سره نه برابرېږي: شتمنۍ %@ %@ راتلونکی او %@ وتلونکی لري، له فیس او سوځولو سره" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o valor do witness não confere com as saídas: o ativo %@ tem %@ de entrada e %@ de saída com a taxa e as queimas" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "значение witness не совпадает с выходами: у актива %@ на входе %@, на выходе %@ с комиссией и сжиганиями" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "witness-இன் மதிப்பு வெளியீடுகளுடன் பொருந்தவில்லை: சொத்து %@-இல் உள்ளீடு %@, கட்டணம் மற்றும் எரிப்புகளுடன் வெளியீடு %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ویٹنس کی قدر آؤٹ پٹس سے مطابقت نہیں رکھتی: اثاثہ %@ میں %@ ان پٹ اور فیس و برن کے ساتھ %@ آؤٹ پٹ ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "见证数据的金额与输出不一致:资产 %@ 输入 %@,输出 %@(含手续费和销毁)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "見證資料的金額與輸出不一致:資產 %@ 輸入 %@,輸出 %@(含手續費及銷毀)" + } + } + } + }, + "the proposal expires at %@, outside its bundle's window (%@, %@]" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the proposal expires at %@, outside its bundle's window (%@, %@]" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ينتهي الاقتراح عند %@، خارج نافذة حزمته (%@, %@]" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Vorschlag läuft bei %@ ab, außerhalb des Fensters seines Bundles (%@, %@]" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la propuesta caduca en %@, fuera de la ventana de su bundle (%@, %@]" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیشنهاد در %@ منقضی می‌شود، بیرون از بازه bundle خود (%@, %@]" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la proposition expire à %@, hors de la fenêtre de son lot (%@, %@]" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव %@ पर समाप्त होता है, जो उसके बंडल की सीमा (%@, %@] से बाहर है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "proposal kedaluwarsa pada %@, di luar jendela bundle-nya (%@, %@]" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la proposta scade a %@, fuori dalla finestra del suo bundle (%@, %@]" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案の期限 %@ は、そのバンドルの範囲 (%@, %@] の外です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안의 만료 %@이(가) 번들의 구간 (%@, %@] 밖에 있습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "cadangan tamat tempoh pada %@, di luar tetingkap bundle-nya (%@, %@]" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "propozycja wygasa na %@, poza oknem swojego bundle (%@, %@]" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وړاندیز په %@ کې پای ته رسېږي، د خپل bundle له کړکۍ (%@, %@] بهر" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a proposta expira em %@, fora da janela do seu bundle (%@, %@]" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "предложение истекает на %@, вне окна его bundle (%@, %@]" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவு %@-இல் காலாவதியாகிறது, அதன் bundle-இன் சாளரத்துக்கு (%@, %@] வெளியே" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز %@ پر ختم ہوتی ہے، جو اس کے بنڈل کی حد (%@, %@] سے باہر ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案在 %@ 过期,超出其捆绑包的有效区间 (%@, %@]" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案在 %@ 過期,超出其捆綁包的有效區間 (%@, %@]" + } + } + } + }, + "the transaction is for chain %@, the proposal says chain %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the transaction is for chain %@, the proposal says chain %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المعاملة للسلسلة %@، بينما يذكر الاقتراح السلسلة %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Transaktion gilt für Chain %@, der Vorschlag nennt Chain %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transacción es para la cadena %@, la propuesta dice la cadena %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تراکنش برای شبکه %@ است، پیشنهاد شبکه %@ را می‌گوید" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transaction est pour la chaîne %@, la proposition indique la chaîne %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लेन-देन चेन %@ के लिए है, प्रस्ताव चेन %@ बताता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi ini untuk chain %@, proposal menyebut chain %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la transazione è per la catena %@, la proposta indica la catena %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トランザクションはチェーン %@ 用ですが、提案はチェーン %@ を指しています" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "트랜잭션은 체인 %@용이지만 제안은 체인 %@을(를) 가리킵니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "transaksi ini untuk rantaian %@, cadangan menyebut rantaian %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "transakcja jest dla łańcucha %@, propozycja wskazuje łańcuch %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "راکړه ورکړه د زنځیر %@ لپاره ده، وړاندیز زنځیر %@ وايي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a transação é para a cadeia %@, a proposta diz a cadeia %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "транзакция предназначена для сети %@, а предложение указывает сеть %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பரிவர்த்தனை செயின் %@-க்கானது, முன்மொழிவு செயின் %@ என்கிறது" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "لین دین چین %@ کے لیے ہے، تجویز چین %@ بتاتی ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易属于链 %@,而提案指向链 %@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "交易屬於鏈 %@,而提案指向鏈 %@" + } + } + } + }, + "signer %@'s signature does not verify over this proposal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "signer %@'s signature does not verify over this proposal" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "توقيع الموقّع %@ غير صالح لهذا الاقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Signatur von Unterzeichner %@ ist für diesen Vorschlag nicht gültig" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la firma del firmante %@ no es válida para esta propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضای امضاکننده %@ برای این پیشنهاد معتبر نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la signature du signataire %@ n'est pas valide pour cette proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता %@ का हस्ताक्षर इस प्रस्ताव के लिए मान्य नहीं है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "tanda tangan penanda tangan %@ tidak valid untuk proposal ini" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la firma del firmatario %@ non è valida per questa proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者 %@ の署名は、この提案に対して有効ではありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 %@의 서명이 이 제안에 대해 유효하지 않습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "tandatangan penandatangan %@ tidak sah untuk cadangan ini" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "podpis sygnatariusza %@ nie jest poprawny dla tej propozycji" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي %@ لاسلیک د دې وړاندیز لپاره سم نه دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a assinatura do signatário %@ não é válida para esta proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "подпись подписанта %@ не верна для этого предложения" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் %@-இன் கையொப்பம் இந்த முன்மொழிவுக்குச் சரியானதல்ல" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ %@ کا دستخط اس تجویز کے لیے درست نہیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人 %@ 的签名对此提案验证未通过" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人 %@ 的簽署對此提案驗證不通過" + } + } + } + }, + "signer %@ is not in this policy (it has %@ signers)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "signer %@ is not in this policy (it has %@ signers)" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الموقّع %@ ليس في هذه السياسة (فيها %@ موقّعين)" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterzeichner %@ gehört nicht zu dieser Richtlinie (sie hat %@ Unterzeichner)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el firmante %@ no está en esta política (tiene %@ firmantes)" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضاکننده %@ در این سیاست نیست (این سیاست %@ امضاکننده دارد)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "le signataire %@ n'est pas dans cette politique (elle compte %@ signataires)" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता %@ इस पॉलिसी में नहीं है (इसमें %@ हस्ताक्षरकर्ता हैं)" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "penanda tangan %@ tidak ada dalam kebijakan ini (kebijakan ini memiliki %@ penanda tangan)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "il firmatario %@ non è in questa policy (ha %@ firmatari)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者 %@ はこのポリシーに含まれていません (署名者は %@ 人です)" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 %@은(는) 이 정책에 없습니다 (서명자는 %@명입니다)" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "penandatangan %@ tiada dalam dasar ini (ia mempunyai %@ penandatangan)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "sygnatariusza %@ nie ma w tej polityce (ma ona %@ sygnatariuszy)" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لاسلیک کوونکی %@ په دې پالیسۍ کې نشته (دا %@ لاسلیک کوونکي لري)" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o signatário %@ não está nesta política (ela tem %@ signatários)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "подписанта %@ нет в этой политике (в ней %@ подписантов)" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் %@ இந்தக் கொள்கையில் இல்லை (இதில் %@ கையொப்பமிடுபவர்கள் உள்ளனர்)" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ %@ اس پالیسی میں نہیں ہے (اس میں %@ دستخط کنندگان ہیں)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人 %@ 不在此策略中(该策略有 %@ 位签名人)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人 %@ 不在此政策內(該政策有 %@ 位簽署人)" + } + } + } + }, + "expected recipient %@: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "expected recipient %@: %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المستلم المتوقع %@: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "erwarteter Empfänger %@: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "destinatario esperado %@: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "دریافت‌کننده مورد انتظار %@: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "destinataire attendu %@ : %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अपेक्षित प्राप्तकर्ता %@: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "penerima yang diharapkan %@: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "destinatario atteso %@: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "想定される受取人 %@: %@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "예상 수취인 %@: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "penerima dijangka %@: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "oczekiwany odbiorca %@: %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "تمه شوی ترلاسه کوونکی %@: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "destinatário esperado %@: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "ожидаемый получатель %@: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "எதிர்பார்க்கப்படும் பெறுநர் %@: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "متوقع وصول کنندہ %@: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预期收款人 %@:%@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "預期收款人 %@:%@" + } + } + } + }, + "the policy does not parse: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the policy does not parse: %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تحليل السياسة: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "die Richtlinie lässt sich nicht parsen: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la política no se puede interpretar: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "سیاست تجزیه نمی‌شود: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la politique ne peut pas être analysée : %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पॉलिसी पार्स नहीं होती: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "kebijakan tidak dapat diurai: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la policy non si può analizzare: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポリシーを解析できません: %@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "정책을 해석할 수 없습니다: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "dasar tidak dapat dihuraikan: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "nie można odczytać polityki: %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "پالیسي نشي تحلیل کېدای: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a política não pôde ser lida: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "политику не удалось разобрать: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கொள்கையைப் பாகுபடுத்த முடியவில்லை: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "پالیسی پارس نہیں ہوتی: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "策略无法解析:%@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "政策無法解析:%@" + } + } + } + }, + "the proposal does not parse: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "the proposal does not parse: %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تحليل الاقتراح: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "der Vorschlag lässt sich nicht parsen: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la propuesta no se puede interpretar: %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیشنهاد تجزیه نمی‌شود: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "la proposition ne peut pas être analysée : %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव पार्स नहीं होता: %@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "proposal tidak dapat diurai: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la proposta non si può analizzare: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案を解析できません: %@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안을 해석할 수 없습니다: %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "cadangan tidak dapat dihuraikan: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "nie można odczytać propozycji: %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وړاندیز نشي تحلیل کېدای: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "a proposta não pôde ser lida: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "предложение не удалось разобрать: %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவைப் பாகுபடுத்த முடியவில்லை: %@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز پارس نہیں ہوتی: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案无法解析:%@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案無法解析:%@" + } + } + } + }, + "The proposal check returned an incomplete answer, so this proposal is not signed." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The proposal check returned an incomplete answer, so this proposal is not signed." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أعاد التحقق من الاقتراح ردًّا ناقصًا، لذا لن يُوقَّع هذا الاقتراح." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Vorschlagsprüfung hat eine unvollständige Antwort geliefert, daher wird dieser Vorschlag nicht signiert." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La comprobación de la propuesta devolvió una respuesta incompleta, así que esta propuesta no se firma." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی پیشنهاد پاسخ ناقصی برگرداند، پس این پیشنهاد امضا نمی‌شود." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La vérification de la proposition a renvoyé une réponse incomplète, cette proposition n'est donc pas signée." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव की जाँच ने अधूरा उत्तर लौटाया, इसलिए इस प्रस्ताव पर हस्ताक्षर नहीं किए गए।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pemeriksaan proposal mengembalikan jawaban yang tidak lengkap, sehingga proposal ini tidak ditandatangani." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La verifica della proposta ha restituito una risposta incompleta, quindi questa proposta non viene firmata." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案の確認が不完全な応答を返したため、この提案には署名しません。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안 확인이 불완전한 응답을 반환했으므로 이 제안에는 서명하지 않습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semakan cadangan memulangkan jawapan yang tidak lengkap, jadi cadangan ini tidak ditandatangani." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdzenie propozycji zwróciło niepełną odpowiedź, więc ta propozycja nie zostaje podpisana." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د وړاندیز څېړنې ناقص ځواب راستون کړ، نو دا وړاندیز نه لاسلیک کېږي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "A verificação da proposta devolveu uma resposta incompleta, então esta proposta não é assinada." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверка предложения вернула неполный ответ, поэтому это предложение не подписано." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவுச் சரிபார்ப்பு முழுமையற்ற பதிலைத் தந்தது, எனவே இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز کی جانچ نے ادھورا جواب لوٹایا، اس لیے اس تجویز پر دستخط نہیں کیے گئے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案核对返回了不完整的结果,因此此提案不会签名。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案核對傳回不完整的結果,因此此提案不會簽署。" + } + } + } + }, + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أدخل عنوان كل مستلم: لا يمكن توقيع مخرج لا يطابق العنوان الذي أدخلته أو عنوانه فارغ." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Adresse jedes Zahlungsempfängers ein: Eine Ausgabe, die nicht zur eingegebenen Adresse passt oder deren Adresse leer ist, kann nicht signiert werden." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce la dirección de cada beneficiario: una salida que no coincide con la dirección introducida, o cuya dirección está vacía, no se puede firmar." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آدرس هر دریافت‌کننده را وارد کنید: خروجی‌ای که با آدرس واردشده مطابقت ندارد یا آدرسش خالی است قابل امضا نیست." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saisissez l'adresse de chaque bénéficiaire : une sortie qui ne correspond pas à l'adresse saisie, ou dont l'adresse est vide, ne peut pas être signée." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हर प्राप्तकर्ता का पता दर्ज करें: जो आउटपुट दर्ज किए पते से मेल नहीं खाता, या जिसका पता खाली है, उस पर हस्ताक्षर नहीं हो सकते।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan alamat setiap penerima pembayaran: output yang tidak cocok dengan alamat yang Anda masukkan, atau yang alamatnya kosong, tidak dapat ditandatangani." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'indirizzo di ogni beneficiario: un output che non corrisponde all'indirizzo inserito, o il cui indirizzo è vuoto, non può essere firmato." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての受取人のアドレスを入力してください。入力したアドレスと一致しない出力、またはアドレスが空の出力には署名できません。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 수취인의 주소를 입력하세요. 입력한 주소와 일치하지 않거나 주소가 비어 있는 출력은 서명할 수 없습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan alamat setiap penerima bayaran: output yang tidak sepadan dengan alamat yang anda masukkan, atau yang alamatnya kosong, tidak boleh ditandatangani." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wpisz adres każdego odbiorcy: wyjścia niepasującego do wpisanego adresu lub mającego pusty adres nie można podpisać." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د هر تادیه ترلاسه کوونکي پته ولیکئ: هغه محصول چې له لیکل شوې پتې سره نه برابرېږي، یا پته یې خالي ده، لاسلیک نشي کېدای." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe o endereço de cada beneficiário: uma saída que não corresponde ao endereço informado, ou cujo endereço está vazio, não pode ser assinada." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите адрес каждого получателя: выход, не совпадающий с введённым адресом или с пустым адресом, подписать нельзя." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பெறுபவர் ஒவ்வொருவரின் முகவரியையும் உள்ளிடுங்கள்: நீங்கள் உள்ளிட்ட முகவரியுடன் பொருந்தாத அல்லது முகவரி வெறுமையாக உள்ள வெளியீட்டில் கையொப்பமிட முடியாது." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ہر وصول کنندہ کا پتہ درج کریں: جو آؤٹ پٹ درج کیے گئے پتے سے مطابقت نہیں رکھتا، یا جس کا پتہ خالی ہے، اس پر دستخط نہیں ہو سکتے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入每位收款人的地址:与所输入地址不匹配、或地址为空的输出无法签名。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入每位收款人的地址:與所輸入地址不相符、或地址為空的輸出無法簽署。" + } + } + } + }, + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم تردّ أي عقدة على %@، لذا لا يمكن التحقق من انتهاء الصلاحية ولن يُوقَّع هذا الاقتراح. أعد المحاولة عندما تصبح العقدة متاحة." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unter %@ hat kein Knoten geantwortet, daher lässt sich der Ablauf nicht prüfen und dieser Vorschlag wird nicht signiert. Versuche es erneut, wenn der Knoten erreichbar ist." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ningún nodo respondió en %@, así que no se puede comprobar la caducidad y esta propuesta no se firma. Reintenta cuando el nodo esté accesible." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "هیچ گره‌ای در %@ پاسخ نداد، پس انقضا قابل بررسی نیست و این پیشنهاد امضا نمی‌شود. وقتی گره در دسترس بود دوباره تلاش کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun nœud n'a répondu à %@, l'expiration ne peut donc pas être vérifiée et cette proposition n'est pas signée. Réessayez quand le nœud sera joignable." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ पर किसी नोड ने जवाब नहीं दिया, इसलिए समाप्ति की जाँच नहीं हो सकती और इस प्रस्ताव पर हस्ताक्षर नहीं किए गए। नोड उपलब्ध होने पर फिर कोशिश करें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidak ada node yang menjawab di %@, sehingga masa berlaku tidak dapat diperiksa dan proposal ini tidak ditandatangani. Coba lagi saat node dapat dijangkau." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun nodo ha risposto a %@, quindi la scadenza non si può verificare e questa proposta non viene firmata. Riprova quando il nodo è raggiungibile." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ でノードから応答がなかったため、有効期限を確認できず、この提案には署名しません。ノードに接続できるようになってから再試行してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에서 응답한 노드가 없어 만료를 확인할 수 없으므로 이 제안에는 서명하지 않습니다. 노드에 연결할 수 있을 때 다시 시도하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiada nod menjawab di %@, jadi tempoh tamat tidak dapat disemak dan cadangan ini tidak ditandatangani. Cuba lagi apabila nod boleh dicapai." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Żaden węzeł nie odpowiedział pod %@, więc nie można sprawdzić wygaśnięcia i ta propozycja nie zostaje podpisana. Spróbuj ponownie, gdy węzeł będzie osiągalny." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "په %@ کې هیڅ نوډ ځواب نه کړ، نو پای ته رسېدل نشي څېړل کېدای او دا وړاندیز نه لاسلیک کېږي. کله چې نوډ ترلاسه وي بیا هڅه وکړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhum nó respondeu em %@, então a expiração não pode ser verificada e esta proposta não é assinada. Tente de novo quando o nó estiver acessível." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ни один узел не ответил по адресу %@, поэтому срок действия проверить нельзя, и это предложение не подписано. Повторите, когда узел станет доступен." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@-இல் எந்த நோட்டும் பதிலளிக்கவில்லை, எனவே காலாவதியைச் சரிபார்க்க முடியாது; இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை. நோட் எட்டக்கூடியதாகும்போது மீண்டும் முயலுங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ پر کسی نوڈ نے جواب نہیں دیا، اس لیے میعاد کی جانچ نہیں ہو سکتی اور اس تجویز پر دستخط نہیں کیے گئے۔ نوڈ دستیاب ہونے پر دوبارہ کوشش کریں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 没有节点应答,因此无法核对过期时间,此提案不会签名。待节点可访问后重试。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 沒有節點回應,因此無法核對過期時間,此提案不會簽署。待節點可連線後再試。" + } + } + } + }, + "This proposal is for chain %@; this wallet is on chain %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This proposal is for chain %@; this wallet is on chain %@." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا الاقتراح للسلسلة %@؛ وهذه المحفظة على السلسلة %@." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieser Vorschlag gilt für Chain %@; diese Wallet ist auf Chain %@." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta propuesta es para la cadena %@; esta billetera está en la cadena %@." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیشنهاد برای شبکه %@ است؛ این کیف پول روی شبکه %@ است." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette proposition est pour la chaîne %@ ; ce portefeuille est sur la chaîne %@." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्रस्ताव चेन %@ के लिए है; यह वॉलेट चेन %@ पर है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposal ini untuk chain %@; dompet ini berada di chain %@." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questa proposta è per la catena %@; questo portafoglio è sulla catena %@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この提案はチェーン %@ 用ですが、このウォレットはチェーン %@ にあります。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 제안은 체인 %@용이며, 이 지갑은 체인 %@에 있습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadangan ini untuk rantaian %@; dompet ini berada pada rantaian %@." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta propozycja jest dla łańcucha %@; ten portfel jest w łańcuchu %@." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا وړاندیز د زنځیر %@ لپاره دی؛ دا والټ په زنځیر %@ کې دی." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta proposta é para a cadeia %@; esta carteira está na cadeia %@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это предложение для сети %@; этот кошелёк в сети %@." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த முன்மொழிவு செயின் %@-க்கானது; இந்த வாலட் செயின் %@-இல் உள்ளது." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ تجویز چین %@ کے لیے ہے؛ یہ والیٹ چین %@ پر ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案属于链 %@;此钱包在链 %@ 上。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案屬於鏈 %@;此錢包在鏈 %@ 上。" + } + } + } + }, + "This proposal is bound to another genesis than the one this wallet's node serves." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This proposal is bound to another genesis than the one this wallet's node serves." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا الاقتراح مرتبط بنشأة (genesis) مختلفة عن التي تخدمها عقدة هذه المحفظة." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieser Vorschlag ist an ein anderes Genesis gebunden als das, das der Knoten dieser Wallet bedient." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta propuesta está ligada a un génesis distinto del que sirve el nodo de esta billetera." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیشنهاد به genesis دیگری بسته شده است، نه آنچه گره این کیف پول ارائه می‌دهد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette proposition est liée à un autre genesis que celui que sert le nœud de ce portefeuille." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्रस्ताव उस जेनेसिस से अलग जेनेसिस से बँधा है जिसे इस वॉलेट का नोड परोसता है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposal ini terikat pada genesis yang berbeda dari yang dilayani node dompet ini." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questa proposta è legata a un genesis diverso da quello che serve il nodo di questo portafoglio." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この提案は、このウォレットのノードが提供するものとは別のジェネシスに結び付けられています。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 제안은 이 지갑의 노드가 제공하는 것과 다른 제네시스에 묶여 있습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadangan ini terikat pada genesis yang berbeza daripada yang dilayani nod dompet ini." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta propozycja jest związana z innym genesis niż ten, który obsługuje węzeł tego portfela." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا وړاندیز له هغه genesis پرته بل ته تړل شوی چې د دې والټ نوډ یې وړاندې کوي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta proposta está vinculada a um genesis diferente do que o nó desta carteira serve." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это предложение привязано к другому genesis, чем тот, который обслуживает узел этого кошелька." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த முன்மொழிவு, இந்த வாலட்டின் நோட் வழங்கும் genesis-ஐ விட வேறொன்றுடன் பிணைக்கப்பட்டுள்ளது." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ تجویز اس جینیسس سے مختلف جینیسس سے بندھی ہے جو اس والیٹ کا نوڈ فراہم کرتا ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案绑定的创世区块与此钱包的节点所提供的不同。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案綁定的創世區塊與此錢包的節點所提供的不同。" + } + } + } + }, + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ينتهي هذا الاقتراح عند الارتفاع %@ والسلسلة عند %@: فات أوان التوقيع. اطلب اقتراحًا جديدًا." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieser Vorschlag läuft bei Höhe %@ ab und die Chain steht bei %@: zu spät zum Signieren. Fordere einen neuen Vorschlag an." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta propuesta caduca en la altura %@ y la cadena va por %@: es tarde para firmar. Pide una propuesta nueva." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیشنهاد در ارتفاع %@ منقضی می‌شود و شبکه در %@ است: برای امضا دیر شده است. پیشنهاد جدید بخواهید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette proposition expire à la hauteur %@ et la chaîne en est à %@ : trop tard pour signer. Demandez une nouvelle proposition." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्रस्ताव ऊँचाई %@ पर समाप्त होता है और चेन %@ पर है: हस्ताक्षर के लिए बहुत देर हो चुकी। नया प्रस्ताव माँगें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposal ini kedaluwarsa pada ketinggian %@ dan chain berada di %@: terlambat untuk menandatangani. Minta proposal baru." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questa proposta scade all'altezza %@ e la catena è a %@: troppo tardi per firmare. Chiedi una nuova proposta." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この提案は高さ %@ で期限切れとなり、チェーンは %@ にあります: 署名には遅すぎます。新しい提案を依頼してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 제안은 높이 %@에서 만료되며 체인은 %@에 있습니다: 서명하기에는 너무 늦었습니다. 새 제안을 요청하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadangan ini tamat tempoh pada ketinggian %@ dan rantaian berada di %@: terlambat untuk menandatangani. Minta cadangan baharu." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ta propozycja wygasa na wysokości %@, a łańcuch jest na %@: za późno na podpis. Poproś o nową propozycję." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا وړاندیز په %@ لوړوالي کې پای ته رسېږي او زنځیر په %@ کې دی: د لاسلیک لپاره ډېر ناوخته دی. نوی وړاندیز وغواړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esta proposta expira na altura %@ e a cadeia está em %@: tarde demais para assinar. Peça uma nova proposta." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это предложение истекает на высоте %@, а сеть на %@: подписывать поздно. Запросите новое предложение." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த முன்மொழிவு உயரம் %@-இல் காலாவதியாகும், செயின் %@-இல் உள்ளது: கையொப்பமிட மிகவும் தாமதம். புதிய முன்மொழிவைக் கேளுங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ تجویز اونچائی %@ پر ختم ہوتی ہے اور چین %@ پر ہے: دستخط کے لیے بہت دیر ہو چکی۔ نئی تجویز مانگیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案在高度 %@ 过期,而链已到 %@:已来不及签名。请索取新的提案。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案在高度 %@ 過期,而鏈已去到 %@:已來不及簽署。請索取新的提案。" + } + } + } + }, + "The signer keys could not be removed, so the wallet was not forgotten. Try again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The signer keys could not be removed, so the wallet was not forgotten. Try again." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّرت إزالة مفاتيح الموقّعين، لذا لم تُنسَ المحفظة. حاول مرة أخرى." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Unterzeichner-Schlüssel konnten nicht entfernt werden, daher wurde die Wallet nicht vergessen. Versuche es erneut." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudieron quitar las claves de firmante, así que no se olvidó la billetera. Inténtalo de nuevo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "کلیدهای امضاکننده حذف نشدند، پس کیف پول فراموش نشد. دوباره تلاش کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les clés de signataire n'ont pas pu être retirées, donc le portefeuille n'a pas été oublié. Réessayez." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता की कुंजियाँ हटाई नहीं जा सकीं, इसलिए वॉलेट को भुलाया नहीं गया। फिर से कोशिश करें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunci penanda tangan tidak dapat dihapus, jadi dompet tidak dilupakan. Coba lagi." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è stato possibile rimuovere le chiavi del firmatario, quindi il portafoglio non è stato dimenticato. Riprova." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者の鍵を削除できなかったため、ウォレットは削除されませんでした。もう一度お試しください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 키를 제거하지 못해 지갑을 삭제하지 않았습니다. 다시 시도하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunci penandatangan tidak dapat dibuang, jadi dompet tidak dilupakan. Cuba lagi." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się usunąć kluczy sygnatariusza, więc portfel nie został zapomniany. Spróbuj ponownie." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي کلیګانې لرې نشوې، نو بټوه هېره نشوه. بیا هڅه وکړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível remover as chaves de signatário, então a carteira não foi esquecida. Tente de novo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось удалить ключи подписанта, поэтому кошелёк не был забыт. Попробуйте снова." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் விசைகளை நீக்க முடியவில்லை, எனவே பணப்பை மறக்கப்படவில்லை. மீண்டும் முயலவும்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کی کلیدیں ہٹائی نہ جا سکیں، اس لیے والیٹ نہیں بھولا گیا۔ دوبارہ کوشش کریں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法移除签名人密钥,因此钱包没有被忘记。请重试。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法移除簽署人密鑰,因此錢包沒有被忘記。請再試一次。" + } + } + } + }, + "Manage multisig signers" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage multisig signers" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إدارة موقّعي التوقيع المتعدد" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multisig-Unterzeichner verwalten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestionar firmantes multifirma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مدیریت امضاکنندگان چندامضایی" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gérer les signataires multisig" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मल्टीसिग हस्ताक्षरकर्ता प्रबंधित करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kelola penanda tangan multisig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gestisci i firmatari multisig" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マルチシグ署名者を管理" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "멀티시그 서명자 관리" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Urus penandatangan multisig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zarządzaj sygnatariuszami multisig" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ملټي‌سېګ لاسلیک کوونکي اداره کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerenciar signatários multiassinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Управление подписантами мультиподписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மல்டிசிக் கையொப்பமிடுபவர்களை நிர்வகி" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملٹی سگ دستخط کنندگان کا انتظام کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理多签签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理多重簽署簽署人" + } + } + } + }, + "Multisig signers" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multisig signers" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "موقّعو التوقيع المتعدد" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multisig-Unterzeichner" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmantes multifirma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضاکنندگان چندامضایی" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signataires multisig" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मल्टीसिग हस्ताक्षरकर्ता" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Penanda tangan multisig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmatari multisig" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マルチシグ署名者" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "멀티시그 서명자" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Penandatangan multisig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sygnatariusze multisig" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ملټي‌سېګ لاسلیک کوونکي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signatários multiassinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подписанты мультиподписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மல்டிசிக் கையொப்பமிடுபவர்கள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملٹی سگ دستخط کنندگان" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "多签签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "多重簽署簽署人" + } + } + } + }, + "OK" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حسنًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تأیید" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ठीक है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "سمه ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "சரி" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ٹھیک ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "好" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "好" + } + } + } + }, + "Enter the address of every payment." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the address of every payment." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أدخل عنوان كل دفعة." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die Adresse jeder Zahlung ein." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce la dirección de cada pago." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آدرس هر پرداخت را وارد کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saisissez l'adresse de chaque paiement." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हर भुगतान का पता दर्ज करें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan alamat setiap pembayaran." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'indirizzo di ogni pagamento." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての支払いのアドレスを入力してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 결제의 주소를 입력하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan alamat setiap pembayaran." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wpisz adres każdej płatności." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د هرې تادیې پته ولیکئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe o endereço de cada pagamento." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите адрес каждого платежа." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "ஒவ்வொரு கட்டணத்தின் முகவரியையும் உள்ளிடுங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ہر ادائیگی کا پتہ درج کریں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入每笔付款的地址。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入每筆付款的地址。" + } + } + } + }, + "Enter the amount of every payment: it is what stops a split or an inflated payment." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the amount of every payment: it is what stops a split or an inflated payment." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أدخل مبلغ كل دفعة: فهو ما يمنع التجزئة أو تضخيم الدفعة." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib den Betrag jeder Zahlung ein: Er verhindert eine Aufteilung oder eine überhöhte Zahlung." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce el monto de cada pago: es lo que impide una división o un pago inflado." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار هر پرداخت را وارد کنید: همین است که جلوی تقسیم یا پرداخت باد‌شده را می‌گیرد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saisissez le montant de chaque paiement : c'est ce qui empêche un fractionnement ou un paiement gonflé." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हर भुगतान की राशि दर्ज करें: यही भुगतान को बाँटने या बढ़ाए जाने से रोकती है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan jumlah setiap pembayaran: inilah yang mencegah pemecahan atau pembayaran yang digelembungkan." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci l'importo di ogni pagamento: è ciò che impedisce una divisione o un pagamento gonfiato." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべての支払いの金額を入力してください。支払いの分割や水増しを防ぐのは金額です。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 결제의 금액을 입력하세요. 결제를 쪼개거나 부풀리는 것을 막아 주는 것이 금액입니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan amaun setiap pembayaran: ia menghalang pemecahan atau pembayaran yang dibengkakkan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wpisz kwotę każdej płatności: to ona chroni przed podziałem lub zawyżoną płatnością." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د هرې تادیې اندازه ولیکئ: همدا هغه څه دي چې ویش یا پړسول شوې تادیه ځنډوي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe o valor de cada pagamento: é o que impede uma divisão ou um pagamento inflado." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите сумму каждого платежа: именно она не даёт разбить платёж или завысить его." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "ஒவ்வொரு கட்டணத்தின் தொகையையும் உள்ளிடுங்கள்: பிரித்தலையோ பெருக்கிய கட்டணத்தையோ தடுப்பது இதுவே." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ہر ادائیگی کی رقم درج کریں: یہی ادائیگی کو بانٹنے یا پھلانے سے روکتی ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入每笔付款的金额:它可以防止拆分或虚增付款。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入每筆付款的金額:它可防止拆分或虛增付款。" + } + } + } + }, + "An amount is a number of RAND, such as 1.5." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An amount is a number of RAND, such as 1.5." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المبلغ هو عدد من RAND، مثل 1.5." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ein Betrag ist eine Anzahl RAND, zum Beispiel 1.5." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un monto es una cantidad de RAND, como 1.5." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار یک عدد RAND است، مانند 1.5." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un montant est un nombre de RAND, comme 1.5." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "राशि RAND की एक संख्या होती है, जैसे 1.5।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jumlah adalah bilangan RAND, misalnya 1.5." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un importo è un numero di RAND, come 1.5." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額は RAND の数値です。例: 1.5" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "금액은 RAND 단위의 숫자입니다. 예: 1.5" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amaun ialah bilangan RAND, seperti 1.5." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kwota to liczba RAND, na przykład 1.5." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "اندازه د RAND شمېره ده، لکه 1.5." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Um valor é um número de RAND, como 1.5." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сумма — это число RAND, например 1.5." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "தொகை என்பது RAND-இன் எண், எடுத்துக்காட்டாக 1.5." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "رقم RAND کی ایک عددی مقدار ہے، جیسے 1.5۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "金额是 RAND 的数值,例如 1.5。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額是 RAND 的數值,例如 1.5。" + } + } + } + }, + "Sign a proposal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign a proposal" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "توقيع اقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Vorschlag signieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmar una propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضای یک پیشنهاد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signer une proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव पर हस्ताक्षर करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangani proposal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firma una proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案に署名" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안에 서명" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangani cadangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podpisz propozycję" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وړاندیز لاسلیک کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assinar uma proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подписать предложение" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவில் கையொப்பமிடு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز پر دستخط کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签署提案" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署提案" + } + } + } + }, + "Proposal (proposal.json)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposal (proposal.json)" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الاقتراح (proposal.json)" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorschlag (proposal.json)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Propuesta (proposal.json)" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پیشنهاد (proposal.json)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposition (proposal.json)" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव (proposal.json)" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposal (proposal.json)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposta (proposal.json)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案 (proposal.json)" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안 (proposal.json)" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cadangan (proposal.json)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Propozycja (proposal.json)" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وړاندیز (proposal.json)" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proposta (proposal.json)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предложение (proposal.json)" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவு (proposal.json)" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز (proposal.json)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案(proposal.json)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案(proposal.json)" + } + } + } + }, + "Check a share" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check a share" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "التحقق من حصة توقيع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Signaturanteil prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar una parte de firma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی یک سهم امضا" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier une part de signature" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षर अंश जाँचें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periksa bagian tanda tangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica una quota di firma" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名シェアを確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명 조각 확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semak bahagian tandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź udział podpisu" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک برخه وڅېړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar uma parte da assinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить долю подписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பப் பங்கைச் சரிபார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخطی حصہ جانچیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "核对签名份额" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "核對簽署份額" + } + } + } + }, + "%@ of %@ · signer %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ of %@ · signer %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ من %@ · الموقّع %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ von %@ · Unterzeichner %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ · firmante %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ از %@ · امضاکننده %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sur %@ · signataire %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ में से %1$@ · हस्ताक्षरकर्ता %3$@" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dari %@ · penanda tangan %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ di %@ · firmatario %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 署名者 %@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 서명자 %@" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ daripada %@ · penandatangan %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ z %@ · sygnatariusz %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د %2$@ څخه %1$@ · لاسلیک کوونکی %3$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ · signatário %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ из %@ · подписант %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@-இல் %1$@ · கையொப்பமிடுபவர் %3$@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ میں سے %1$@ · دستخط کنندہ %3$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 签名人 %@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 簽署人 %@" + } + } + } + }, + "Open file" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open file" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فتح ملف" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datei öffnen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir archivo" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "باز کردن فایل" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrir un fichier" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फ़ाइल खोलें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buka berkas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri file" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルを開く" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 열기" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buka fail" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz plik" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "فایل خلاص کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir arquivo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открыть файл" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கோப்பைத் திற" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "فائل کھولیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开文件" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟檔案" + } + } + } + }, + "Checking…" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking…" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "جارٍ التحقق…" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prüfe …" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobando…" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "در حال بررسی…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérification…" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जाँच हो रही है…" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Memeriksa…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica in corso…" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認中…" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인하는 중…" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menyemak…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdzanie…" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "څېړل کېږي…" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificando…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверка…" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "சரிபார்க்கிறது…" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "جانچ ہو رہی ہے…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在核对…" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在核對…" + } + } + } + }, + "Check proposal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check proposal" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "التحقق من الاقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorschlag prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی پیشنهاد" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier la proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रस्ताव जाँचें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periksa proposal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica la proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "提案を確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제안 확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semak cadangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź propozycję" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وړاندیز وڅېړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить предложение" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முன்மொழிவைச் சரிபார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تجویز جانچیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "核对提案" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "核對提案" + } + } + } + }, + "What this proposal does, as checked against your policy" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What this proposal does, as checked against your policy" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ما يفعله هذا الاقتراح، مُتحقَّقًا منه مقابل سياستك" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Was dieser Vorschlag tut, geprüft gegen deine Richtlinie" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lo que hace esta propuesta, comprobado contra tu política" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیشنهاد چه می‌کند، بررسی‌شده در برابر سیاست شما" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce que fait cette proposition, vérifié par rapport à votre politique" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्रस्ताव क्या करता है, आपकी पॉलिसी के विरुद्ध जाँचा गया" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apa yang dilakukan proposal ini, diperiksa terhadap kebijakan Anda" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cosa fa questa proposta, verificato rispetto alla tua policy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この提案が行うこと (あなたのポリシーと照合済み)" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 제안이 하는 일 (정책과 대조해 확인됨)" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apa yang cadangan ini lakukan, disemak terhadap dasar anda" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Co robi ta propozycja, sprawdzone względem Twojej polityki" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا وړاندیز څه کوي، ستاسو د پالیسۍ پر وړاندې څېړل شوی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O que esta proposta faz, verificado contra a sua política" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Что делает это предложение — проверено по вашей политике" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்த முன்மொழிவு என்ன செய்கிறது, உங்கள் கொள்கைக்கு எதிராகச் சரிபார்க்கப்பட்டது" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ تجویز کیا کرتی ہے، آپ کی پالیسی کے مقابل جانچا گیا" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案的作用(已对照你的策略核对)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此提案的作用(已對照你的政策核對)" + } + } + } + }, + "Your share — send it to whoever proves the proposal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your share — send it to whoever proves the proposal" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حصة توقيعك — أرسلها إلى من يُثبت الاقتراح" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dein Signaturanteil — schicke ihn an die Person, die den Vorschlag beweist" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tu parte de firma — envíasela a quien pruebe la propuesta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "سهم امضای شما — آن را برای کسی بفرستید که پیشنهاد را اثبات می‌کند" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Votre part de signature — envoyez-la à qui prouve la proposition" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपका हस्ताक्षर अंश — इसे उसे भेजें जो प्रस्ताव प्रूव करता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bagian tanda tangan Anda — kirim ke yang membuktikan proposal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La tua quota di firma — mandala a chi dimostra la proposta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "あなたの署名シェア — 提案を証明する人に送ってください" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "내 서명 조각 — 제안을 증명하는 사람에게 보내세요" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bahagian tandatangan anda — hantar kepada yang membuktikan cadangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twój udział podpisu — wyślij go temu, kto dowodzi propozycji" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "ستاسو د لاسلیک برخه — هغه چا ته یې ولېږئ چې وړاندیز ثابتوي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sua parte da assinatura — envie-a a quem prova a proposta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваша доля подписи — отправьте её тому, кто доказывает предложение" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "உங்கள் கையொப்பப் பங்கு — முன்மொழிவை நிரூபிப்பவருக்கு அனுப்புங்கள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "آپ کا دستخطی حصہ — اسے اس کو بھیجیں جو تجویز ثابت کرتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的签名份额 — 请发给负责证明提案的人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的簽署份額 — 請交給負責證明提案的人" + } + } + } + }, + "index:hex" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "index:hex" + } + } + } + }, + "Check share" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check share" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "التحقق من حصة التوقيع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signaturanteil prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar parte de firma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی سهم امضا" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier la part de signature" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षर अंश जाँचें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periksa bagian tanda tangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica la quota di firma" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名シェアを確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명 조각 확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semak bahagian tandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź udział podpisu" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک برخه وڅېړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar parte da assinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить долю подписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பப் பங்கைச் சரிபார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخطی حصہ جانچیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "核对签名份额" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "核對簽署份額" + } + } + } + }, + "Signer %@'s share verifies. %@ of %@ in hand: signers %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signer %@'s share verifies. %@ of %@ in hand: signers %@." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حصة توقيع الموقّع %@ صحيحة. لديك %@ من %@: الموقّعون %@." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Signaturanteil von Unterzeichner %@ ist gültig. %@ von %@ liegen vor: Unterzeichner %@." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La parte de firma del firmante %@ es válida. Tienes %@ de %@: firmantes %@." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "سهم امضای امضاکننده %@ معتبر است. %@ از %@ در دست است: امضاکنندگان %@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La part de signature du signataire %@ est valide. %@ sur %@ réunies : signataires %@." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता %1$@ का हस्ताक्षर अंश मान्य है। %3$@ में से %2$@ हाथ में हैं: हस्ताक्षरकर्ता %4$@।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bagian tanda tangan penanda tangan %@ valid. %@ dari %@ sudah terkumpul: penanda tangan %@." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "La quota di firma del firmatario %@ è valida. %@ di %@ in mano: firmatari %@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者 %1$@ の署名シェアは有効です。%3$@ 件中 %2$@ 件が揃っています: 署名者 %4$@。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 %1$@의 서명 조각이 유효합니다. %3$@개 중 %2$@개 확보: 서명자 %4$@." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bahagian tandatangan penandatangan %@ sah. %@ daripada %@ sudah ada: penandatangan %@." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Udział podpisu sygnatariusza %@ jest poprawny. Masz %@ z %@: sygnatariusze %@." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي %1$@ د لاسلیک برخه سمه ده. د %3$@ څخه %2$@ په لاس کې دي: لاسلیک کوونکي %4$@." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "A parte da assinatura do signatário %@ é válida. %@ de %@ em mãos: signatários %@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Доля подписи подписанта %@ верна. Собрано %@ из %@: подписанты %@." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் %1$@-இன் கையொப்பப் பங்கு சரியானது. %3$@-இல் %2$@ கையில் உள்ளன: கையொப்பமிடுபவர்கள் %4$@." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ %1$@ کا دستخطی حصہ درست ہے۔ %3$@ میں سے %2$@ ہاتھ میں ہیں: دستخط کنندگان %4$@۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人 %@ 的签名份额验证通过。已收集 %@/%@:签名人 %@。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人 %@ 的簽署份額驗證通過。已收集 %@/%@:簽署人 %@。" + } + } + } + }, + "That file is larger than a proposal, a policy or a key file can be." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "That file is larger than a proposal, a policy or a key file can be." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا الملف أكبر مما يمكن أن يكون عليه اقتراح أو سياسة أو ملف مفتاح." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Datei ist größer, als ein Vorschlag, eine Richtlinie oder eine Schlüsseldatei sein kann." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ese archivo es mayor de lo que puede ser una propuesta, una política o un archivo de clave." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آن فایل از اندازه‌ای که یک پیشنهاد، سیاست یا فایل کلید می‌تواند داشته باشد بزرگ‌تر است." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce fichier est plus grand que ne peut l'être une proposition, une politique ou un fichier de clé." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वह फ़ाइल किसी प्रस्ताव, पॉलिसी या की फ़ाइल के संभव आकार से बड़ी है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berkas itu lebih besar dari yang mungkin untuk sebuah proposal, kebijakan, atau berkas kunci." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quel file è più grande di quanto possa essere una proposta, una policy o un file di chiave." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのファイルは、提案・ポリシー・キーファイルとして許容されるサイズを超えています。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일이 제안, 정책, 키 파일로 허용되는 크기보다 큽니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail itu lebih besar daripada yang boleh bagi sesebuah cadangan, dasar atau fail kunci." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ten plik jest większy, niż może być propozycja, polityka lub plik klucza." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا فایل له هغه څخه لوی دی چې یو وړاندیز، پالیسي یا د کیلي فایل کېدای شي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esse arquivo é maior do que uma proposta, uma política ou um arquivo de chave pode ser." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Этот файл больше, чем может быть предложение, политика или файл ключа." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அந்தக் கோப்பு ஒரு முன்மொழிவு, கொள்கை அல்லது விசைக் கோப்பு இருக்கக்கூடியதை விடப் பெரியது." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "وہ فائل کسی تجویز، پالیسی یا کی فائل کے ممکنہ سائز سے بڑی ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "该文件大于提案、策略或密钥文件所允许的大小。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "該檔案大於提案、政策或金鑰檔案所容許的大小。" + } + } + } + }, + "Add payment" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add payment" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إضافة دفعة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlung hinzufügen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir pago" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "افزودن پرداخت" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un paiement" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान जोड़ें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambah pembayaran" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi pagamento" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払いを追加" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제 추가" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambah pembayaran" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj płatność" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "تادیه زیاته کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar pagamento" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить платёж" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கட்டணத்தைச் சேர்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ادائیگی شامل کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加付款" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增付款" + } + } + } + }, + "Check payments" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check payments" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "التحقق من المدفوعات" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlungen prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar pagos" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی پرداخت‌ها" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier les paiements" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान जाँचें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periksa pembayaran" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica i pagamenti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払いを確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제 확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semak pembayaran" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź płatności" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "تادیات وڅېړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar pagamentos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить платежи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கட்டணங்களைச் சரிபார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ادائیگیاں جانچیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "核对付款" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "核對付款" + } + } + } + }, + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amounts cannot be checked for non-RAND payments: leave the amount empty for those." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يمكن التحقق من المبالغ في المدفوعات بغير RAND: اترك المبلغ فارغًا لها." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beträge können bei Zahlungen, die nicht in RAND erfolgen, nicht geprüft werden: Lass den Betrag dort leer." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los montos no se pueden comprobar en pagos que no son en RAND: deja el monto vacío en esos." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار پرداخت‌های غیر RAND قابل بررسی نیست: مقدار را برای آن‌ها خالی بگذارید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les montants ne peuvent pas être vérifiés pour les paiements hors RAND : laissez le montant vide pour ceux-là." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAND के अलावा अन्य भुगतानों की राशि जाँची नहीं जा सकती: उनके लिए राशि खाली छोड़ें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jumlah tidak dapat diperiksa untuk pembayaran non-RAND: kosongkan jumlahnya untuk pembayaran tersebut." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gli importi non si possono verificare per i pagamenti non in RAND: lascia vuoto l'importo per quelli." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAND 以外の支払いは金額を確認できません。その場合は金額を空欄にしてください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAND가 아닌 결제는 금액을 확인할 수 없습니다. 이 경우 금액을 비워 두세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amaun tidak boleh disemak untuk pembayaran bukan RAND: biarkan amaun kosong bagi pembayaran itu." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kwot nie można sprawdzić dla płatności innych niż w RAND: zostaw dla nich kwotę pustą." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د غیر RAND تادیاتو اندازه نشي څېړل کېدای: د هغوی لپاره اندازه خالي پرېږدئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os valores não podem ser verificados em pagamentos que não são em RAND: deixe o valor vazio nesses." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Суммы нельзя проверить для платежей не в RAND: оставьте для них сумму пустой." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAND அல்லாத கட்டணங்களின் தொகையைச் சரிபார்க்க முடியாது: அவற்றுக்குத் தொகையை வெறுமையாக விடுங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "غیر RAND ادائیگیوں کی رقم جانچی نہیں جا سکتی: ان کے لیے رقم خالی چھوڑیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "非 RAND 付款无法核对金额:这类付款请将金额留空。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "非 RAND 付款無法核對金額:該類付款請將金額留空。" + } + } + } + }, + "Payee address" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Payee address" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عنوان المستلم" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adresse des Zahlungsempfängers" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dirección del beneficiario" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "آدرس دریافت‌کننده" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adresse du bénéficiaire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्राप्तकर्ता का पता" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alamat penerima pembayaran" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Indirizzo del beneficiario" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "受取人のアドレス" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "수취인 주소" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alamat penerima bayaran" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adres odbiorcy" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ترلاسه کوونکي پته" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Endereço do beneficiário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Адрес получателя" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "பெறுபவர் முகவரி" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "وصول کنندہ کا پتہ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "收款人地址" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "收款人地址" + } + } + } + }, + "Remove" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إزالة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entfernen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eliminar" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Supprimer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हटाएँ" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hapus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rimuovi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "削除" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "삭제" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لرې یې کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remover" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "நீக்கு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ہٹائیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除" + } + } + } + }, + "Amount in RAND" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amount in RAND" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المبلغ بعملة RAND" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Betrag in RAND" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Monto en RAND" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "مقدار به RAND" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Montant en RAND" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "राशि (RAND में)" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jumlah dalam RAND" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importo in RAND" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額 (RAND)" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "금액 (RAND)" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amaun dalam RAND" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kwota w RAND" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "په RAND کې اندازه" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valor em RAND" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сумма в RAND" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "RAND-இல் தொகை" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "رقم (RAND میں)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "金额(RAND)" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "金額(RAND)" + } + } + } + }, + "Intended payments" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intended payments" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المدفوعات المقصودة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorgesehene Zahlungen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pagos previstos" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پرداخت‌های موردنظر" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paiements prévus" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इच्छित भुगतान" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pembayaran yang dimaksud" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pagamenti previsti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "予定している支払い" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "예정된 결제" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pembayaran yang dimaksudkan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zamierzone płatności" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "موخه شوې تادیات" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pagamentos pretendidos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предполагаемые платежи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "நோக்கிய கட்டணங்கள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "مطلوبہ ادائیگیاں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "拟进行的付款" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "擬進行的付款" + } + } + } + }, + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أدخل كل دفعة تنوي إجراءها: العنوان والمبلغ بعملة RAND. الدفعة إلى العنوان نفسه مرتين تُدخل مرة واحدة بالمجموع. لا يمكن توقيع مخرج لا يطابق أيًّا منها." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib jede Zahlung ein, die du leisten willst: die Adresse und den Betrag in RAND. Eine Zahlung, die zweimal an dieselbe Adresse geht, wird einmal mit der Summe eingetragen. Eine Ausgabe, die zu keiner davon passt, kann nicht signiert werden." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Introduce cada pago que piensas hacer: la dirección y el monto en RAND. Un pago a la misma dirección dos veces se introduce una sola vez, con el total. Una salida que no coincide con ninguno no se puede firmar." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "هر پرداختی را که قصد انجامش را دارید وارد کنید: آدرس و مقدار به RAND. دو پرداخت به یک آدرس یک‌بار و با مجموع وارد می‌شود. خروجی‌ای که با هیچ‌کدام مطابقت نداشته باشد قابل امضا نیست." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saisissez chaque paiement que vous comptez faire : l'adresse et le montant en RAND. Un paiement à la même adresse deux fois est saisi une seule fois, avec le total. Une sortie qui ne correspond à aucun ne peut pas être signée." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जो भी भुगतान आप करना चाहते हैं, हर एक दर्ज करें: पता और RAND में राशि। एक ही पते पर दो बार भुगतान कुल राशि के साथ एक बार दर्ज होता है। जो आउटपुट इनमें से किसी से मेल नहीं खाता, उस पर हस्ताक्षर नहीं हो सकते।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan setiap pembayaran yang akan Anda lakukan: alamat dan jumlah dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan satu kali, dengan totalnya. Output yang tidak cocok dengan satu pun tidak dapat ditandatangani." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inserisci ogni pagamento che intendi fare: l'indirizzo e l'importo in RAND. Un pagamento allo stesso indirizzo due volte si inserisce una volta sola, con il totale. Un output che non corrisponde a nessuno non può essere firmato." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "行う予定の支払いをすべて入力してください。アドレスと RAND 建ての金額です。同じアドレスへの支払いが2回ある場合は、合計額で1件として入力します。どれにも一致しない出力には署名できません。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보내려는 모든 결제를 입력하세요: 주소와 RAND 금액입니다. 같은 주소로 두 번 결제하는 경우 합계로 한 번만 입력합니다. 어느 것과도 일치하지 않는 출력은 서명할 수 없습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Masukkan setiap pembayaran yang anda ingin buat: alamat dan amaun dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan sekali, dengan jumlahnya. Output yang tidak sepadan dengan mana-mana tidak boleh ditandatangani." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wpisz każdą płatność, którą zamierzasz wykonać: adres i kwotę w RAND. Płatność dwa razy na ten sam adres wpisuje się raz, z sumą. Wyjścia niepasującego do żadnej z nich nie można podpisać." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "هره تادیه چې کول یې غواړئ ولیکئ: پته او په RAND کې اندازه. هماغې پتې ته دوه ځله تادیه یو ځل د ټولیز مجموعې سره لیکل کېږي. هغه محصول چې له هیڅ یوې سره نه برابرېږي لاسلیک نشي کېدای." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe cada pagamento que você pretende fazer: o endereço e o valor em RAND. Um pagamento ao mesmo endereço duas vezes é informado uma vez só, com o total. Uma saída que não corresponde a nenhum não pode ser assinada." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите каждый платёж, который вы собираетесь сделать: адрес и сумму в RAND. Платёж на один и тот же адрес дважды вводится один раз, с итоговой суммой. Выход, не совпадающий ни с одним из них, подписать нельзя." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "நீங்கள் செய்யவிருக்கும் ஒவ்வொரு கட்டணத்தையும் உள்ளிடுங்கள்: முகவரியும் RAND-இல் தொகையும். ஒரே முகவரிக்கு இருமுறை செலுத்தும் கட்டணம் மொத்தத்துடன் ஒருமுறை உள்ளிடப்படும். இவற்றில் எதனுடனும் பொருந்தாத வெளியீட்டில் கையொப்பமிட முடியாது." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "جو بھی ادائیگی آپ کرنا چاہتے ہیں ہر ایک درج کریں: پتہ اور RAND میں رقم۔ ایک ہی پتے پر دو بار ادائیگی کل رقم کے ساتھ ایک بار درج ہوتی ہے۔ جو آؤٹ پٹ ان میں سے کسی سے مطابقت نہیں رکھتا اس پر دستخط نہیں ہو سکتے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入你打算进行的每一笔付款:地址和以 RAND 计的金额。向同一地址的两笔付款只输入一次,填合计金额。与其中任何一笔都不匹配的输出无法签名。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入你打算進行的每一筆付款:地址及以 RAND 計的金額。向同一地址的兩筆付款只輸入一次,填合計金額。與其中任何一筆都不相符的輸出無法簽署。" + } + } + } + }, + "Fee: %@ RAND · expires at height %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fee: %@ RAND · expires at height %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الرسوم: %@ RAND · تنتهي عند الارتفاع %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gebühr: %@ RAND · läuft bei Höhe %@ ab" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comisión: %@ RAND · caduca en la altura %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "کارمزد: %@ RAND · در ارتفاع %@ منقضی می‌شود" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frais : %@ RAND · expire à la hauteur %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "शुल्क: %@ RAND · ऊँचाई %@ पर समाप्त" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biaya: %@ RAND · kedaluwarsa pada ketinggian %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Commissione: %@ RAND · scade all'altezza %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手数料: %@ RAND · 高さ %@ で期限切れ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "수수료: %@ RAND · 높이 %@에서 만료" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yuran: %@ RAND · tamat tempoh pada ketinggian %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opłata: %@ RAND · wygasa na wysokości %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "فیس: %@ RAND · په %@ لوړوالي کې پای ته رسېږي" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taxa: %@ RAND · expira na altura %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Комиссия: %@ RAND · истекает на высоте %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கட்டணம்: %@ RAND · உயரம் %@-இல் காலாவதியாகும்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "فیس: %@ RAND · اونچائی %@ پر ختم" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手续费:%@ RAND · 在高度 %@ 过期" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "手續費:%@ RAND · 於高度 %@ 過期" + } + } + } + }, + "%@ of %@ payments matched." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ of %@ payments matched." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تطابقت %@ من أصل %@ من المدفوعات." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ von %@ Zahlungen stimmen überein." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ pagos coinciden." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ از %@ پرداخت مطابقت دارد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ paiements sur %@ correspondent." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ में से %1$@ भुगतान मेल खाते हैं।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dari %@ pembayaran cocok." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ pagamenti su %@ corrispondono." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ 件中 %1$@ 件の支払いが一致しました。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제 %2$@건 중 %1$@건이 일치합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ daripada %@ pembayaran sepadan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zgodnych płatności: %@ z %@." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د %2$@ تادیاتو څخه %1$@ سره برابر شول." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ pagamentos conferem." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Совпало платежей: %@ из %@." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ கட்டணங்களில் %1$@ பொருந்துகின்றன." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ میں سے %1$@ ادائیگیاں مطابقت رکھتی ہیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ 笔付款中有 %1$@ 笔匹配。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ 筆付款中有 %1$@ 筆相符。" + } + } + } + }, + "Retry" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إعادة المحاولة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut versuchen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reintentar" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "تلاش دوباره" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Réessayer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "दोबारा कोशिश करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Coba lagi" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riprova" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再試行" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다시 시도" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuba lagi" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ponów" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "بیا هڅه وکړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tentar de novo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Повторить" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மீண்டும் முயல்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دوبارہ کوشش کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "重試" + } + } + } + }, + "%@ blocks left to prove it." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ blocks left to prove it." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بقي %@ كتلة لإثباته." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noch %@ Blöcke, um es zu beweisen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quedan %@ bloques para probarlo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ بلوک مانده تا اثبات شود." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il reste %@ blocs pour la prouver." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इसे प्रूव करने के लिए %@ ब्लॉक बाकी हैं।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tersisa %@ blok untuk membuktikannya." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mancano %@ blocchi per dimostrarlo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "証明まであと %@ ブロックです。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "증명까지 %@ 블록 남았습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tinggal %@ blok lagi untuk membuktikannya." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zostało %@ bloków, aby to udowodnić." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ثبوت لپاره %@ بلاکونه پاتې دي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Faltam %@ blocos para prová-la." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Осталось %@ блоков, чтобы доказать это." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இதை நிரூபிக்க இன்னும் %@ பிளாக்குகள் உள்ளன." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اسے ثابت کرنے کے لیے %@ بلاک باقی ہیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还差 %@ 个区块即可证明。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚欠 %@ 個區塊便可證明。" + } + } + } + }, + "Payments changed: check them again before signing." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Payments changed: check them again before signing." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تغيّرت المدفوعات: تحقق منها مجددًا قبل التوقيع." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zahlungen geändert: Prüfe sie vor dem Signieren erneut." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los pagos cambiaron: compruébalos de nuevo antes de firmar." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پرداخت‌ها تغییر کردند: پیش از امضا دوباره بررسی‌شان کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les paiements ont changé : vérifiez-les de nouveau avant de signer." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "भुगतान बदल गए: हस्ताक्षर से पहले उन्हें फिर जाँचें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pembayaran berubah: periksa lagi sebelum menandatangani." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I pagamenti sono cambiati: verificali di nuovo prima di firmare." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "支払いが変更されました。署名する前にもう一度確認してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결제가 변경되었습니다. 서명하기 전에 다시 확인하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pembayaran berubah: semak semula sebelum menandatangani." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Płatności się zmieniły: sprawdź je ponownie przed podpisaniem." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "تادیات بدل شول: مخکې له لاسلیک بیا یې وڅېړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os pagamentos mudaram: verifique-os de novo antes de assinar." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Платежи изменились: проверьте их снова перед подписью." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கட்டணங்கள் மாறின: கையொப்பமிடும் முன் மீண்டும் சரிபாருங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ادائیگیاں بدل گئیں: دستخط سے پہلے انہیں دوبارہ جانچیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "付款已变更:签名前请重新核对。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "付款已更改:簽署前請重新核對。" + } + } + } + }, + "Signing…" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signing…" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "جارٍ التوقيع…" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signiere …" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmando…" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "در حال امضا…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signature…" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षर हो रहा है…" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menandatangani…" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firma in corso…" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名中…" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명하는 중…" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menandatangani…" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podpisywanie…" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لاسلیک کېږي…" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assinando…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подписание…" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுகிறது…" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط ہو رہے ہیں…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在签名…" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在簽署…" + } + } + } + }, + "Sign" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "توقيع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmar" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضا" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षर करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangani" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firma" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podpisz" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لاسلیک" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assinar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подписать" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署" + } + } + } + }, + "Type the last four characters of the account fingerprint" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Type the last four characters of the account fingerprint" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اكتب آخر أربعة أحرف من بصمة الحساب" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gib die letzten vier Zeichen des Konto-Fingerabdrucks ein" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escribe los últimos cuatro caracteres de la huella de la cuenta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "چهار نویسه آخر اثر انگشت حساب را بنویسید" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saisissez les quatre derniers caractères de l'empreinte du compte" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "खाते के फ़िंगरप्रिंट के अंतिम चार अक्षर लिखें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ketik empat karakter terakhir sidik jari akun" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digita gli ultimi quattro caratteri dell'impronta dell'account" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アカウントのフィンガープリントの末尾4文字を入力してください" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계정 지문의 마지막 네 글자를 입력하세요" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taip empat aksara terakhir cap jari akaun" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wpisz ostatnie cztery znaki odcisku konta" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د حساب د ګوتنښې وروستي څلور توري ولیکئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digite os últimos quatro caracteres da impressão digital da conta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Введите последние четыре символа отпечатка аккаунта" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கணக்கு கைரேகையின் கடைசி நான்கு எழுத்துகளை உள்ளிடுங்கள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اکاؤنٹ کے فنگر پرنٹ کے آخری چار حروف لکھیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入账户指纹的后四个字符" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請輸入帳戶指紋的最後四個字元" + } + } + } + }, + "Save as file" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save as file" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حفظ كملف" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Als Datei speichern" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar como archivo" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "ذخیره به‌صورت فایل" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistrer comme fichier" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फ़ाइल के रूप में सहेजें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simpan sebagai berkas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salva come file" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイルとして保存" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일로 저장" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simpan sebagai fail" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisz jako plik" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د فایل په توګه خوندي کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvar como arquivo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранить как файл" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கோப்பாகச் சேமி" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "فائل کے طور پر محفوظ کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "另存为文件" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "另存為檔案" + } + } + } + }, + "Sign a multisig proposal" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign a multisig proposal" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "توقيع اقتراح متعدد التوقيع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Multisig-Vorschlag signieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firmar una propuesta multifirma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "امضای یک پیشنهاد چندامضایی" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signer une proposition multisig" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मल्टीसिग प्रस्ताव पर हस्ताक्षर करें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangani proposal multisig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Firma una proposta multisig" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マルチシグの提案に署名" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "멀티시그 제안에 서명" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tandatangani cadangan multisig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podpisz propozycję multisig" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ملټي‌سېګ وړاندیز لاسلیک کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Assinar uma proposta multiassinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подписать предложение мультиподписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மல்டிசிக் முன்மொழிவில் கையொப்பமிடு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملٹی سگ تجویز پر دستخط کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签署多签提案" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署多重簽署提案" + } + } + } + }, + "this signer is already saved as %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "this signer is already saved as %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا الموقّع محفوظ مسبقًا باسم %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "dieser Unterzeichner ist bereits als %@ gespeichert" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "este firmante ya está guardado como %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این امضاکننده از قبل با نام %@ ذخیره شده است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ce signataire est déjà enregistré sous le nom %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह हस्ताक्षरकर्ता पहले से %@ के रूप में सहेजा गया है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "penanda tangan ini sudah tersimpan sebagai %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "questo firmatario è già salvato come %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この署名者はすでに %@ として保存されています" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 서명자는 이미 %@(으)로 저장되어 있습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "penandatangan ini sudah disimpan sebagai %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ten sygnatariusz jest już zapisany jako %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا لاسلیک کوونکی لا دمخه د %@ په نامه خوندي دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "este signatário já está salvo como %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "этот подписант уже сохранён как %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்தக் கையொப்பமிடுபவர் ஏற்கெனவே %@ என்று சேமிக்கப்பட்டுள்ளார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ دستخط کنندہ پہلے سے %@ کے طور پر محفوظ ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此签名人已保存为 %@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此簽署人已儲存為 %@" + } + } + } + }, + "a signer label is 1-64 characters" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "a signer label is 1-64 characters" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تسمية الموقّع من 1-64 حرفًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "eine Unterzeichner-Bezeichnung hat 1-64 Zeichen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "la etiqueta de un firmante tiene 1-64 caracteres" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "برچسب امضاکننده ۱-۶۴ نویسه است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "une étiquette de signataire compte 1-64 caractères" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता का लेबल 1-64 अक्षरों का होता है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "label penanda tangan terdiri dari 1-64 karakter" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "l'etichetta di un firmatario è di 1-64 caratteri" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者のラベルは 1-64 文字です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 레이블은 1-64자입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "label penandatangan ialah 1-64 aksara" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "etykieta sygnatariusza ma 1-64 znaki" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي نښه ۱-۶۴ توري ده" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "o rótulo de um signatário tem 1-64 caracteres" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "метка подписанта — 1-64 символа" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் லேபிள் 1-64 எழுத்துகள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کا لیبل 1-64 حروف کا ہوتا ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人标签为 1-64 个字符" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人標籤為 1-64 個字元" + } + } + } + }, + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عيّن رمز دخول للجهاز أولًا: لا يُقرأ مفتاح الموقّع إلا بعد Face ID أو Touch ID أو رمز الدخول." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lege zuerst einen Geräte-Code fest: Ein Unterzeichner-Schlüssel wird erst nach Face ID, Touch ID oder dem Code gelesen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primero configura un código en el dispositivo: la clave de firmante solo se lee tras Face ID, Touch ID o el código." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "ابتدا برای دستگاه کد عبور تنظیم کنید: کلید امضاکننده فقط پس از Face ID، Touch ID یا کد عبور خوانده می‌شود." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Définissez d'abord un code sur l'appareil : une clé de signataire n'est lue qu'après Face ID, Touch ID ou le code." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पहले डिवाइस का पासकोड सेट करें: हस्ताक्षरकर्ता की कुंजी Face ID, Touch ID या पासकोड के बाद ही पढ़ी जाती है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setel kode sandi perangkat terlebih dahulu: kunci penanda tangan hanya dibaca setelah Face ID, Touch ID, atau kode sandi." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imposta prima un codice sul dispositivo: la chiave di un firmatario viene letta solo dopo Face ID, Touch ID o il codice." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "先にデバイスのパスコードを設定してください。署名者の鍵は Face ID、Touch ID、またはパスコードの後にのみ読み取られます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "먼저 기기 암호를 설정하세요. 서명자 키는 Face ID, Touch ID 또는 암호 확인 후에만 읽습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tetapkan kod laluan peranti dahulu: kunci penandatangan hanya dibaca selepas Face ID, Touch ID atau kod laluan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Najpierw ustaw kod urządzenia: klucz sygnatariusza jest odczytywany dopiero po Face ID, Touch ID lub kodzie." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لومړی د وسیلې پاس کوډ وټاکئ: د لاسلیک کوونکي کلیځ یوازې د Face ID، Touch ID یا پاس کوډ وروسته لوستل کېږي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Defina primeiro um código no dispositivo: a chave de signatário só é lida depois do Face ID, do Touch ID ou do código." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сначала задайте код-пароль устройства: ключ подписанта читается только после Face ID, Touch ID или код-пароля." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "முதலில் சாதனக் கடவுக்குறியீட்டை அமைக்கவும்: கையொப்பமிடுபவர் விசை Face ID, Touch ID அல்லது கடவுக்குறியீட்டுக்குப் பிறகே படிக்கப்படும்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "پہلے ڈیوائس کا پاس کوڈ سیٹ کریں: دستخط کنندہ کی کلید صرف Face ID، Touch ID یا پاس کوڈ کے بعد پڑھی جاتی ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请先设置设备密码:签名人密钥只有在通过 Face ID、Touch ID 或设备密码后才会被读取。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請先設定裝置密碼:簽署人密鑰只有在通過 Face ID、Touch ID 或密碼後才會被讀取。" + } + } + } + }, + "No such signer profile." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No such signer profile." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يوجد ملف موقّع كهذا." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieses Unterzeichnerprofil gibt es nicht." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No existe ese perfil de firmante." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "چنین نمایه امضاکننده‌ای وجود ندارد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce profil de signataire n'existe pas." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऐसी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil penanda tangan itu tidak ada." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun profilo di firmatario del genere." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのような署名者プロファイルはありません。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "해당 서명자 프로필이 없습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiada profil penandatangan sedemikian." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie ma takiego profilu sygnatariusza." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "داسې د لاسلیک کوونکي پروفایل نشته." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não existe esse perfil de signatário." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Такого профиля подписанта нет." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அப்படி ஒரு கையொப்பமிடுபவர் சுயவிவரம் இல்லை." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ایسی کوئی دستخط کنندہ پروفائل نہیں ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有这个签名人资料。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有這個簽署人資料。" + } + } + } + }, + "This signer profile has no key on this device: remove it and add it again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This signer profile has no key on this device: remove it and add it again." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملف الموقّع هذا لا مفتاح له على هذا الجهاز: أزله وأضفه من جديد." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieses Unterzeichnerprofil hat auf diesem Gerät keinen Schlüssel: Entferne es und füge es erneut hinzu." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este perfil de firmante no tiene clave en este dispositivo: quítalo y vuelve a añadirlo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این نمایه امضاکننده روی این دستگاه کلیدی ندارد: آن را حذف کنید و دوباره اضافه کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce profil de signataire n'a pas de clé sur cet appareil : retirez-le et ajoutez-le de nouveau." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस हस्ताक्षरकर्ता प्रोफ़ाइल की इस डिवाइस पर कोई की नहीं है: इसे हटाकर फिर जोड़ें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil penanda tangan ini tidak memiliki kunci di perangkat ini: hapus lalu tambahkan lagi." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo profilo di firmatario non ha una chiave su questo dispositivo: rimuovilo e aggiungilo di nuovo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この署名者プロファイルには、この端末にキーがありません。削除してから追加し直してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 서명자 프로필에는 이 기기에 키가 없습니다. 제거한 뒤 다시 추가하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil penandatangan ini tiada kunci pada peranti ini: buangnya dan tambah semula." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ten profil sygnatariusza nie ma klucza na tym urządzeniu: usuń go i dodaj ponownie." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا د لاسلیک کوونکي پروفایل په دې وسیله کې کیلي نه لري: لرې یې کړئ او بیا یې زیات کړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este perfil de signatário não tem chave neste dispositivo: remova-o e adicione-o de novo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "У этого профиля подписанта нет ключа на этом устройстве: удалите его и добавьте снова." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்தக் கையொப்பமிடுபவர் சுயவிவரத்துக்கு இந்தச் சாதனத்தில் விசை இல்லை: அதை நீக்கி மீண்டும் சேருங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اس دستخط کنندہ پروفائل کی اس ڈیوائس پر کوئی کی نہیں ہے: اسے ہٹا کر دوبارہ شامل کریں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此签名人资料在此设备上没有密钥:请移除后重新添加。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此簽署人資料在此裝置上沒有金鑰:請移除後重新新增。" + } + } + } + }, + "No signer profiles yet" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No signer profiles yet" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد ملفات موقّعين بعد" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noch keine Unterzeichnerprofile" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aún no hay perfiles de firmante" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "هنوز نمایه امضاکننده‌ای نیست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun profil de signataire pour l'instant" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "अभी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belum ada profil penanda tangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ancora nessun profilo di firmatario" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者プロファイルはまだありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 프로필이 아직 없습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belum ada profil penandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak profili sygnatariuszy" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "تر اوسه د لاسلیک کوونکي پروفایلونه نشته" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ainda não há perfis de signatário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профилей подписантов пока нет" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் சுயவிவரங்கள் இன்னும் இல்லை" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ابھی کوئی دستخط کنندہ پروفائل نہیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚无签名人资料" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未有簽署人資料" + } + } + } + }, + "Add the policy file and your signer key to sign for a private multisig account." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add the policy file and your signer key to sign for a private multisig account." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أضف ملف السياسة ومفتاح الموقّع الخاص بك للتوقيع عن حساب متعدد التوقيع خاص." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Füge die Richtliniendatei und deinen Unterzeichner-Schlüssel hinzu, um für ein privates Multisig-Konto zu signieren." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añade el archivo de política y tu clave de firmante para firmar por una cuenta multifirma privada." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "فایل سیاست و کلید امضاکننده خود را اضافه کنید تا برای یک حساب چندامضایی خصوصی امضا کنید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajoutez le fichier de politique et votre clé de signataire pour signer pour un compte multisig privé." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "निजी मल्टीसिग खाते के लिए हस्ताक्षर करने हेतु पॉलिसी फ़ाइल और अपनी हस्ताक्षरकर्ता की जोड़ें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambahkan berkas kebijakan dan kunci penanda tangan Anda untuk menandatangani akun multisig privat." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi il file della policy e la tua chiave di firmatario per firmare per un account multisig privato." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "プライベートなマルチシグアカウントで署名するには、ポリシーファイルと署名者キーを追加してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "비공개 멀티시그 계정으로 서명하려면 정책 파일과 서명자 키를 추가하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambah fail dasar dan kunci penandatangan anda untuk menandatangani bagi akaun multisig persendirian." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj plik polityki i swój klucz sygnatariusza, aby podpisywać dla prywatnego konta multisig." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د خصوصي ملټي‌سېګ حساب لپاره د لاسلیک کولو لپاره د پالیسۍ فایل او خپله د لاسلیک کوونکي کیلي زیاته کړئ." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicione o arquivo de política e sua chave de signatário para assinar por uma conta multiassinatura privada." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавьте файл политики и ключ подписанта, чтобы подписывать за приватный мультиподписной аккаунт." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "தனிப்பட்ட மல்டிசிக் கணக்கிற்குக் கையொப்பமிட, கொள்கைக் கோப்பையும் உங்கள் கையொப்பமிடுபவர் விசையையும் சேருங்கள்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "نجی ملٹی سگ اکاؤنٹ کے لیے دستخط کرنے کی غرض سے پالیسی فائل اور اپنی دستخط کنندہ کی شامل کریں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加策略文件和你的签名人密钥,即可为私密多签账户签名。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增政策檔案及你的簽署人金鑰,即可為私密多重簽署帳戶簽署。" + } + } + } + }, + "Remove this signer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove this signer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إزالة هذا الموقّع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diesen Unterzeichner entfernen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar este firmante" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف این امضاکننده" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer ce signataire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस हस्ताक्षरकर्ता को हटाएँ" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hapus penanda tangan ini" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rimuovi questo firmatario" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この署名者を削除" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 서명자 제거" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buang penandatangan ini" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń tego sygnatariusza" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا لاسلیک کوونکی لرې کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remover este signatário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить этого подписанта" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்தக் கையொப்பமிடுபவரை நீக்கு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اس دستخط کنندہ کو ہٹائیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除此签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除此簽署人" + } + } + } + }, + "Remove a multisig signer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove a multisig signer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إزالة موقّع متعدد التوقيع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Einen Multisig-Unterzeichner entfernen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitar un firmante multifirma" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "حذف یک امضاکننده چندامضایی" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer un signataire multisig" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मल्टीसिग हस्ताक्षरकर्ता हटाएँ" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hapus penanda tangan multisig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rimuovi un firmatario multisig" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "マルチシグの署名者を削除" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "멀티시그 서명자 제거" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buang penandatangan multisig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usuń sygnatariusza multisig" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د ملټي‌سېګ لاسلیک کوونکی لرې کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remover um signatário multiassinatura" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить подписанта мультиподписи" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "மல்டிசிக் கையொப்பமிடுபவரை நீக்கு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملٹی سگ دستخط کنندہ ہٹائیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除多签签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除多重簽署簽署人" + } + } + } + }, + "Add a signer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add a signer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إضافة موقّع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterzeichner hinzufügen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Añadir un firmante" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "افزودن امضاکننده" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajouter un signataire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता जोड़ें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambah penanda tangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aggiungi un firmatario" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者を追加" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 추가" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tambah penandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dodaj sygnatariusza" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لاسلیک کوونکی زیات کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adicionar um signatário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добавить подписанта" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவரைச் சேர்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ شامل کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "新增簽署人" + } + } + } + }, + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يُحفظ ملف الموقّع في سلسلة مفاتيح (Keychain) هذا الهاتف. يطلب كل توقيع Face ID أو Touch ID أو رمز الدخول من جديد." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ein Unterzeichnerprofil wird im Schlüsselbund (Keychain) dieses iPhones gespeichert. Jede Signatur verlangt erneut Face ID, Touch ID oder deinen Code." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un perfil de firmante se guarda en el llavero (Keychain) de este teléfono. Cada firma vuelve a pedir Face ID, Touch ID o tu código." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "نمایهٔ امضاکننده در Keychain این گوشی نگه داشته می‌شود. هر امضا دوباره Face ID، Touch ID یا کد عبور شما را می‌خواهد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un profil de signataire est conservé dans le trousseau (Keychain) de ce téléphone. Chaque signature redemande Face ID, Touch ID ou votre code." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता प्रोफ़ाइल इस फ़ोन की Keychain में रखी जाती है। हर हस्ताक्षर पर फिर से Face ID, Touch ID या आपका पासकोड माँगा जाता है।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil penanda tangan disimpan di Keychain ponsel ini. Setiap tanda tangan meminta Face ID, Touch ID, atau kode sandi Anda lagi." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un profilo di firmatario è conservato nel portachiavi (Keychain) di questo telefono. Ogni firma chiede di nuovo Face ID, Touch ID o il tuo codice." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者プロファイルはこのiPhoneのキーチェーン(Keychain)に保存されます。署名のたびに Face ID、Touch ID、またはパスコードを再度求めます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 프로필은 이 휴대폰의 키체인(Keychain)에 보관됩니다. 서명할 때마다 Face ID, Touch ID 또는 암호를 다시 요청합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil penandatangan disimpan dalam Keychain telefon ini. Setiap tandatangan meminta Face ID, Touch ID atau kod laluan anda sekali lagi." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil sygnatariusza jest przechowywany w pęku kluczy (Keychain) tego telefonu. Każdy podpis ponownie wymaga Face ID, Touch ID lub kodu." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي پروفایل د دې تلیفون په Keychain کې ساتل کېږي. هر لاسلیک بیا Face ID، Touch ID یا ستاسو پاس کوډ غواړي." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Um perfil de signatário fica guardado no chaveiro (Keychain) deste telefone. Cada assinatura pede Face ID, Touch ID ou seu código de novo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Профиль подписанта хранится в связке ключей (Keychain) этого телефона. Каждая подпись снова запрашивает Face ID, Touch ID или ваш код-пароль." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் சுயவிவரம் இந்தத் தொலைபேசியின் Keychain-இல் வைக்கப்படுகிறது. ஒவ்வொரு கையொப்பமும் மீண்டும் Face ID, Touch ID அல்லது உங்கள் கடவுக்குறியீட்டைக் கேட்கும்." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کا پروفائل اس فون کی Keychain میں رکھا جاتا ہے۔ ہر دستخط پر دوبارہ Face ID، Touch ID یا آپ کا پاس کوڈ مانگا جاتا ہے۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人配置保存在这部手机的钥匙串(Keychain)中。每次签名都会再次要求 Face ID、Touch ID 或设备密码。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人設定檔保存在這部手機的鑰匙圈(Keychain)中。每次簽署都會再次要求 Face ID、Touch ID 或密碼。" + } + } + } + }, + "Policy file" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Policy file" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملف السياسة" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Richtliniendatei" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo de política" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "فایل سیاست" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fichier de politique" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पॉलिसी फ़ाइल" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berkas kebijakan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "File della policy" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポリシーファイル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "정책 파일" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail dasar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plik polityki" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د پالیسۍ فایل" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivo de política" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Файл политики" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கொள்கைக் கோப்பு" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "پالیسی فائل" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "策略文件" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "政策檔案" + } + } + } + }, + "Signer key file, or its 64-hex seed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signer key file, or its 64-hex seed" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملف مفتاح الموقّع، أو بذرته من 64 حرفًا سداسيًا عشريًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterzeichner-Schlüsseldatei oder ihr Seed aus 64 Hex-Zeichen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo de clave de firmante, o su semilla de 64 hex" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "فایل کلید امضاکننده، یا seed ۶۴ نویسه‌ای هگز آن" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fichier de clé de signataire, ou sa graine de 64 hex" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता की फ़ाइल, या उसका 64-हेक्स सीड" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berkas kunci penanda tangan, atau seed 64 hex-nya" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "File della chiave del firmatario, o il suo seed di 64 hex" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者キーファイル、または 64 桁16進のシード" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 키 파일 또는 64자리 16진수 시드" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail kunci penandatangan, atau seed 64-hex-nya" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plik klucza sygnatariusza lub jego seed w 64 znakach hex" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د لاسلیک کوونکي د کیلي فایل، یا د هغه ۶۴ هېکس seed" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivo de chave do signatário, ou sua seed de 64 hex" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Файл ключа подписанта или его seed из 64 hex-символов" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவர் விசைக் கோப்பு, அல்லது அதன் 64-ஹெக்ஸ் சீட்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ کی فائل، یا اس کا 64-ہیکس سیڈ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "签名人密钥文件,或其 64 位十六进制种子" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "簽署人金鑰檔案,或其 64 位十六進制種子" + } + } + } + }, + "Account fingerprint — compare it with every other signer before you save" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account fingerprint — compare it with every other signer before you save" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بصمة الحساب — قارنها مع كل موقّع آخر قبل أن تحفظ" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konto-Fingerabdruck — vergleiche ihn mit jedem anderen Unterzeichner, bevor du speicherst" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huella de la cuenta — compárala con la de cada otro firmante antes de guardar" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "اثر انگشت حساب — پیش از ذخیره، آن را با هر امضاکننده دیگر مقایسه کنید" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Empreinte du compte — comparez-la avec celle de chaque autre signataire avant d'enregistrer" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "खाते का फ़िंगरप्रिंट — सहेजने से पहले इसे हर दूसरे हस्ताक्षरकर्ता से मिलाएँ" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sidik jari akun — bandingkan dengan setiap penanda tangan lain sebelum menyimpan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impronta dell'account — confrontala con quella di ogni altro firmatario prima di salvare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アカウントのフィンガープリント — 保存する前に、他のすべての署名者と照合してください" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계정 지문 — 저장하기 전에 다른 모든 서명자와 비교하세요" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cap jari akaun — bandingkan dengan setiap penandatangan lain sebelum menyimpan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odcisk konta — porównaj go z odciskiem każdego innego sygnatariusza, zanim zapiszesz" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د حساب ګوتنښه — مخکې له خوندي کولو یې له هر بل لاسلیک کوونکي سره پرتله کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impressão digital da conta — compare-a com a de cada outro signatário antes de salvar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отпечаток аккаунта — сверьте его с каждым другим подписантом, прежде чем сохранять" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கணக்கு கைரேகை — சேமிப்பதற்கு முன் மற்ற ஒவ்வொரு கையொப்பமிடுபவருடனும் ஒப்பிடுங்கள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اکاؤنٹ کا فنگر پرنٹ — محفوظ کرنے سے پہلے اسے ہر دوسرے دستخط کنندہ سے ملائیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "账户指纹 — 保存前请与其他每位签名人核对" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "帳戶指紋 — 儲存前請與其他每位簽署人核對" + } + } + } + }, + "Label" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Label" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "التسمية" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezeichnung" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etiqueta" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "برچسب" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Étiquette" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लेबल" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Label" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etichetta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ラベル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "레이블" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Label" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etykieta" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "نښه" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rótulo" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Метка" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "லேபிள்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "لیبل" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "標籤" + } + } + } + }, + "Check" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحقق" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prüfen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Comprobar" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "بررسی" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vérifier" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जाँचें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periksa" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifica" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semak" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sprawdź" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "وڅېړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verificar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проверить" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "சரிபார்" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "جانچیں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "核对" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "核對" + } + } + } + }, + "%@ of %@ · this key is signer %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ of %@ · this key is signer %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ من %@ · هذا المفتاح هو الموقّع %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ von %@ · dieser Schlüssel ist Unterzeichner %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ · esta clave es el firmante %@" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ از %@ · این کلید امضاکننده %@ است" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sur %@ · cette clé est le signataire %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ में से %1$@ · यह की हस्ताक्षरकर्ता %3$@ है" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dari %@ · kunci ini adalah penanda tangan %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ di %@ · questa chiave è il firmatario %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · このキーは署名者 %@ です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 이 키는 서명자 %@입니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ daripada %@ · kunci ini ialah penandatangan %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ z %@ · ten klucz to sygnatariusz %@" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د %2$@ څخه %1$@ · دا کیلي لاسلیک کوونکی %3$@ دی" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ de %@ · esta chave é o signatário %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ из %@ · этот ключ — подписант %@" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@-இல் %1$@ · இந்த விசை கையொப்பமிடுபவர் %3$@" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "%2$@ میں سے %1$@ · یہ کی دستخط کنندہ %3$@ ہے" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 此密钥是签名人 %@" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ · 此金鑰是簽署人 %@" + } + } + } + }, + "Save signer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save signer" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حفظ الموقّع" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterzeichner speichern" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardar firmante" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "ذخیره امضاکننده" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistrer le signataire" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हस्ताक्षरकर्ता सहेजें" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simpan penanda tangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salva firmatario" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "署名者を保存" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서명자 저장" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simpan penandatangan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisz sygnatariusza" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "لاسلیک کوونکی خوندي کړئ" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvar signatário" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранить подписанта" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "கையொப்பமிடுபவரைச் சேமி" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "دستخط کنندہ محفوظ کریں" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存签名人" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "儲存簽署人" + } + } + } + }, + "That file could not be read." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "That file could not be read." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّرت قراءة هذا الملف." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Datei konnte nicht gelesen werden." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo leer ese archivo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این فایل خوانده نشد." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce fichier n'a pas pu être lu." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वह फ़ाइल पढ़ी नहीं जा सकी।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "File itu tidak dapat dibaca." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile leggere quel file." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "そのファイルを読み込めませんでした。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "해당 파일을 읽을 수 없습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail itu tidak dapat dibaca." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się odczytać tego pliku." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا فایل ونه لوستل شو." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível ler esse arquivo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось прочитать этот файл." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அந்தக் கோப்பைப் படிக்க முடியவில்லை." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "وہ فائل پڑھی نہ جا سکی۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法读取该文件。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法讀取該檔案。" + } + } + } + }, + "Remove %@?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove %@?" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إزالة %@؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ entfernen?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quitar %@?" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ حذف شود؟" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retirer %@ ?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ को हटाएँ?" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hapus %@?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rimuovere %@?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@を削除しますか?" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 서명자를 제거할까요?" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buang %@?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usunąć %@?" + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ لرې کړئ؟" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remover %@?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удалить %@?" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ஐ நீக்கவா?" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ کو ہٹائیں؟" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除%@?" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除%@?" + } + } + } + }, + "You need the signer's key file to add this signer back." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You need the signer's key file to add this signer back." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تحتاج إلى ملف مفتاح الموقّع لإضافته مرة أخرى." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du brauchst die Schlüsseldatei des Unterzeichners, um ihn wieder hinzuzufügen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesitas el archivo de clave del firmante para volver a añadirlo." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "برای افزودن دوبارهٔ این امضاکننده به فایل کلید او نیاز دارید." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il faut le fichier de clé du signataire pour le rajouter." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस हस्ताक्षरकर्ता को दोबारा जोड़ने के लिए आपको उसकी कुंजी फ़ाइल चाहिए होगी।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anda memerlukan file kunci penanda tangan untuk menambahkannya kembali." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serve il file della chiave del firmatario per aggiungerlo di nuovo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この署名者を再び追加するには、署名者の鍵ファイルが必要です。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 서명자를 다시 추가하려면 서명자의 키 파일이 필요합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anda memerlukan fail kunci penandatangan untuk menambahnya semula." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aby ponownie dodać tego sygnatariusza, potrzebujesz jego pliku klucza." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "د دې لاسلیک کوونکي د بیا زیاتولو لپاره ستاسو د هغه د کلیځ فایل ته اړتیا ده." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você precisa do arquivo de chave do signatário para adicioná-lo de novo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Чтобы снова добавить этого подписанта, нужен его файл ключа." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்தக் கையொப்பமிடுபவரை மீண்டும் சேர்க்க அவருடைய விசைக் கோப்பு தேவை." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "اس دستخط کنندہ کو دوبارہ شامل کرنے کے لیے آپ کو اس کی کلیدی فائل چاہیے ہوگی۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "要重新添加此签名人,需要他的密钥文件。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "要重新新增此簽署人,需要他的密鑰檔案。" + } + } + } + }, + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يتلقى هذا المستفيد RAND ورمزًا معًا: مدفوعات RAND والرموز إلى مستفيد واحد تحتاج إلى مقترحات منفصلة." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieser Empfänger erhält sowohl RAND als auch einen Token: RAND- und Token-Zahlungen an einen Empfänger brauchen getrennte Vorschläge." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este beneficiario recibe RAND y también un token: los pagos en RAND y en tokens a un mismo beneficiario necesitan propuestas separadas." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "این گیرنده هم RAND و هم یک توکن دریافت می‌کند: پرداخت‌های RAND و توکن به یک گیرنده به پیشنهادهای جداگانه نیاز دارند." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ce bénéficiaire reçoit à la fois des RAND et un jeton : les paiements en RAND et en jetons à un même bénéficiaire exigent des propositions séparées." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यह प्राप्तकर्ता RAND और एक टोकन दोनों प्राप्त करता है: एक ही प्राप्तकर्ता को RAND और टोकन भुगतानों के लिए अलग-अलग प्रस्ताव चाहिए।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Penerima ini menerima RAND sekaligus token: pembayaran RAND dan token ke satu penerima memerlukan proposal terpisah." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Questo beneficiario riceve sia RAND sia un token: i pagamenti in RAND e in token a uno stesso beneficiario richiedono proposte separate." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この受取人は RAND とトークンの両方を受け取ります。同じ受取人への RAND とトークンの支払いは、別々の提案にする必要があります。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 수취인은 RAND와 토큰을 모두 받습니다. 한 수취인에게 보내는 RAND 결제와 토큰 결제는 별도의 제안이 필요합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Penerima ini menerima RAND dan juga token: pembayaran RAND dan token kepada satu penerima memerlukan cadangan berasingan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ten odbiorca otrzymuje zarówno RAND, jak i token: płatności w RAND i w tokenach do jednego odbiorcy wymagają osobnych propozycji." + } + }, + "ps" : { + "stringUnit" : { + "state" : "translated", + "value" : "دا ترلاسه کوونکی RAND او یو ټوکن دواړه ترلاسه کوي: یو ترلاسه کوونکي ته د RAND او ټوکن تادیات جلا وړاندیزونو ته اړتیا لري." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Este beneficiário recebe RAND e também um token: pagamentos em RAND e em tokens a um mesmo beneficiário precisam de propostas separadas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Этот получатель получает и RAND, и токен: платежи в RAND и в токенах одному получателю требуют отдельных предложений." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "இந்தப் பெறுநர் RAND மற்றும் ஒரு டோக்கன் இரண்டையும் பெறுகிறார்: ஒரே பெறுநருக்கான RAND மற்றும் டோக்கன் கொடுப்பனவுகளுக்குத் தனித்தனி முன்மொழிவுகள் தேவை." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "یہ وصول کنندہ RAND اور ایک ٹوکن دونوں وصول کرتا ہے: ایک وصول کنندہ کو RAND اور ٹوکن کی ادائیگیوں کے لیے الگ الگ تجاویز درکار ہیں۔" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此收款人同时收取 RAND 和代币:向同一收款人支付 RAND 和代币需要分开的提案。" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此收款人同時收取 RAND 及代幣:向同一收款人支付 RAND 及代幣,需要分開的提案。" + } + } + } } }, "version" : "1.0" diff --git a/ios/RandWallet/RandWalletApp.swift b/ios/RandWallet/RandWalletApp.swift index 2209161..ddc284d 100644 --- a/ios/RandWallet/RandWalletApp.swift +++ b/ios/RandWallet/RandWalletApp.swift @@ -25,6 +25,7 @@ struct RandWalletApp: App { .environmentObject(auth) .environmentObject(contacts) .environmentObject(router) + .environmentObject(SignerStore.shared) // A `randpay:` link opens Send pre-filled once the wallet is unlocked. Never sends. .onOpenURL { router.open($0) } .preferredColorScheme(settings.theme.colorScheme) diff --git a/ios/RandWallet/Services/WalletService.swift b/ios/RandWallet/Services/WalletService.swift index 5e37b80..81e8e51 100644 --- a/ios/RandWallet/Services/WalletService.swift +++ b/ios/RandWallet/Services/WalletService.swift @@ -104,7 +104,12 @@ final class WalletService: ObservableObject { /// Forgets the wallet on this phone. `contacts` is the store the app holds for its whole /// lifetime: it is emptied in memory as well as in the Keychain, so a forgotten wallet's /// contacts are not offered to the next one (final review, finding 2). - func forgetWallet(contacts: ContactsStore) { + /// + /// The signer profiles go first: when the Keychain refuses to remove them all, this throws + /// before anything else is forgotten, so the person can retry rather than be left with a + /// forgotten wallet whose signer keys are still on the phone. + func forgetWallet(contacts: ContactsStore) throws { + try SignerStore.shared.removeAll() lock() Keychain.deleteSpendKey() contacts.forget() diff --git a/ios/RandWallet/Storage/Settings.swift b/ios/RandWallet/Storage/Settings.swift index ed4b3ab..948638b 100644 --- a/ios/RandWallet/Storage/Settings.swift +++ b/ios/RandWallet/Storage/Settings.swift @@ -46,10 +46,24 @@ final class Settings: ObservableObject { } } + /// The marker of the one-time chain-id migration below; set once it has run, whatever it found. + static let chainMigrationKey = "migration.chainId20to21" + + /// Once, on the first launch of a build that has it: a saved chain id of 20 becomes 21 (wallet + /// v0.8.0 runs chain 21 onward; a saved 20 would refuse every proposal and transfer). Any other + /// saved value, or none, is left alone, and nothing else is touched (the RPC URL is not). Later + /// launches see the marker and do nothing, so a user who chooses 20 again keeps it. + static func migrateChainId(_ defaults: UserDefaults) { + guard !defaults.bool(forKey: chainMigrationKey) else { return } + if let saved = defaults.object(forKey: "chainId") as? Int, saved == 20 { defaults.set(21, forKey: "chainId") } + defaults.set(true, forKey: chainMigrationKey) + } + init() { + Settings.migrateChainId(defaults) let core = try? RandCore.constants() rpcUrl = defaults.string(forKey: "rpcUrl") ?? core?.defaultRpcUrl ?? "https://rpc.randprotocol.org" - chainId = defaults.object(forKey: "chainId") as? Int ?? Int(core?.defaultChainId ?? 20) + chainId = defaults.object(forKey: "chainId") as? Int ?? Int(core?.defaultChainId ?? 21) autoLockMinutes = defaults.object(forKey: "autoLockMinutes") as? Int ?? 15 theme = Theme(rawValue: defaults.string(forKey: "theme") ?? "") ?? .system hasBackedUpKey = defaults.bool(forKey: "hasBackedUpKey") diff --git a/ios/RandWallet/Storage/SignerStore.swift b/ios/RandWallet/Storage/SignerStore.swift new file mode 100644 index 0000000..c0cef9b --- /dev/null +++ b/ios/RandWallet/Storage/SignerStore.swift @@ -0,0 +1,233 @@ +import Foundation +import Security +import LocalAuthentication + +/// Where signer profiles keep their secrets: the Keychain (`KeychainSignerSecrets`), or a test's +/// stand-in. +protocol SignerSecrets { + /// `requireUserPresence`: the item is readable only after Face ID, Touch ID or the passcode. + func save(_ data: Data, account: String, requireUserPresence: Bool) throws + /// `prompt` non-nil: the item is behind user presence and the system asks now, with `prompt` + /// as the reason. nil when there is no such item. + func load(account: String, prompt: String?) throws -> Data? + func delete(account: String) + /// Deletes every item whose account starts with `accountPrefix`, listed or not, except the + /// accounts in `keeping`. Throws when the items cannot be listed or one cannot be deleted; + /// nothing to delete is success. + func deleteAll(accountPrefix: String, keeping: Set) throws + /// Whether this device has a passcode, without which an item cannot be kept behind user presence. + var canRequireUserPresence: Bool { get } +} + +/// Keychain items beside the spend key's, never synced or migrated to another device. The policy +/// and the profile list are `WhenUnlockedThisDeviceOnly`. The seed's item is +/// `WhenPasscodeSetThisDeviceOnly` with `.userPresence`: each read asks for Face ID, Touch ID or the +/// passcode, and the system deletes the item if the device passcode is removed. +final class KeychainSignerSecrets: SignerSecrets { + private let service = "org.randprotocol.wallet" + + var canRequireUserPresence: Bool { LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) } + + private func base(_ account: String) -> [String: Any] { + [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account] + } + + func save(_ data: Data, account: String, requireUserPresence: Bool) throws { + SecItemDelete(base(account) as CFDictionary) + var add = base(account) + add[kSecValueData as String] = data + if requireUserPresence { + var error: Unmanaged? + guard let ac = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, &error) else { + error?.release() + throw Keychain.KeychainError(status: errSecParam) + } + add[kSecAttrAccessControl as String] = ac + } else { + add[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly + } + let status = SecItemAdd(add as CFDictionary, nil) + guard status == errSecSuccess else { throw Keychain.KeychainError(status: status) } + } + + func load(account: String, prompt: String?) throws -> Data? { + var q = base(account) + q[kSecReturnData as String] = true + q[kSecMatchLimit as String] = kSecMatchLimitOne + if let prompt { + // A fresh context per read, with no reuse window: an earlier Face ID match does not + // carry over to the next signature. + let ctx = LAContext() + ctx.localizedReason = prompt + ctx.touchIDAuthenticationAllowableReuseDuration = 0 + q[kSecUseAuthenticationContext as String] = ctx + } + var item: CFTypeRef? + let status = SecItemCopyMatching(q as CFDictionary, &item) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess, let cf = item as? Data else { + if status == errSecSuccess { return nil } + throw Keychain.KeychainError(status: status) + } + // `cf` wraps the Security framework's immutable CFData; zeroing it through `Data` would + // copy-on-write and free the original unzeroed. The caller gets its own buffer, which it + // can zero in place; the framework's copy is outside its reach. + return cf.withUnsafeBytes { raw in Data(raw) } + } + + func delete(account: String) { + SecItemDelete(base(account) as CFDictionary) + } + + /// Lists this service's items by attributes only (no data, so no prompt even for an item + /// behind user presence) and deletes each one whose account has the prefix. The service is + /// shared with the spend key and the prover pairing, so the sweep goes by account, never by + /// service alone. + func deleteAll(accountPrefix: String, keeping: Set) throws { + let ctx = LAContext() + ctx.interactionNotAllowed = true + var q: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, + kSecReturnAttributes as String: true, kSecMatchLimit as String: kSecMatchLimitAll] + q[kSecUseAuthenticationContext as String] = ctx + var items: CFTypeRef? + let status = SecItemCopyMatching(q as CFDictionary, &items) + if status == errSecItemNotFound { return } + guard status == errSecSuccess else { throw Keychain.KeychainError(status: status) } + guard let list = items as? [[String: Any]] else { throw Keychain.KeychainError(status: errSecInternalError) } + for attrs in list { + guard let account = attrs[kSecAttrAccount as String] as? String, account.hasPrefix(accountPrefix), + !keeping.contains(account) else { continue } + let deleted = SecItemDelete(base(account) as CFDictionary) + guard deleted == errSecSuccess || deleted == errSecItemNotFound else { throw Keychain.KeychainError(status: deleted) } + } + } +} + +/// What the profile list shows; the policy and the seed are in their own items. +struct SignerProfile: Codable, Identifiable, Equatable { + let id: String + var label: String + let index: Int + let fingerprint: String + let threshold: Int + let signers: Int + let address: String + let policyHash: String +} + +/// Signer profiles (spec §3), in three kinds of item: `msauth.index` (the list), +/// `msauth..policy` and `msauth..seed` (behind user presence). The seed is read once per +/// signature, with the system's prompt, and is not kept by this class. +final class SignerStore: ObservableObject { + static let shared = SignerStore(secrets: KeychainSignerSecrets()) + + @Published private(set) var profiles: [SignerProfile] = [] + private let secrets: SignerSecrets + private static let indexAccount = "msauth.index" + private static func seedAccount(_ id: String) -> String { "msauth.\(id).seed" } + private static func policyAccount(_ id: String) -> String { "msauth.\(id).policy" } + + struct StoreError: LocalizedError, Equatable { let errorDescription: String? } + + init(secrets: SignerSecrets) { + self.secrets = secrets + if let d = try? secrets.load(account: Self.indexAccount, prompt: nil), + let list = try? JSONDecoder().decode([SignerProfile].self, from: d) { + profiles = list + } + } + + /// Saves a profile: the label (1-64 characters), the policy, and the key — a `rand-node keygen` + /// file or 64 hex — which must be one of the policy's signers. Refused without a device + /// passcode, and for a signer already saved. Nothing is left behind when a save fails. + func add(label: String, policy: String, key: String) throws -> SignerProfile { + guard secrets.canRequireUserPresence else { + throw StoreError(errorDescription: String(localized: "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.")) + } + let name = label.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty, name.unicodeScalars.count <= 64 else { throw StoreError(errorDescription: String(localized: "a signer label is 1-64 characters")) } + let pol = MsauthRules.clean(policy) + let info = try RandCore.msauthPolicyInfo(pol) + // The core's reply carries the seed as a hex String, which cannot be wiped; only the Data + // copy below is. + let signer = try RandCore.msauthSignerImport(MsauthRules.clean(key), policy: pol) + guard let index = signer.index else { throw RandCore.CoreError(message: "this key is not one of the policy's signers") } + if let same = profiles.first(where: { $0.policyHash == info.policyHash && $0.index == index }) { + throw StoreError(errorDescription: String(localized: "this signer is already saved as \(same.label)")) + } + guard var seed = Data(hex: signer.seed), seed.count == 32 else { throw RandCore.CoreError(message: "a signer seed is 64 hex characters") } + defer { seed.resetBytes(in: 0.. String { + guard let d = try secrets.load(account: Self.policyAccount(id), prompt: nil) else { + throw StoreError(errorDescription: String(localized: "No such signer profile.")) + } + return String(decoding: d, as: UTF8.self) + } + + private func readSeed(_ id: String, reason: String) throws -> Data { + guard let seed = try secrets.load(account: Self.seedAccount(id), prompt: reason) else { + throw StoreError(errorDescription: String(localized: "This signer profile has no key on this device: remove it and add it again.")) + } + return seed + } + + /// One prompted read of the seed for `body`. The seed is in a buffer this store owns (copied + /// once out of the Keychain's reply), zeroed in place with `resetBytes` when `body` returns or + /// throws. The Security framework's own copy cannot be wiped from here; nor can a copy `body` + /// makes and keeps (the core path makes a hex String, see `RandCore.msauthSign`). + func withSeed(_ id: String, reason: String, _ body: (Data) throws -> T) throws -> T { + var seed = try readSeed(id, reason: reason) + defer { seed.resetBytes(in: 0..(_ id: String, reason: String, _ body: (Data) async throws -> T) async throws -> T { + var seed = try readSeed(id, reason: reason) + defer { seed.resetBytes(in: 0.. Bool { a.addr == b.addr && a.amount == b.amount } +} + +/// An error whose text is for the signer. +struct PaymentEntryError: LocalizedError, Equatable { + let errorDescription: String? +} + +/// The pure decisions behind the payments section. +enum SignRows { + /// An output the signer must name a payee for: it leaves the account and moves something. + /// Decided from the output itself, not from the core's own flag (as the JS screen does). + static func needsPayee(_ o: MsauthOutput) -> Bool { + !o.toAccount && !o.amount.allSatisfy { $0 == "0" } + } + static func isNonRand(_ o: MsauthOutput) -> Bool { o.asset != 0 } + static func isMatched(_ o: MsauthOutput) -> Bool { !(o.matches ?? "").trimmingCharacters(in: .whitespaces).isEmpty } + + /// The rows as one string, to tell whether they changed since a check was sent. + static func key(_ rows: [PaymentRow]) -> String { + rows.map { "\($0.addr.count):\($0.addr)|\($0.amount.count):\($0.amount)" }.joined(separator: "\n") + } + + /// One intended payment as the core's `addr:AMOUNT`. AMOUNT is RAND display text, checked by + /// `Amount.parse` and passed as typed (trimmed). It may be left out only when `amountOptional` + /// (a row whose address owns a non-RAND paying output: the core refuses a pin there). + static func entry(_ row: PaymentRow, amountOptional: Bool) throws -> String { + let a = row.addr.trimmingCharacters(in: .whitespacesAndNewlines) + let v = row.amount.trimmingCharacters(in: .whitespacesAndNewlines) + if a.isEmpty { throw PaymentEntryError(errorDescription: String(localized: "Enter the address of every payment.")) } + if v.isEmpty { + if amountOptional { return a } + throw PaymentEntryError(errorDescription: String(localized: "Enter the amount of every payment: it is what stops a split or an inflated payment.")) + } + guard let units = Amount.parse(v), units > 0 else { + throw PaymentEntryError(errorDescription: String(localized: "An amount is a number of RAND, such as 1.5.")) + } + return "\(a):\(v)" + } + + /// Whether this row may go without an amount: only when the check's `expected` shows the row's + /// address owns a paying non-RAND output. A row the core has not fingerprinted yet (or whose + /// address was edited) may be sent address-only when some paying output is non-RAND, so the + /// next check can tell; the core refuses it unless its fingerprint owns such an output. + /// A payee that is also paid RAND never gets the exception: its RAND amount must be pinned. + static func amountOptional(_ row: PaymentRow, summary: MsauthSummary?) -> Bool { + guard let summary else { return false } + let paying = summary.outputs.filter(needsPayee) + let anyNonRand = paying.contains(where: isNonRand) + let (owners, randOwners) = payeeAssets(paying) + if let known = knownEntry(row, summary: summary) { + return known.fingerprint.map { owners.contains($0) && !randOwners.contains($0) } ?? false + } + return anyNonRand + } + + /// Whether the check showed this row's payee paid both RAND and a non-RAND asset: such a payee + /// needs separate proposals, one per kind. + static func mixedPayee(_ row: PaymentRow, summary: MsauthSummary?) -> Bool { + guard let summary, let fp = knownEntry(row, summary: summary)?.fingerprint else { return false } + let (owners, randOwners) = payeeAssets(summary.outputs.filter(needsPayee)) + return owners.contains(fp) && randOwners.contains(fp) + } + + private static func knownEntry(_ row: PaymentRow, summary: MsauthSummary) -> MsauthExpected? { + let a = row.addr.trimmingCharacters(in: .whitespacesAndNewlines) + guard !a.isEmpty else { return nil } + return summary.expected.first(where: { x in + let head = x.entry.firstIndex(of: ":").map { String(x.entry[..<$0]) } ?? x.entry + return head.trimmingCharacters(in: .whitespacesAndNewlines) == a + }) + } + + private static func payeeAssets(_ paying: [MsauthOutput]) -> (Set, Set) { + let fps = { (list: [MsauthOutput]) in Set(list.map(\.ownerFingerprint).filter { !$0.isEmpty }) } + return (fps(paying.filter(isNonRand)), fps(paying.filter { !isNonRand($0) })) + } +} + +/// Which rows the last check was sent for, and whether editing them since disarmed Sign. Editing any +/// row sets `stale`; only a check whose snapshot of the rows equals the current rows when its result +/// lands clears it. The top Check and Retry resend the last payments, so they cannot re-arm Sign. +struct PaymentsGate: Equatable { + var rows = [PaymentRow()] + var payees: [String] = [] + private(set) var checkedKey = SignRows.key([PaymentRow()]) + private(set) var stale = false + + mutating func edited() { stale = true } + /// "Check payments": the entries are built from the rows (throws if a row is incomplete). + mutating func adopt(entries: [String]) { + payees = entries + checkedKey = SignRows.key(rows) + } + /// A new proposal: its payments are asked again. + mutating func reset() { self = PaymentsGate() } + /// The rows a check is being sent for. + var sentKey: String { checkedKey } + /// A check's result arrived: stale if the rows moved since it was sent (or were edited before). + mutating func landed(sentKey: String) { stale = sentKey != SignRows.key(rows) } +} + +/// What the screen shows of a check: the core's lines verbatim, the refusal's message, and whether +/// Sign may be pressed. +struct SignProposalModel { + let summary: MsauthSummary? + let verdict: MsauthVerdict + + init(summary: MsauthSummary?, verdict: MsauthVerdict) { self.summary = summary; self.verdict = verdict } + init(_ check: MsauthCheck) { self.init(summary: check.summary, verdict: check.verdict) } + + /// The core's summary.lines, verbatim. + var lines: [String] { summary?.lines ?? [] } + /// A summary the core did not shape as expected (no lines, no outputs) is shown as its refusal + /// and nothing else. + var showsSummary: Bool { + guard let object = summary?.json as? [String: Any] else { return false } + guard let lines = object["lines"] as? [Any], lines.allSatisfy({ $0 is String }) else { return false } + return object["outputs"] is [Any] + } + var refusal: String? { verdict.message } + var isRetryable: Bool { if case .noNode = verdict { return true } else { return false } } + func canSign(typed: String, stale: Bool) -> Bool { + guard !stale, case .signable = verdict, let fp = summary?.policyFingerprint else { return false } + return MsauthRules.confirms(fingerprint: fp, typed: typed) + } + var blocksLeft: UInt64? { if case .signable(let n) = verdict { return n } else { return nil } } + var payingTotal: Int { summary?.outputs.filter(SignRows.needsPayee).count ?? 0 } + var payingMatched: Int { summary?.outputs.filter { SignRows.needsPayee($0) && SignRows.isMatched($0) }.count ?? 0 } +} + +/// One screen's checking state, apart from SwiftUI so its ordering can be tested: which text the +/// review is of, which check is the latest, the payments, and the last good summary the payment rows +/// are drawn from. +struct CheckSession { + struct Ticket: Equatable { let id: Int; let proposal: String; let sentKey: String } + + var gate = PaymentsGate() + private(set) var model: SignProposalModel? + /// The last summary the core gave, kept when a later check is refused so the rows stay editable. + private(set) var lastSummary: MsauthSummary? + var typed = "" + private(set) var error: String? + private(set) var inFlight = false + /// The exact text the displayed review was checked on: the only text Sign signs. + private(set) var reviewedProposal: String? + private var sentProposal: String? + private var latest = 0 + + /// A check starts for `proposal` with the payments last adopted. A new proposal text starts the + /// payments over. Any older check still out is ignored when it lands. + mutating func begin(proposal: String) -> Ticket { + if proposal != sentProposal { gate.reset(); lastSummary = nil; sentProposal = proposal } + latest += 1; inFlight = true; error = nil + return Ticket(id: latest, proposal: proposal, sentKey: gate.sentKey) + } + /// A result lands; false when a newer request has superseded it. + @discardableResult + mutating func land(_ t: Ticket, _ check: MsauthCheck) -> Bool { + guard t.id == latest else { return false } + inFlight = false + if let s = check.summary { lastSummary = s } + gate.landed(sentKey: t.sentKey) + model = SignProposalModel(check) + reviewedProposal = t.proposal + typed = "" + return true + } + /// The check threw (the core refused, e.g. a mistyped payee): no review, but the payments stay. + @discardableResult + mutating func fail(_ t: Ticket, message: String) -> Bool { + guard t.id == latest else { return false } + inFlight = false; model = nil; reviewedProposal = nil; typed = ""; error = message + return true + } + /// The proposal text was edited: the review no longer describes it, and a check out is dropped. + /// A text other than the one the payments were asked for also drops them, so rows and the + /// non-RAND hint never come from a different proposal. + mutating func proposalEdited(to text: String) { + latest += 1; inFlight = false; model = nil; reviewedProposal = nil; typed = ""; error = nil + if text != sentProposal { gate.reset(); lastSummary = nil; sentProposal = nil } + } + /// A signature's refusal replaces the verdict on the same summary. + mutating func refused(_ verdict: MsauthVerdict) { + if let m = model { model = SignProposalModel(summary: m.summary, verdict: verdict) } + } + mutating func clearError() { error = nil } + /// The sequence keeps rising, so a check that lands after the screen was left is ignored. + mutating func close() { let n = latest + 1; self = CheckSession(); latest = n } + + /// Sign is armed: signable, the last four typed, the payments unchanged since the check, and the + /// editor still holds the text that was checked. + func canSign(editor: String) -> Bool { + guard let m = model, let reviewed = reviewedProposal, reviewed == editor else { return false } + return m.canSign(typed: typed, stale: gate.stale) + } +} + +/// A share as a text file for `.fileExporter`. +struct ShareDocument: FileDocument { + static var readableContentTypes: [UTType] { [.plainText] } + var text: String + init(text: String) { self.text = text } + init(configuration: ReadConfiguration) throws { + text = String(decoding: configuration.file.regularFileContents ?? Data(), as: UTF8.self) + } + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: Data((text + "\n").utf8)) + } +} + +/// Reads a picked file: refused by its size before it is read, and a read failure has its own +/// message. Shared by the proposal, policy and key pickers. +func readPickedFile(_ url: URL) throws -> String { + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize, size > msauthMaxFileBytes { + throw PaymentEntryError(errorDescription: String(localized: "That file is larger than a proposal, a policy or a key file can be.")) + } + guard let d = try? Data(contentsOf: url) else { + throw PaymentEntryError(errorDescription: String(localized: "That file could not be read.")) + } + guard d.count <= msauthMaxFileBytes else { + throw PaymentEntryError(errorDescription: String(localized: "That file is larger than a proposal, a policy or a key file can be.")) + } + return String(decoding: d, as: UTF8.self) +} + +/// Paste or open proposal.json, enter the payments, review what the core checked, type the last +/// four, authenticate, share. +struct SignProposalView: View { + let profile: SignerProfile + @EnvironmentObject var store: SignerStore + @EnvironmentObject var settings: Settings + + @State private var proposal = "" + @State private var session = CheckSession() + @State private var share: MsauthShare? + @State private var error: String? + @State private var rowError: String? + @State private var importing = false + @State private var exporting = false + @State private var signing = false + @State private var signTask: Task? + @State private var shareToVerify = "" + @State private var verifyNote: String? + @State private var inHand = Set() + + private var signer: MsauthSigning { + MsauthSigning(store: store, core: RandMsauthCore(), node: settings.rpcURL.map { RpcClient(url: $0) }, + chainId: UInt64(max(settings.chainId, 0))) + } + private var checking: Bool { session.inFlight } + + var body: some View { + Form { + Section(profile.label) { + Text(profile.fingerprint).font(.mono) + Text(String(localized: "\(String(profile.threshold)) of \(String(profile.signers)) · signer \(String(profile.index))")) + } + Section("Proposal (proposal.json)") { + TextEditor(text: $proposal).font(.mono).disabled(signing).frame(minHeight: 80).autocorrectionDisabled().textInputAutocapitalization(.never) + Button("Open file") { importing = true } + Button(checking ? "Checking…" : "Check proposal") { startCheck() }.disabled(checking || signing || proposal.isEmpty) + if let e = error ?? session.error { Text(verbatim: e).foregroundColor(.red) } + } + if showsPayments { paymentsSection } + if let m = session.model { reviewSections(m) } + if let share { shareSection(share) } + Section("Check a share") { + TextField("index:hex", text: $shareToVerify).font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + Button("Check share") { verifyShare() } + if let verifyNote { Text(verbatim: verifyNote) } + } + } + .navigationTitle("Sign a proposal") + .fileImporter(isPresented: $importing, allowedContentTypes: [.json, .plainText]) { result in + guard case .success(let url) = result else { return } + do { proposal = try readPickedFile(url); error = nil } catch { self.error = error.localizedDescription } + } + .fileExporter(isPresented: $exporting, document: ShareDocument(text: share?.share ?? ""), contentType: .plainText, + defaultFilename: "share-\(share?.index ?? 0)") { _ in } + // The review describes the text that was checked: editing the text drops it until a new check. + .onChange(of: proposal) { text in session.proposalEdited(to: text); share = nil } + // The proposal carries the account's witness: it is never stored and goes with the screen. + .onDisappear { + signTask?.cancel(); signTask = nil + proposal = ""; share = nil; session.close(); signing = false + } + } + + // MARK: sections + + /// The payments section shows while the last good summary has something to pay for. + private var showsPayments: Bool { + guard let s = session.lastSummary else { return false } + return !s.outputs.filter(SignRows.needsPayee).isEmpty + } + + private func rowBinding(_ id: UUID, _ key: WritableKeyPath) -> Binding { + Binding(get: { session.gate.rows.first { $0.id == id }?[keyPath: key] ?? "" }, + set: { v in + guard let i = session.gate.rows.firstIndex(where: { $0.id == id }) else { return } + session.gate.rows[i][keyPath: key] = v; session.gate.edited() + }) + } + + private var paymentsSection: some View { + Section { + ForEach(session.gate.rows) { row in + VStack(alignment: .leading, spacing: 6) { + if SignRows.amountOptional(row, summary: session.lastSummary) { + Text("Amounts cannot be checked for non-RAND payments: leave the amount empty for those.") + .font(.footnote).foregroundColor(.secondary) + } + if SignRows.mixedPayee(row, summary: session.lastSummary) { + Text("This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.") + .font(.footnote).foregroundColor(.secondary) + } + TextField(text: rowBinding(row.id, \.addr), prompt: Text(verbatim: "rand1…")) { Text("Payee address") } + .font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + HStack { + TextField(text: rowBinding(row.id, \.amount), prompt: Text(verbatim: "0.0")) { Text("Amount in RAND") } + .font(.mono).keyboardType(.decimalPad).autocorrectionDisabled() + Button("Remove", role: .destructive) { + session.gate.rows.removeAll { $0.id == row.id }; session.gate.edited() + }.buttonStyle(.borderless) + } + } + } + Button("Add payment") { session.gate.rows.append(PaymentRow()); session.gate.edited() } + if let rowError { Text(verbatim: rowError).foregroundColor(.red) } + Button("Check payments") { checkPayments() }.disabled(checking || signing) + } header: { Text("Intended payments") } footer: { + Text("Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.") + } + } + + @ViewBuilder private func reviewSections(_ m: SignProposalModel) -> some View { + if m.showsSummary, let s = m.summary { + Section("What this proposal does, as checked against your policy") { + ForEach(Array(m.lines.enumerated()), id: \.offset) { Text(verbatim: $0.element).font(.mono) } + Text(String(localized: "Fee: \(Amount.format(s.fee ?? "0")) RAND · expires at height \(String(s.expiresAt ?? 0))")) + Text(String(localized: "\(String(m.payingMatched)) of \(String(m.payingTotal)) payments matched.")) + .font(.footnote).foregroundColor(.secondary) + } + } + if let r = m.refusal { + Section { + Text(verbatim: r).foregroundColor(.red) + Button("Retry") { startCheck() }.disabled(checking || signing) + } + } else { + Section { + if let left = m.blocksLeft { Text(String(localized: "\(String(left)) blocks left to prove it.")) } + TextField(text: $session.typed) { Text("Type the last four characters of the account fingerprint") } + .font(.mono).autocorrectionDisabled().textInputAutocapitalization(.never) + if session.gate.stale { + Text("Payments changed: check them again before signing.").font(.footnote).foregroundColor(.secondary) + } + Button(signing ? "Signing…" : "Sign") { sign() }.disabled(signing || checking || !session.canSign(editor: proposal)) + } header: { Text("Type the last four characters of the account fingerprint") } + } + } + + private func shareSection(_ s: MsauthShare) -> some View { + Section("Your share — send it to whoever proves the proposal") { + Text(verbatim: s.share).font(.mono).textSelection(.enabled).lineLimit(3) + Button("Copy") { UIPasteboard.general.string = s.share } + ShareLink(item: s.share) + Button("Save as file") { exporting = true } + } + } + + // MARK: actions + + /// "Check payments": build the entries from the rows, then check. + private func checkPayments() { + do { + let entries = try session.gate.rows.map { + try SignRows.entry($0, amountOptional: SignRows.amountOptional($0, summary: session.lastSummary)) + } + rowError = nil + session.gate.adopt(entries: entries) + } catch { rowError = error.localizedDescription; return } + startCheck() + } + + /// The top Check and Retry send the payments last adopted; edits made since keep Sign disarmed. + /// Only the latest request's result is shown. + private func startCheck() { + guard !session.inFlight else { return } + error = nil; share = nil + let ticket = session.begin(proposal: proposal) + let payees = session.gate.payees + Task { + do { + let r = try await signer.check(id: profile.id, proposal: ticket.proposal, expectedRecipients: payees) + session.land(ticket, r) + } catch { session.fail(ticket, message: error.localizedDescription) } + } + } + + private func sign() { + guard session.canSign(editor: proposal), let text = session.reviewedProposal else { return } + signing = true; error = nil + let payees = session.gate.payees + signTask = Task { + defer { signing = false } + if Task.isCancelled { return } + do { + // The text signed is the text that was checked, never the editor's current text. The + // core's check and the head are redone right before and again after the device asks + // for Face ID (MsauthSigning.sign), so a refusal comes back as its message. + let result = try await signer.sign(id: profile.id, proposal: text, expectedRecipients: payees, + reason: String(localized: "Sign a multisig proposal")) + if Task.isCancelled { return } + share = result + session.typed = "" + } catch let refusal as MsauthRefusal { + if !Task.isCancelled { session.refused(refusal.verdict) } + } catch { if !Task.isCancelled { self.error = error.localizedDescription } } + } + } + + private func verifyShare() { + do { + let s = MsauthRules.clean(shareToVerify) + try signer.verifyShare(id: profile.id, proposal: session.reviewedProposal ?? proposal, share: s) + guard let i = Int(s.split(separator: ":").first.map { $0.trimmingCharacters(in: .whitespaces) } ?? "") else { return } + inHand.insert(i) + verifyNote = String(localized: "Signer \(String(i))'s share verifies. \(String(inHand.count)) of \(String(profile.threshold)) in hand: signers \(inHand.sorted().map(String.init).joined(separator: ", ")).") + } catch { verifyNote = error.localizedDescription } + } +} + +extension Array { + subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil } +} diff --git a/ios/RandWallet/UI/SignersView.swift b/ios/RandWallet/UI/SignersView.swift new file mode 100644 index 0000000..23a71ed --- /dev/null +++ b/ios/RandWallet/UI/SignersView.swift @@ -0,0 +1,148 @@ +import SwiftUI +import UniformTypeIdentifiers + +/// Settings → Multisig signers: the saved signer profiles and a form that adds one. The policy and +/// the key come from files or a paste; the key is stored behind the device passcode +/// (`SignerStore.add`), and every signature asks for Face ID / Touch ID / the passcode again. +struct SignersView: View { + @EnvironmentObject var store: SignerStore + @State private var adding = false + @State private var removeError: String? + @State private var pendingRemove: SignerProfile? + + var body: some View { + List { + if store.profiles.isEmpty { + Section { + Text("No signer profiles yet").font(.headline) + Text("Add the policy file and your signer key to sign for a private multisig account.").foregroundColor(.secondary) + } + } else { + Section { + ForEach(store.profiles) { p in + NavigationLink { SignProposalView(profile: p) } label: { + VStack(alignment: .leading, spacing: 2) { + Text(verbatim: p.label) + Text(verbatim: p.fingerprint).font(.mono) + Text(String(localized: "\(String(p.threshold)) of \(String(p.signers)) · signer \(String(p.index))")) + .font(.footnote).foregroundColor(.secondary) + } + } + .swipeActions(allowsFullSwipe: false) { + Button("Remove this signer", role: .destructive) { pendingRemove = p } + } + } + } + } + if let removeError { Section { Text(verbatim: removeError).foregroundColor(.red) } } + Section { + Button("Add a signer") { adding = true } + } footer: { + Text("A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.") + } + } + .navigationTitle("Multisig signers") + .confirmationDialog(String(localized: "Remove \(pendingRemove?.label ?? "")?"), isPresented: Binding(get: { pendingRemove != nil }, set: { if !$0 { pendingRemove = nil } }), + titleVisibility: .visible, presenting: pendingRemove) { p in + Button("Remove this signer", role: .destructive) { Task { await remove(p) } } + } message: { _ in + Text("You need the signer's key file to add this signer back.") + } + .sheet(isPresented: $adding) { NavigationStack { AddSignerView() } } + } + + private func remove(_ p: SignerProfile) async { + removeError = nil + guard await Keychain.authenticate(reason: String(localized: "Remove a multisig signer")) else { return } + store.remove(p.id) + } +} + +/// What the add form shows once the core has read the policy and the key. +struct SignerPreview: Equatable { + let fingerprint: String + let address: String + let threshold: Int + let signers: Int + let index: Int? +} + +/// Reads the policy and the key through the core; throws the core's refusal. The seed in the +/// core's reply is not kept. +func previewSigner(policy: String, key: String) throws -> SignerPreview { + let pol = MsauthRules.clean(policy) + let info = try RandCore.msauthPolicyInfo(pol) + let signer = try RandCore.msauthSignerImport(MsauthRules.clean(key), policy: pol) + return SignerPreview(fingerprint: info.fingerprint, address: info.address, threshold: info.threshold, + signers: info.signers, index: signer.index) +} + +struct AddSignerView: View { + @EnvironmentObject var store: SignerStore + @Environment(\.dismiss) private var dismiss + @State private var label = "" + @State private var policy = "" + @State private var key = "" + @State private var preview: SignerPreview? + @State private var error: String? + @State private var target: Target? + enum Target { case policy, key } + + var body: some View { + Form { + Section { + TextField("Label", text: $label).onChange(of: label) { _ in preview = nil } + } + fileSection("Policy file", text: $policy, target: .policy) + fileSection("Signer key file, or its 64-hex seed", text: $key, target: .key) + Section { + if let error { Text(verbatim: error).foregroundColor(.red) } + Button("Check") { check() }.disabled(policy.isEmpty || key.isEmpty) + } + if let p = preview { + Section("Account fingerprint — compare it with every other signer before you save") { + Text(verbatim: p.fingerprint).font(.mono.weight(.semibold)) + Text(verbatim: p.address).font(.mono).foregroundColor(.secondary) + if let i = p.index { + Text(String(localized: "\(String(p.threshold)) of \(String(p.signers)) · this key is signer \(String(i))")) + } + Button("Save signer") { save() } + } + } + } + .navigationTitle("Add a signer") + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } + .fileImporter(isPresented: Binding(get: { target != nil }, set: { if !$0 { target = nil } }), + allowedContentTypes: [.json, .plainText]) { result in + let t = target; target = nil + guard case .success(let url) = result else { return } + do { + let text = try readPickedFile(url) + if t == .policy { policy = text } else { key = text } + preview = nil; error = nil + } catch { self.error = error.localizedDescription } + } + } + + private func fileSection(_ title: LocalizedStringKey, text: Binding, target t: Target) -> some View { + Section(title) { + TextEditor(text: text).font(.mono).frame(minHeight: 70).autocorrectionDisabled().textInputAutocapitalization(.never) + .onChange(of: text.wrappedValue) { _ in preview = nil } + Button("Open file") { target = t } + } + } + + private func check() { + error = nil; preview = nil + do { preview = try previewSigner(policy: policy, key: key) } catch { self.error = error.localizedDescription } + } + + private func save() { + do { + _ = try store.add(label: label, policy: policy, key: key) + label = ""; policy = ""; key = ""; preview = nil + dismiss() + } catch { self.error = error.localizedDescription } + } +} diff --git a/ios/RandWalletTests/ContactsTests.swift b/ios/RandWalletTests/ContactsTests.swift index 348b060..2261afe 100644 --- a/ios/RandWalletTests/ContactsTests.swift +++ b/ios/RandWalletTests/ContactsTests.swift @@ -87,7 +87,7 @@ final class ContactsTests: XCTestCase { let store = ContactsStore(backing: MemoryBlob()) try store.add(name: "alice", address: a1) let wallet = WalletService(settings: Settings()) - wallet.forgetWallet(contacts: store) + try wallet.forgetWallet(contacts: store) XCTAssertTrue(store.book.sorted.isEmpty) } diff --git a/ios/RandWalletTests/CoreSmokeTests.swift b/ios/RandWalletTests/CoreSmokeTests.swift index 410590e..237663c 100644 --- a/ios/RandWalletTests/CoreSmokeTests.swift +++ b/ios/RandWalletTests/CoreSmokeTests.swift @@ -5,8 +5,8 @@ import XCTest final class CoreSmokeTests: XCTestCase { func testVersionReportsChainDefaults() throws { let c = try RandCore.constants() - XCTAssertEqual(c.defaultChainId, 20) - XCTAssertEqual(c.chainBuild, "37c4fdd", "fullnode main 37c4fdd (the v0.8.0 candidate), on chain 20 (cut on v0.6.8, c9c9bd3)") + XCTAssertEqual(c.defaultChainId, 21) + XCTAssertEqual(c.chainBuild, "c0878c3", "fullnode e34117be (private multisig, constraint set 9), chain 21") XCTAssertEqual(c.tokenSymbol, "RAND") XCTAssertEqual(c.bundleBaseFee, "1000000") // Constraint set 8: the gas every bundle proof declares — chain 18's genesis pin. diff --git a/ios/RandWalletTests/FakeSignerSecrets.swift b/ios/RandWalletTests/FakeSignerSecrets.swift new file mode 100644 index 0000000..8f6fa6f --- /dev/null +++ b/ios/RandWalletTests/FakeSignerSecrets.swift @@ -0,0 +1,32 @@ +@testable import RandWallet +import Foundation + +/// The Keychain stand-in: records, per item, whether it was stored behind user presence and how +/// often it was read with a prompt. The simulator has no passcode, so `.userPresence` cannot be +/// created there; the device check is manual (task-10 report). +final class FakeSignerSecrets: SignerSecrets { + var items: [String: (data: Data, presence: Bool)] = [:] + var promptedReads: [String] = [] + var canRequireUserPresence = true + /// Runs inside a prompted read, where the system's Face ID / passcode sheet would be: a test + /// moves the chain on here to stand for the time a person takes to authenticate. + var duringPrompt: () -> Void = {} + /// A failing save for the named account (a full Keychain, a passcode removed mid-add). + var failSaveFor: String? + + func save(_ data: Data, account: String, requireUserPresence: Bool) throws { + if account == failSaveFor { throw Keychain.KeychainError(status: errSecNotAvailable) } + items[account] = (data, requireUserPresence) + } + func load(account: String, prompt: String?) throws -> Data? { + if prompt != nil { promptedReads.append(account); duringPrompt() } + return items[account]?.data + } + func delete(account: String) { items[account] = nil } + /// The Keychain listing fails (`SecItemCopyMatching` refused): the sweep throws, nothing deleted. + var failListing = false + func deleteAll(accountPrefix: String, keeping: Set) throws { + if failListing { throw Keychain.KeychainError(status: errSecInteractionNotAllowed) } + items = items.filter { !$0.key.hasPrefix(accountPrefix) || keeping.contains($0.key) } + } +} diff --git a/ios/RandWalletTests/LiveMsauthTests.swift b/ios/RandWalletTests/LiveMsauthTests.swift new file mode 100644 index 0000000..4fa163b --- /dev/null +++ b/ios/RandWalletTests/LiveMsauthTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import RandWallet + +/// One private-multisig signature through `MsauthSigning` (the app's check-then-sign order) and the +/// real RandCore on the simulator, against the devnet scripts/e2e-msauth.sh runs. The script passes +/// the files' contents as TEST_RUNNER_RAND_MSAUTH_* (the runner strips the prefix); without them the +/// test is skipped. The Keychain's `.userPresence` cannot exist on a passcode-less simulator, so the +/// store runs over `FakeSignerSecrets` here; the device check is manual (task-10 report). +@MainActor +final class LiveMsauthTests: XCTestCase { + func testSignsADevnetProposalThroughTheCore() async throws { + let env = ProcessInfo.processInfo.environment + guard let proposal = env["RAND_MSAUTH_PROPOSAL"], let policy = env["RAND_MSAUTH_POLICY"], let key = env["RAND_MSAUTH_KEY"], + let payment = env["RAND_MSAUTH_PAYEE"], let rpc = env["RAND_MSAUTH_RPC"].flatMap(URL.init(string:)) else { + throw XCTSkip("run scripts/e2e-msauth.sh") + } + let chainId = UInt64(env["RAND_MSAUTH_CHAIN_ID"] ?? "21") ?? 21 + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let profile = try store.add(label: "ios signer", policy: policy, key: key) + let signing = MsauthSigning(store: store, core: RandMsauthCore(), node: RpcClient(url: rpc), chainId: chainId) + + // No payee, and the payee without its amount: nothing signable. + let none = try await signing.check(id: profile.id, proposal: proposal, expectedRecipients: []) + XCTAssertFalse(none.verdict.isSignable, "\(none.verdict)") + let address = String(payment[..<(payment.lastIndex(of: ":") ?? payment.endIndex)]) + let amountless = try await signing.check(id: profile.id, proposal: proposal, expectedRecipients: [address]) + XCTAssertFalse(amountless.verdict.isSignable, "\(amountless.verdict)") + + let checked = try await signing.check(id: profile.id, proposal: proposal, expectedRecipients: [payment]) + XCTAssertTrue(checked.verdict.isSignable, "\(checked.verdict)") + XCTAssertEqual(checked.summary?.recipientsChecked, true) + + let share = try await signing.sign(id: profile.id, proposal: proposal, expectedRecipients: [payment], reason: "e2e") + XCTAssertEqual(share.index, profile.index) + XCTAssertEqual(secrets.promptedReads.count, 1, "the seed is read once, behind the (fake) prompt") + XCTAssertNoThrow(try signing.verifyShare(id: profile.id, proposal: proposal, share: share.share)) + print("MSAUTH-SHARE \(share.share)") + } +} diff --git a/ios/RandWalletTests/LiveSendTests.swift b/ios/RandWalletTests/LiveSendTests.swift index 7893abd..b1380ca 100644 --- a/ios/RandWalletTests/LiveSendTests.swift +++ b/ios/RandWalletTests/LiveSendTests.swift @@ -16,7 +16,7 @@ final class LiveSendTests: XCTestCase { } let settings = Settings() settings.rpcUrl = env["RAND_LIVE_RPC"] ?? "https://rpc.randprotocol.org" - settings.chainId = 20 + settings.chainId = 21 settings.prover = nil settings.noProver = false let wallet = WalletService(settings: settings) diff --git a/ios/RandWalletTests/LiveSwapTests.swift b/ios/RandWalletTests/LiveSwapTests.swift index e3afb57..1db706e 100644 --- a/ios/RandWalletTests/LiveSwapTests.swift +++ b/ios/RandWalletTests/LiveSwapTests.swift @@ -21,7 +21,7 @@ final class LiveSwapTests: XCTestCase { let dx = UInt64(env["RAND_LIVE_SWAP_UNITS"] ?? "") ?? 200_000_000 let settings = Settings() settings.rpcUrl = env["RAND_LIVE_RPC"] ?? "https://rpc.randprotocol.org" - settings.chainId = 20 + settings.chainId = 21 settings.prover = nil settings.noProver = false let wallet = WalletService(settings: settings) diff --git a/ios/RandWalletTests/MsauthTests.swift b/ios/RandWalletTests/MsauthTests.swift new file mode 100644 index 0000000..b766b22 --- /dev/null +++ b/ios/RandWalletTests/MsauthTests.swift @@ -0,0 +1,528 @@ +import XCTest +@testable import RandWallet + +/// Private multisig signing on iOS (spec 2026-10-11-msauth-signing-design.md §3, §4): the core +/// wrappers against fullnode's vectors, `MsauthRules` — the same decisions as ui/engine/signers.js +/// `verdict` — the signer store behind user presence, and the order `MsauthSigning.sign` keeps. +final class MsauthTests: XCTestCase { + private func fx(_ name: String) throws -> String { + let url = try XCTUnwrap(Bundle(for: MsauthTests.self).url(forResource: name, withExtension: "json")) + return try String(contentsOf: url, encoding: .utf8) + } + private func json(_ name: String) throws -> [String: Any] { + try XCTUnwrap(JSONSerialization.jsonObject(with: Data(try fx(name).utf8)) as? [String: Any]) + } + /// The valid proposal's real payee, read from its informational summary ("pay 10 RAND to rand1…"). + private func payee() throws -> String { + let summary = try XCTUnwrap(try json("proposal-valid")["summary"] as? [String]) + return try XCTUnwrap(summary[0].split(separator: " ").last.map(String.init)) + } + /// The intended payment as the app passes it: the address and its amount, pinned. + private func payment() throws -> String { try payee() + ":10" } + private func seeds() throws -> [String] { try XCTUnwrap(try json("signer-seeds")["members"] as? [String]) } + + // MARK: the core, on fullnode's vectors + + func testTheCoreChecksAndSignsTheVectors() throws { + let s = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy"), expectedRecipients: [try payment()]) + XCTAssertTrue(s.recipientsChecked) + XCTAssertEqual(s.policyFingerprint, "NFYW-K8E9-E5K9-AAK3") + XCTAssertEqual(s.chainId, 7) + XCTAssertEqual(s.expiresAt, 2024) + XCTAssertEqual(s.bindingDomain, 0) + XCTAssertEqual(s.outputs.count, 4) + XCTAssertEqual(s.outputs[2].amount, "10000000000") + XCTAssertEqual(s.outputs[2].matches, "A5SR-HRMB-SY4M-5ZYF (10 RAND)") + XCTAssertTrue(s.outputs[2].needsPayee) + XCTAssertTrue(s.outputs[3].toAccount) + XCTAssertEqual(s.expected, [MsauthExpected(entry: try payment(), fingerprint: try json("expected")["recipient_owner_fingerprint"] as? String)]) + XCTAssertEqual(s.lines, try json("expected")["lines_expecting_recipient"] as? [String]) + XCTAssertTrue(s.lines[1...4].allSatisfy { $0.contains("owner key verified") }) + + let expected = try json("expected") + let share = try RandCore.msauthSign(proposal: try fx("proposal-valid"), policy: try fx("policy"), + seed: try XCTUnwrap(Data(hex: try seeds()[2])), expectedRecipients: [try payment()]) + XCTAssertEqual(share, expected["share_2"] as? String) + XCTAssertNoThrow(try RandCore.msauthVerifyShare(proposal: try fx("proposal-valid"), policy: try fx("policy"), share: expected["share_0"] as! String)) + XCTAssertThrowsError(try RandCore.msauthCheck(proposal: try fx("proposal-unbound"), policy: try fx("policy"))) { e in + XCTAssertTrue((e as! RandCore.CoreError).message.contains("does not hash to the proposal's binding")) + } + let info = try RandCore.msauthPolicyInfo(try fx("policy")) + XCTAssertEqual(info.fingerprint, "NFYW-K8E9-E5K9-AAK3") + XCTAssertEqual(info.signers, 3) + XCTAssertEqual(try RandCore.msauthSignerImport(try seeds()[1], policy: try fx("policy")).index, 1) + XCTAssertNil(try RandCore.msauthSignerImport(try seeds()[1], policy: nil).index) + } + + func testASubstitutedRecipientIsRefusedByTheCoreInItsOwnSentence() throws { + let refusal = try XCTUnwrap(try json("expected")["substituted_refusal"] as? String) + XCTAssertThrowsError(try RandCore.msauthCheck(proposal: try fx("proposal-substituted-recipient"), policy: try fx("policy"), expectedRecipients: [try payment()])) { e in + XCTAssertEqual((e as! RandCore.CoreError).message, refusal) + } + XCTAssertThrowsError(try RandCore.msauthSign(proposal: try fx("proposal-substituted-recipient"), policy: try fx("policy"), + seed: try XCTUnwrap(Data(hex: try seeds()[0])), expectedRecipients: [try payment()])) { e in + XCTAssertEqual((e as! RandCore.CoreError).message, refusal) + } + } + + func testWithNoPayeeNamedTheCoreSaysUncheckedAndRefusesToSign() throws { + let none = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy")) + XCTAssertFalse(none.recipientsChecked) + XCTAssertEqual(MsauthRules.verdict(summary: none, head: 1, chainId: 7, genesis: nil, url: "u", recipients: []), .uncheckedRecipient) + XCTAssertThrowsError(try RandCore.msauthSign(proposal: try fx("proposal-valid"), policy: try fx("policy"), + seed: try XCTUnwrap(Data(hex: try seeds()[2])), expectedRecipients: [])) { e in + XCTAssertEqual((e as! RandCore.CoreError).message, "no expected recipient given: the payee was NOT checked") + } + } + + func testTheExpiryAndChainRulesOnTheVector() throws { + let s = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy"), expectedRecipients: [try payment()]) // chain 7, domain 0, expires 2024 + let r = [try payment()] + XCTAssertEqual(MsauthRules.expiryMargin, 16) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 2007, chainId: 7, genesis: nil, url: "u", recipients: r), .signable(blocksLeft: 17)) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 2008, chainId: 7, genesis: nil, url: "u", recipients: r), .expired(head: 2008, expiresAt: 2024)) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: nil, chainId: 7, genesis: nil, url: "http://n", recipients: r), .noNode(url: "http://n")) + XCTAssertEqual(MsauthRules.verdict(summary: s, head: 1, chainId: 21, genesis: nil, url: "u", recipients: r), .wrongChain(proposal: 7, wallet: 21)) + // The vector pays RAND only: the address alone is not enough, the amount is pinned. + let bare = try RandCore.msauthCheck(proposal: try fx("proposal-valid"), policy: try fx("policy"), expectedRecipients: [try payee()]) + XCTAssertEqual(MsauthRules.verdict(summary: bare, head: 1, chainId: 7, genesis: nil, url: "u", recipients: [try payee()]), .uncheckedRecipient) + } + + func testConfirmsAndClean() { + XCTAssertTrue(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: " aak3")) + XCTAssertFalse(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: "aak")) + XCTAssertFalse(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: "e5k9")) + XCTAssertEqual(MsauthRules.clean("\u{FEFF} {\"a\":1}\r\n"), "{\"a\":1}") + XCTAssertEqual(MsauthRules.payees([" rand1a:5 ", "rand1b:2\n"]), ["rand1a:5", "rand1b:2"]) + } + + // MARK: MsauthRules.verdict, rule by rule (ui/test/signers-engine.test.mjs's cases) + + private let genesis = String(repeating: "aa", count: 32) + private func summary(_ changes: [String: Any?] = [:]) -> [String: Any] { + var s: [String: Any] = ["expires_at": 200, "chain_id": 13, "genesis": genesis, "binding_domain": 1, "lines": ["x"], + "policy_fingerprint": "NFYW-K8E9-E5K9-AAK3", "recipients_checked": true, + "outputs": [["to_account": false, "amount": "5", "needs_payee": true, "matches": "FP"]]] + for (k, v) in changes { s[k] = v } // nil removes the field + return s + } + private func v(_ json: Any?, head: UInt64? = 1, recipients: [String] = ["rand1payee:5"]) -> MsauthVerdict { + MsauthRules.verdict(summary: MsauthSummary(json: json), head: head, chainId: 13, genesis: genesis, url: "u", recipients: recipients) + } + + func testAWellFormedCheckedSummaryIsSignable() { + XCTAssertEqual(v(summary()), .signable(blocksLeft: 199)) + } + + func testAMalformedSummaryIsRefused() { + let bad: [(String, Any?)] = [ + ("expires_at", nil), ("expires_at", "soon"), ("expires_at", -1), ("expires_at", 1.5), ("expires_at", NSNull()), + ("expires_at", 9_007_199_254_740_992), ("expires_at", true), + ("chain_id", nil), ("chain_id", "x"), ("chain_id", -1), ("chain_id", 2.5), + ("binding_domain", nil), ("binding_domain", "x"), ("binding_domain", 1.5), ("binding_domain", -1), + ("genesis", nil), ("genesis", ""), ("genesis", "zz"), ("genesis", 7), + ("outputs", nil), ("outputs", "x"), ("outputs", [1]), + ] + for (key, value) in bad { + XCTAssertEqual(v(summary([key: value])), .malformedSummary, "\(key) = \(String(describing: value))") + } + XCTAssertEqual(v(nil), .malformedSummary) + XCTAssertEqual(v(NSNull()), .malformedSummary) + XCTAssertEqual(v("a summary"), .malformedSummary) + XCTAssertEqual(v([summary()]), .malformedSummary, "an array is not an object") + } + + func testAMalformedOutputIsRefused() { + let out = { (o: Any) in self.summary(["outputs": [o]]) } + XCTAssertEqual(v(out(NSNull())), .malformedSummary) + XCTAssertEqual(v(out(["to_account": false, "matches": NSNull()])), .malformedSummary, "no amount") + XCTAssertEqual(v(out(["to_account": false, "amount": "", "matches": NSNull()])), .malformedSummary) + XCTAssertEqual(v(out(["to_account": false, "amount": "abc", "matches": NSNull()])), .malformedSummary) + XCTAssertEqual(v(out(["to_account": false, "amount": -5, "matches": NSNull()])), .malformedSummary) + XCTAssertEqual(v(out(["to_account": false, "amount": 1.5, "matches": NSNull()])), .malformedSummary) + XCTAssertEqual(v(out(["amount": "5", "matches": NSNull()])), .malformedSummary, "to_account missing") + XCTAssertEqual(v(out(["to_account": 0, "amount": "5", "matches": "FP"])), .malformedSummary, "to_account is a Bool, not a number") + XCTAssertEqual(v(out(["to_account": "false", "amount": "5", "matches": "FP"])), .malformedSummary) + for amount: Any in ["0", "00", " 0", 0] { + XCTAssertEqual(v(out(["to_account": false, "amount": amount, "matches": NSNull()])), .signable(blocksLeft: 199), "\(amount) is zero") + } + XCTAssertEqual(v(out(["to_account": false, "amount": 5, "matches": "FP"])), .signable(blocksLeft: 199), "a non-negative integer amount") + } + + func testAnUncheckedSummaryIsRefusedBeforeAnythingElse() { + let unchecked = summary(["recipients_checked": false, "outputs": [["to_account": false, "amount": "5", "matches": NSNull()]]]) + XCTAssertEqual(v(unchecked), .uncheckedRecipient) + XCTAssertEqual(v(unchecked, head: nil), .uncheckedRecipient) + XCTAssertEqual(v(summary(["recipients_checked": nil])), .uncheckedRecipient) + XCTAssertEqual(v(summary(["recipients_checked": 1])), .uncheckedRecipient, "only true is true") + XCTAssertEqual(v(summary(["recipients_checked": "true"])), .uncheckedRecipient) + } + + func testAPayingOutputWithoutAMatchIsUnchecked() { + let o = { (x: [String: Any]) in self.v(self.summary(["outputs": [x]])) } + XCTAssertEqual(o(["to_account": false, "amount": "5", "needs_payee": true, "matches": NSNull()]), .uncheckedRecipient) + XCTAssertEqual(o(["to_account": false, "amount": "5", "needs_payee": true]), .uncheckedRecipient) + for m: Any in [false, "", " ", 0, [String: Any](), NSNull(), true] { + XCTAssertEqual(o(["to_account": false, "amount": "5", "matches": m]), .uncheckedRecipient, "\(m)") + } + XCTAssertEqual(o(["to_account": true, "amount": "5", "matches": NSNull()]), .signable(blocksLeft: 199), "change needs no payee") + XCTAssertEqual(o(["to_account": false, "amount": "0", "matches": NSNull()]), .signable(blocksLeft: 199), "a zero amount pays nothing") + } + + func testAnEmptyOrBlankPaymentListIsUnchecked() { + XCTAssertEqual(v(summary(), recipients: []), .uncheckedRecipient, "a match alone does not make an empty list pass") + XCTAssertEqual(v(summary(["outputs": [["to_account": true, "amount": "5"]]]), recipients: []), .signable(blocksLeft: 199), "nothing pays: nothing to name") + XCTAssertEqual(v(summary(), recipients: ["rand1a:5", ""]), .uncheckedRecipient) + XCTAssertEqual(v(summary(), recipients: ["rand1a:5", " "]), .uncheckedRecipient) + XCTAssertEqual(v(summary(), recipients: [":5"]), .uncheckedRecipient, "an entry without its address") + } + + func testAnAmountIsPinnedOnEveryRandPayment() { + XCTAssertEqual(v(summary(), recipients: ["rand1a"]), .uncheckedRecipient) + XCTAssertEqual(v(summary(), recipients: ["rand1a:"]), .uncheckedRecipient) + XCTAssertEqual(v(summary(), recipients: ["rand1a: "]), .uncheckedRecipient) + // A RAND output owned by the entry's fingerprint still needs the amount. + let rand = summary(["outputs": [["to_account": false, "amount": "5", "asset": 0, "owner_fingerprint": "F", "matches": "ABCD"]], + "expected": [["entry": "rand1a", "fingerprint": "F"]]]) + XCTAssertEqual(v(rand, recipients: ["rand1a"]), .uncheckedRecipient) + } + + func testTheAmountlessExceptionIsPerRowForNonRandOutputs() { + let token = summary(["outputs": [["to_account": false, "amount": "5", "asset": 7, "owner_fingerprint": "F", "matches": "ABCD"]], + "expected": [["entry": "rand1a", "fingerprint": "F"]]]) + XCTAssertEqual(v(token, recipients: ["rand1a"]), .signable(blocksLeft: 199)) + let mixed = { (expected: Any?) -> [String: Any] in + self.summary(["expected": expected, + "outputs": [["to_account": false, "amount": "5", "asset": 7, "owner_fingerprint": "F", "matches": "ABCD"], + ["to_account": false, "amount": "9", "asset": 0, "owner_fingerprint": "G", "matches": "EFGH"]]]) + } + let exp: [[String: Any]] = [["entry": "rand1f", "fingerprint": "F"], ["entry": "rand1g", "fingerprint": "G"]] + XCTAssertEqual(v(mixed(exp), recipients: ["rand1f", "rand1g:9"]), .signable(blocksLeft: 199), "the non-RAND owner may go unpinned") + XCTAssertEqual(v(mixed(exp), recipients: ["rand1f:5", "rand1g"]), .uncheckedRecipient, "a RAND payee may not") + XCTAssertEqual(v(mixed(nil), recipients: ["rand1f"]), .uncheckedRecipient, "expected missing: fail closed") + XCTAssertEqual(v(mixed("nope"), recipients: ["rand1f"]), .uncheckedRecipient) + XCTAssertEqual(v(mixed([["entry": "rand1f"]]), recipients: ["rand1f"]), .uncheckedRecipient, "no fingerprint: fail closed") + XCTAssertEqual(v(mixed([["entry": "rand1f", "fingerprint": ""]]), recipients: ["rand1f"]), .uncheckedRecipient) + XCTAssertEqual(v(mixed([["entry": " rand1f", "fingerprint": "F"]]), recipients: ["rand1f"]), .uncheckedRecipient, "matched by exact entry") + XCTAssertEqual(v(mixed([["entry": "rand1f", "fingerprint": "G"]]), recipients: ["rand1f"]), .uncheckedRecipient, "G owns RAND only") + // A zero-amount non-RAND output is not a paying output: it unpins nothing. + let dust = summary(["expected": [["entry": "rand1a", "fingerprint": "F"]], + "outputs": [["to_account": false, "amount": "0", "asset": 7, "owner_fingerprint": "F", "matches": NSNull()], + ["to_account": false, "amount": "9", "asset": 0, "owner_fingerprint": "G", "matches": "EFGH"]]]) + XCTAssertEqual(v(dust, recipients: ["rand1a"]), .uncheckedRecipient) + // Change in a token is not a payment either. + let change = summary(["expected": [["entry": "rand1a", "fingerprint": "F"]], + "outputs": [["to_account": true, "amount": "5", "asset": 7, "owner_fingerprint": "F"], + ["to_account": false, "amount": "9", "asset": 0, "owner_fingerprint": "G", "matches": "EFGH"]]]) + XCTAssertEqual(v(change, recipients: ["rand1a"]), .uncheckedRecipient) + } + + func testTheExpiryChainAndGenesisRules() { + XCTAssertEqual(v(summary(), head: 183), .signable(blocksLeft: 17)) + XCTAssertEqual(v(summary(), head: 184), .expired(head: 184, expiresAt: 200)) + XCTAssertEqual(v(summary(), head: 500), .expired(head: 500, expiresAt: 200)) + XCTAssertEqual(v(summary(), head: nil), .noNode(url: "u")) + XCTAssertEqual(v(summary(["chain_id": 21])), .wrongChain(proposal: 21, wallet: 13)) + XCTAssertEqual(v(summary(["genesis": String(repeating: "bb", count: 32)])), .wrongGenesis) + XCTAssertEqual(v(summary(["genesis": genesis.uppercased()])), .signable(blocksLeft: 199), "hex case does not matter") + XCTAssertEqual(v(summary(["binding_domain": 0, "genesis": String(repeating: "bb", count: 32)])), .signable(blocksLeft: 199), + "a chain-id-bound proposal binds no genesis") + XCTAssertEqual(MsauthRules.verdict(summary: MsauthSummary(json: summary()), head: 1, chainId: 13, genesis: nil, url: "u", recipients: ["rand1a:5"]), + .wrongGenesis, "a node that names no genesis cannot vouch for a genesis-bound proposal") + XCTAssertNotNil(MsauthVerdict.expired(head: 1, expiresAt: 2).message) + XCTAssertNil(MsauthVerdict.signable(blocksLeft: 1).message) + } + + // MARK: the signer store + + func testTheStoreKeepsTheSeedBehindPresenceAndReadsItPerSignature() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let p = try store.add(label: " treasury ", policy: try fx("policy"), key: try seeds()[0]) + XCTAssertEqual(p.index, 0) + XCTAssertEqual(p.label, "treasury") + XCTAssertEqual(p.fingerprint, "NFYW-K8E9-E5K9-AAK3") + XCTAssertEqual(secrets.items["msauth.\(p.id).seed"]?.presence, true) + XCTAssertEqual(secrets.items["msauth.\(p.id).seed"]?.data, Data(hex: try seeds()[0])) + XCTAssertEqual(secrets.items["msauth.\(p.id).policy"]?.presence, false) + XCTAssertEqual(secrets.items["msauth.index"]?.presence, false) + XCTAssertTrue(secrets.promptedReads.isEmpty, "adding asks for nothing") + XCTAssertEqual(try store.policy(p.id), MsauthRules.clean(try fx("policy"))) + XCTAssertTrue(secrets.promptedReads.isEmpty, "the policy is not behind the prompt") + for _ in 0..<2 { _ = try store.withSeed(p.id, reason: "sign") { $0.count } } + XCTAssertEqual(secrets.promptedReads, ["msauth.\(p.id).seed", "msauth.\(p.id).seed"], "one prompted read per signature, nothing cached") + XCTAssertThrowsError(try store.add(label: "again", policy: try fx("policy"), key: try seeds()[0])) + XCTAssertEqual(SignerStore(secrets: secrets).profiles, store.profiles, "the list survives a restart") + store.remove(p.id) + XCTAssertNil(secrets.items["msauth.\(p.id).seed"]) + XCTAssertNil(secrets.items["msauth.\(p.id).policy"]) + XCTAssertTrue(store.profiles.isEmpty) + XCTAssertThrowsError(try store.withSeed(p.id, reason: "sign") { $0.count }) + } + + func testAddRefusesABadLabelAKeyOutsideThePolicyAndNoPasscode() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + XCTAssertThrowsError(try store.add(label: " ", policy: try fx("policy"), key: try seeds()[0])) + XCTAssertThrowsError(try store.add(label: String(repeating: "x", count: 65), policy: try fx("policy"), key: try seeds()[0])) + let outsider = try XCTUnwrap(try json("signer-seeds")["non_member"] as? String) + XCTAssertThrowsError(try store.add(label: "x", policy: try fx("policy"), key: outsider)) { e in + XCTAssertEqual((e as? RandCore.CoreError)?.message, "this key is not one of the policy's signers") + } + XCTAssertTrue(secrets.items.isEmpty) + secrets.canRequireUserPresence = false + XCTAssertThrowsError(try store.add(label: "x", policy: try fx("policy"), key: try seeds()[0])) + XCTAssertTrue(secrets.items.isEmpty, "no passcode, no profile, nothing written") + } + + func testAFailedSaveLeavesNothingBehind() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + secrets.failSaveFor = "msauth.index" + XCTAssertThrowsError(try store.add(label: "x", policy: try fx("policy"), key: try seeds()[0])) + XCTAssertTrue(secrets.items.isEmpty, "\(secrets.items.keys)") + XCTAssertTrue(store.profiles.isEmpty) + } + + func testRemoveAllForgetsEveryProfile() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + _ = try store.add(label: "a", policy: try fx("policy"), key: try seeds()[0]) + _ = try store.add(label: "b", policy: try fx("policy"), key: try seeds()[1]) + try store.removeAll() + XCTAssertTrue(store.profiles.isEmpty) + XCTAssertTrue(secrets.items.isEmpty, "\(secrets.items.keys)") + } + + func testRemoveAllSweepsOrphansTheIndexDoesNotList() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + _ = try store.add(label: "a", policy: try fx("policy"), key: try seeds()[0]) + // A crash between the seed and the index, or an index that no longer decodes. + secrets.items["msauth.orphan.seed"] = (Data([1, 2, 3]), true) + secrets.items["msauth.orphan.policy"] = (Data("{}".utf8), false) + secrets.items["spend_key"] = (Data("keep".utf8), false) + secrets.items["prover_pairing"] = (Data("keep".utf8), false) + try store.removeAll() + XCTAssertEqual(Set(secrets.items.keys), ["spend_key", "prover_pairing"], "every msauth.* item is gone, nothing else") + let broken = FakeSignerSecrets() + broken.items["msauth.index"] = (Data("not json".utf8), false) + broken.items["msauth.x.seed"] = (Data([9]), true) + try SignerStore(secrets: broken).removeAll() + XCTAssertTrue(broken.items.isEmpty, "\(broken.items.keys)") + } + + func testAFailedSweepKeepsTheIndexAndSaysSo() throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let p = try store.add(label: "a", policy: try fx("policy"), key: try seeds()[0]) + secrets.items["msauth.orphan.seed"] = (Data([1]), true) + secrets.failListing = true + XCTAssertThrowsError(try store.removeAll()) + XCTAssertNotNil(secrets.items["msauth.index"], "the index survives a failed sweep") + XCTAssertNotNil(secrets.items["msauth.orphan.seed"]) + XCTAssertEqual(store.profiles.map(\.id), [p.id]) + secrets.failListing = false + XCTAssertNoThrow(try store.removeAll(), "a retry finishes the job") + XCTAssertTrue(secrets.items.isEmpty, "\(secrets.items.keys)") + XCTAssertTrue(store.profiles.isEmpty) + } + + func testTheLabelLimitCountsCodePoints() throws { + let store = SignerStore(secrets: FakeSignerSecrets()) + // 33 characters, 66 code points (e + combining acute): over the limit, as in the JS. + XCTAssertThrowsError(try store.add(label: String(repeating: "e\u{301}", count: 33), policy: try fx("policy"), key: try seeds()[0])) + XCTAssertNoThrow(try store.add(label: String(repeating: "e\u{301}", count: 32), policy: try fx("policy"), key: try seeds()[0])) + } + + // MARK: MsauthSigning: check, then sign, in the order the rules need + + private final class FakeNode: MsauthNode { + var nodeURL = "https://node.test" + var chain: UInt64 = 13 + var heads: [UInt64] = [100] + var genesis: String? + var failHead = false + func chainId() async throws -> UInt64 { chain } + func headHeight() async throws -> UInt64 { + if failHead { throw RpcClient.RpcError(code: -32000, message: "busy") } + return heads.count > 1 ? heads.removeFirst() : heads[0] + } + func genesisHash() async throws -> String? { genesis } + } + private final class FakeCore: MsauthCore { + var summary: Any? = nil + var calls: [(String, String, [String])] = [] + var seeds: [Data] = [] + func check(proposal: String, policy: String, expectedRecipients: [String]) throws -> MsauthSummary { + calls.append(("check", proposal, expectedRecipients)); return MsauthSummary(json: summary) + } + func sign(proposal: String, policy: String, seed: Data, expectedRecipients: [String]) throws -> String { + calls.append(("sign", proposal, expectedRecipients)); seeds.append(Data(seed)); return "1:abcd" + } + } + private func signing(_ core: FakeCore, _ node: FakeNode?) throws -> (MsauthSigning, FakeSignerSecrets, SignerProfile) { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let p = try store.add(label: "treasury", policy: try fx("policy"), key: try seeds()[1]) + node?.genesis = genesis + return (MsauthSigning(store: store, core: core, node: node, chainId: 13), secrets, p) + } + + func testSignHandsTheCoreTheSameCleanedListTheCheckUsed() async throws { + let core = FakeCore(); core.summary = summary() + let (m, secrets, p) = try signing(core, FakeNode()) + let share = try await m.sign(id: p.id, proposal: "\u{FEFF}PROP\n", expectedRecipients: [" rand1payee:5 ", "rand1other:2\n"], reason: "sign") + XCTAssertEqual(share, MsauthShare(share: "1:abcd", index: 1)) + XCTAssertEqual(core.calls.map { $0.0 }, ["check", "sign"]) + XCTAssertEqual(core.calls.map { $0.1 }, ["PROP", "PROP"]) + XCTAssertEqual(core.calls.map { $0.2 }, [["rand1payee:5", "rand1other:2"], ["rand1payee:5", "rand1other:2"]]) + XCTAssertEqual(core.seeds, [try XCTUnwrap(Data(hex: try seeds()[1]))]) + XCTAssertEqual(secrets.promptedReads, ["msauth.\(p.id).seed"]) + } + + func testACancelledSignStopsBeforeTheDevicePrompt() async throws { + let core = FakeCore(); core.summary = summary() + let (m, secrets, p) = try signing(core, FakeNode()) + let task = Task { () -> Bool in + withUnsafeCurrentTask { $0?.cancel() } // the screen was left + do { + _ = try await m.sign(id: p.id, proposal: "P", expectedRecipients: ["rand1payee:5"], reason: "sign") + return false + } catch is CancellationError { return true } catch { return false } + } + let stopped = await task.value + XCTAssertTrue(stopped) + XCTAssertEqual(secrets.promptedReads, [], "no Face ID prompt") + XCTAssertFalse(core.calls.contains { $0.0 == "sign" }, "msauth_sign never ran") + } + + func testTheRuleIsReRunAfterDeviceAuthAndARefusalThereSignsNothing() async throws { + let core = FakeCore(); core.summary = summary() + let node = FakeNode(); node.heads = [100, 190] + let (m, secrets, p) = try signing(core, node) + do { + _ = try await m.sign(id: p.id, proposal: "P", expectedRecipients: ["rand1payee:5"], reason: "sign") + XCTFail("signed past the expiry") + } catch let e as MsauthRefusal { + XCTAssertEqual(e.verdict, .expired(head: 190, expiresAt: 200)) + } + XCTAssertEqual(secrets.promptedReads.count, 1, "the head moved during the prompt") + XCTAssertEqual(core.calls.map { $0.0 }, ["check"], "msauth_sign never ran") + + // The chain moves while the person authenticates: the head read after the prompt decides. + let node2 = FakeNode() + let (m2, secrets2, p2) = try signing(core, node2) + secrets2.duringPrompt = { node2.heads = [184] } + core.calls = [] + do { + _ = try await m2.sign(id: p2.id, proposal: "P", expectedRecipients: ["rand1payee:5"], reason: "sign") + XCTFail("signed past the expiry") + } catch let e as MsauthRefusal { + XCTAssertEqual(e.verdict, .expired(head: 184, expiresAt: 200)) + } + XCTAssertEqual(core.calls.map { $0.0 }, ["check"]) + + // Or the node goes away during the prompt. + let node3 = FakeNode() + let (m3, secrets3, p3) = try signing(core, node3) + secrets3.duringPrompt = { node3.failHead = true } + core.calls = [] + do { + _ = try await m3.sign(id: p3.id, proposal: "P", expectedRecipients: ["rand1payee:5"], reason: "sign") + XCTFail("signed with no node") + } catch let e as MsauthRefusal { + XCTAssertEqual(e.verdict, .noNode(url: "https://node.test")) + } + XCTAssertEqual(core.calls.map { $0.0 }, ["check"]) + } + + func testARefusalBeforeAuthNeverReadsTheSeed() async throws { + let cases: [(Any?, FakeNode?, [String], MsauthVerdict)] = [ + (summary(), nil, ["rand1payee:5"], .noNode(url: "")), + (summary(["chain_id": 21]), FakeNode(), ["rand1payee:5"], .wrongChain(proposal: 21, wallet: 13)), + (summary(["genesis": String(repeating: "bb", count: 32)]), FakeNode(), ["rand1payee:5"], .wrongGenesis), + (summary(), { let n = FakeNode(); n.heads = [184]; return n }(), ["rand1payee:5"], .expired(head: 184, expiresAt: 200)), + (summary(["recipients_checked": false]), FakeNode(), ["rand1payee:5"], .uncheckedRecipient), + (summary(["expires_at": "soon"]), FakeNode(), ["rand1payee:5"], .malformedSummary), + (summary(), FakeNode(), [], .uncheckedRecipient), + (summary(), FakeNode(), ["rand1payee"], .uncheckedRecipient), + (summary(), { let n = FakeNode(); n.failHead = true; return n }(), ["rand1payee:5"], .noNode(url: "https://node.test")), + (summary(), { let n = FakeNode(); n.chain = 21; return n }(), ["rand1payee:5"], .noNode(url: "https://node.test")), + ] + for (i, (s, node, recipients, want)) in cases.enumerated() { + let core = FakeCore(); core.summary = s + let (m, secrets, p) = try signing(core, node) + do { + _ = try await m.sign(id: p.id, proposal: "P", expectedRecipients: recipients, reason: "sign") + XCTFail("case \(i) signed") + } catch let e as MsauthRefusal { + XCTAssertEqual(e.verdict, want, "case \(i)") + } + XCTAssertTrue(secrets.promptedReads.isEmpty, "case \(i): no prompt for a proposal that will not be signed") + XCTAssertFalse(core.calls.contains { $0.0 == "sign" }, "case \(i)") + } + } + + func testABlankEntryIsRefusedAndNeverReachesTheCore() async throws { + let core = FakeCore(); core.summary = summary() + let (m, secrets, p) = try signing(core, FakeNode()) + let r = try await m.check(id: p.id, proposal: "P", expectedRecipients: ["rand1a:1", " "]) + XCTAssertNil(r.summary) + XCTAssertEqual(r.verdict, .uncheckedRecipient) + do { + _ = try await m.sign(id: p.id, proposal: "P", expectedRecipients: ["", "rand1b:1"], reason: "sign") + XCTFail("signed with a blank entry") + } catch let e as MsauthRefusal { XCTAssertEqual(e.verdict, .uncheckedRecipient) } + XCTAssertTrue(core.calls.isEmpty, "nothing was passed on with the blanks removed") + XCTAssertTrue(secrets.promptedReads.isEmpty) + } + + func testAHeadPastTheSafeIntegerRangeIsNoNode() async throws { + let core = FakeCore(); core.summary = summary(["expires_at": 9_007_199_254_740_991]) + let node = FakeNode(); node.heads = [9_007_199_254_740_992] + let (m, _, p) = try signing(core, node) + let r = try await m.check(id: p.id, proposal: "P", expectedRecipients: ["rand1payee:5"]) + XCTAssertEqual(r.verdict, .noNode(url: "https://node.test")) + node.heads = [9_007_199_254_740_991 - 17] + let ok = try await m.check(id: p.id, proposal: "P", expectedRecipients: ["rand1payee:5"]) + XCTAssertEqual(ok.verdict, .signable(blocksLeft: 17)) + } + + func testWithNoNodeTheCheckStillShowsTheSummary() async throws { + let core = FakeCore(); core.summary = summary() + let (m, _, p) = try signing(core, nil) + let r = try await m.check(id: p.id, proposal: "P", expectedRecipients: ["rand1payee:5"]) + XCTAssertEqual(r.summary?.lines, ["x"]) + XCTAssertEqual(r.verdict, .noNode(url: "")) + let node = FakeNode(); node.failHead = true + let (m2, _, p2) = try signing(core, node) + let r2 = try await m2.check(id: p2.id, proposal: "P", expectedRecipients: ["rand1payee:5"]) + XCTAssertEqual(r2.verdict, .noNode(url: "https://node.test")) + } + + func testTheRealCoreSignsTheVectorThroughTheStore() async throws { + let secrets = FakeSignerSecrets() + let store = SignerStore(secrets: secrets) + let p = try store.add(label: "two", policy: try fx("policy"), key: try seeds()[2]) + let node = FakeNode(); node.chain = 7; node.heads = [2000] + let m = MsauthSigning(store: store, core: RandMsauthCore(), node: node, chainId: 7) + let r = try await m.check(id: p.id, proposal: try fx("proposal-valid"), expectedRecipients: [try payment()]) + XCTAssertEqual(r.verdict, .signable(blocksLeft: 24)) + let share = try await m.sign(id: p.id, proposal: try fx("proposal-valid"), expectedRecipients: [try payment()], reason: "sign") + XCTAssertEqual(share.share, try json("expected")["share_2"] as? String) + XCTAssertEqual(share.index, 2) + XCTAssertNoThrow(try m.verifyShare(id: p.id, proposal: try fx("proposal-valid"), share: "\u{FEFF}" + (try json("expected")["share_0"] as! String) + "\n")) + } + + // MARK: the core's sentences reach the screen in the user's language + + func testTheMsauthSentencesAreKnown() { + for s in ["this key is not one of the policy's signers", "a signer seed is 64 hex characters", + "no expected recipient given: the payee was NOT checked", "the transaction does not hash to the proposal's binding"] { + XCTAssertEqual(CoreErrors.localized(s), s, "English passes through") + } + XCTAssertEqual(CoreErrors.localized("signer 4 is not in this policy (it has 3 signers)"), "signer 4 is not in this policy (it has 3 signers)") + XCTAssertEqual(CoreErrors.localized("expected recipient rand1x: shielded address is not base58"), "expected recipient rand1x: shielded address is not base58") + } +} diff --git a/ios/RandWalletTests/SettingsMigrationTests.swift b/ios/RandWalletTests/SettingsMigrationTests.swift new file mode 100644 index 0000000..e4cf7bd --- /dev/null +++ b/ios/RandWalletTests/SettingsMigrationTests.swift @@ -0,0 +1,47 @@ +import XCTest +@testable import RandWallet + +/// The one-time chain-id migration (wallet v0.8.0 runs chain 21 onward): a saved 20 becomes 21 on +/// the first launch, once, and nothing else changes. +final class SettingsMigrationTests: XCTestCase { + private var suite: String! + private var defaults: UserDefaults! + + override func setUp() { + suite = "SettingsMigrationTests.\(UUID().uuidString)" + defaults = UserDefaults(suiteName: suite) + } + override func tearDown() { defaults.removePersistentDomain(forName: suite) } + + func testASavedTwentyBecomesTwentyOneAndNothingElseChanges() { + defaults.set(20, forKey: "chainId") + defaults.set("http://10.0.0.5:8545", forKey: "rpcUrl") + defaults.set(5, forKey: "autoLockMinutes") + Settings.migrateChainId(defaults) + XCTAssertEqual(defaults.object(forKey: "chainId") as? Int, 21) + XCTAssertEqual(defaults.string(forKey: "rpcUrl"), "http://10.0.0.5:8545", "the RPC URL is not touched") + XCTAssertEqual(defaults.object(forKey: "autoLockMinutes") as? Int, 5) + XCTAssertTrue(defaults.bool(forKey: Settings.chainMigrationKey)) + } + + func testItRunsOnce() { + defaults.set(20, forKey: "chainId") + Settings.migrateChainId(defaults) + defaults.set(20, forKey: "chainId") // the user chose 20 again afterwards + Settings.migrateChainId(defaults) + XCTAssertEqual(defaults.object(forKey: "chainId") as? Int, 20, "a later launch leaves the user's choice alone") + } + + func testOtherValuesAndNoValueAreLeftAlone() { + defaults.set(13, forKey: "chainId") + Settings.migrateChainId(defaults) + XCTAssertEqual(defaults.object(forKey: "chainId") as? Int, 13) + + let fresh = "\(suite!).fresh" + let empty = UserDefaults(suiteName: fresh)! + defer { empty.removePersistentDomain(forName: fresh) } + Settings.migrateChainId(empty) + XCTAssertNil(empty.object(forKey: "chainId"), "no saved chain id: none is written") + XCTAssertEqual(Set(empty.persistentDomain(forName: fresh)?.keys.map { $0 } ?? []), [Settings.chainMigrationKey]) + } +} diff --git a/ios/RandWalletTests/SignScreenTests.swift b/ios/RandWalletTests/SignScreenTests.swift new file mode 100644 index 0000000..8b00ad2 --- /dev/null +++ b/ios/RandWalletTests/SignScreenTests.swift @@ -0,0 +1,403 @@ +import XCTest +@testable import RandWallet + +/// The logic behind the multisig signing screens (SignProposalView): rows to `addr:AMOUNT` entries, +/// when an amount may be left out, the stale tracking that disarms Sign after an edit, the last-four +/// confirmation and how each refusal is shown. The same behavior as ui/screens/signers.js. +final class SignScreenTests: XCTestCase { + private func fx(_ name: String) throws -> String { + let url = try XCTUnwrap(Bundle(for: SignScreenTests.self).url(forResource: name, withExtension: "json")) + return try String(contentsOf: url, encoding: .utf8) + } + private let genesis = String(repeating: "aa", count: 32) + private func summary(_ outputs: [[String: Any]], expected: [[String: Any]] = []) -> MsauthSummary { + MsauthSummary(json: ["expires_at": 200, "chain_id": 13, "genesis": genesis, "binding_domain": 1, "lines": ["line one", "line two"], + "policy_fingerprint": "NFYW-K8E9-E5K9-AAK3", "recipients_checked": true, "fee": "1000000", + "outputs": outputs, "expected": expected]) + } + private func verdict(_ s: MsauthSummary, head: UInt64? = 1, recipients: [String] = ["rand1payee:5"]) -> MsauthVerdict { + MsauthRules.verdict(summary: s, head: head, chainId: 13, genesis: genesis, url: "http://node", recipients: recipients) + } + private let rand5: [String: Any] = ["to_account": false, "amount": "5", "asset": 0, "owner_fingerprint": "G", "matches": "GGGG (5 RAND)"] + private let token: [String: Any] = ["to_account": false, "amount": "5", "asset": 7, "owner_fingerprint": "F", "matches": "FFFF"] + + // MARK: final fix wave: a dust token does not unpin its payee's RAND + + /// The reviewer's probe: X is paid 900 RAND beside 1 unit of asset 5, named with no amount. + func testADustTokenBesideARandOutputToTheSamePayeeIsRefused() { + let json: [String: Any] = ["recipients_checked": true, "expires_at": 1000, "chain_id": 21, "binding_domain": 1, + "genesis": String(repeating: "0", count: 64), "lines": ["x"], + "outputs": [["to_account": false, "amount": "900000000000", "asset": 0, "owner_fingerprint": "X", "matches": "AAAA (900 RAND)"], + ["to_account": false, "amount": "1", "asset": 5, "owner_fingerprint": "X", "matches": "AAAA (0.000000001 RAND)"], + ["to_account": true, "amount": "5", "asset": 0, "owner_fingerprint": "self", "matches": NSNull()]], + "expected": [["entry": "rand1payee", "fingerprint": "X"]]] + let v = MsauthRules.verdict(summary: MsauthSummary(json: json), head: 10, chainId: 21, genesis: String(repeating: "0", count: 64), + url: "u", recipients: ["rand1payee"]) + XCTAssertEqual(v, .uncheckedRecipient) + // A missing asset counts as RAND (fails closed). + var noAsset = json + var outs = json["outputs"] as! [[String: Any]] + outs[0].removeValue(forKey: "asset") + noAsset["outputs"] = outs + XCTAssertEqual(MsauthRules.verdict(summary: MsauthSummary(json: noAsset), head: 10, chainId: 21, genesis: String(repeating: "0", count: 64), + url: "u", recipients: ["rand1payee"]), .uncheckedRecipient) + // The payee paid the token alone may still go unpinned. + var tokenOnly = json + tokenOnly["outputs"] = Array((json["outputs"] as! [[String: Any]])[1...]) + XCTAssertEqual(MsauthRules.verdict(summary: MsauthSummary(json: tokenOnly), head: 10, chainId: 21, genesis: String(repeating: "0", count: 64), + url: "u", recipients: ["rand1payee"]), .signable(blocksLeft: 990)) + } + + func testAPayeePaidRandAndATokenIsNotOfferedTheExceptionAndIsToldWhy() { + let xRand: [String: Any] = ["to_account": false, "amount": "900", "asset": 0, "owner_fingerprint": "X", "matches": "a"] + let xTok: [String: Any] = ["to_account": false, "amount": "1", "asset": 5, "owner_fingerprint": "X", "matches": "b"] + let s = summary([xRand, xTok, token], expected: [["entry": "rand1x", "fingerprint": "X"], ["entry": "rand1f", "fingerprint": "F"]]) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1x", amount: ""), summary: s)) + XCTAssertTrue(SignRows.mixedPayee(PaymentRow(addr: "rand1x", amount: ""), summary: s)) + XCTAssertTrue(SignRows.amountOptional(PaymentRow(addr: "rand1f", amount: ""), summary: s), "a token-only payee keeps the exception") + XCTAssertFalse(SignRows.mixedPayee(PaymentRow(addr: "rand1f", amount: ""), summary: s)) + XCTAssertFalse(SignRows.mixedPayee(PaymentRow(addr: "rand1new", amount: ""), summary: s), "unknown rows are not called mixed") + } + + func testLinesMissingOrNotAllStringsIsMalformed() { + for lines in [nil, "x", ["x", 1], [NSNull()], ["a": "b"]] as [Any?] { + var json: [String: Any] = ["expires_at": 200, "chain_id": 13, "genesis": genesis, "binding_domain": 1, + "policy_fingerprint": "NFYW-K8E9-E5K9-AAK3", "recipients_checked": true, "fee": "1000000", + "outputs": [rand5], "expected": []] + if let lines { json["lines"] = lines } + XCTAssertEqual(verdict(MsauthSummary(json: json), recipients: ["rand1g:5"]), .malformedSummary, String(describing: lines)) + } + XCTAssertEqual(verdict(summary([rand5]), recipients: ["rand1g:5"]), .signable(blocksLeft: 199)) + } + + func testTheConfirmationIgnoresInnerWhitespaceAsTheWebDoes() { + XCTAssertTrue(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: "a a k 3")) + XCTAssertTrue(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: " A-A\tk3\n")) + XCTAssertFalse(MsauthRules.confirms(fingerprint: "NFYW-K8E9-E5K9-AAK3", typed: "a a k")) + } + + // MARK: rows to entries + + func testARowBecomesAddrColonAmountAsTyped() throws { + XCTAssertEqual(try SignRows.entry(PaymentRow(addr: " rand1a ", amount: " 1.5 "), amountOptional: false), "rand1a:1.5") + XCTAssertEqual(try SignRows.entry(PaymentRow(addr: "rand1a", amount: ".25"), amountOptional: false), "rand1a:.25") + } + + func testARowNeedsAnAddressAndAValidPositiveAmount() { + func reason(_ r: PaymentRow, optional: Bool = false) -> String? { + do { _ = try SignRows.entry(r, amountOptional: optional); return nil } catch { return error.localizedDescription } + } + XCTAssertEqual(reason(PaymentRow(addr: " ", amount: "1")), "Enter the address of every payment.") + XCTAssertEqual(reason(PaymentRow(addr: "", amount: ""), optional: true), "Enter the address of every payment.") + XCTAssertEqual(reason(PaymentRow(addr: "rand1a", amount: "")), "Enter the amount of every payment: it is what stops a split or an inflated payment.") + for bad in ["0", "0.0", "-1", "1.5.2", "abc", "1,5", "0.0000000001"] { + XCTAssertEqual(reason(PaymentRow(addr: "rand1a", amount: bad)), "An amount is a number of RAND, such as 1.5.", bad) + } + } + + func testAnAmountMayBeLeftOutOnlyWhereTheRowIsMarkedOptional() throws { + XCTAssertEqual(try SignRows.entry(PaymentRow(addr: " rand1f ", amount: ""), amountOptional: true), "rand1f") + // An amount typed on an optional row is still sent (and checked). + XCTAssertEqual(try SignRows.entry(PaymentRow(addr: "rand1f", amount: "2"), amountOptional: true), "rand1f:2") + XCTAssertThrowsError(try SignRows.entry(PaymentRow(addr: "rand1f", amount: "x"), amountOptional: true)) + } + + func testAnAmountlessRowIsOffloadedOnlyForTheOwnerOfANonRandPayingOutput() { + let s = summary([token, rand5], expected: [["entry": "rand1f", "fingerprint": "F"], ["entry": "rand1g:5", "fingerprint": "G"]]) + XCTAssertTrue(SignRows.amountOptional(PaymentRow(addr: "rand1f", amount: ""), summary: s), "F owns the token output") + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1g", amount: "5"), summary: s), "G owns RAND only") + // Not yet fingerprinted (or edited): optional while some paying output is non-RAND, so a check can tell. + XCTAssertTrue(SignRows.amountOptional(PaymentRow(addr: "rand1new", amount: ""), summary: s)) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1new", amount: ""), summary: summary([rand5]))) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1f", amount: ""), summary: nil), "no check yet: always pinned") + // An expected entry with no fingerprint fails closed. + let noFp = summary([token], expected: [["entry": "rand1f"]]) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1f", amount: ""), summary: noFp)) + // A zero amount or change in a token is not a paying output. + let dust = summary([["to_account": false, "amount": "0", "asset": 7, "owner_fingerprint": "F"], rand5]) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1new", amount: ""), summary: dust)) + let change = summary([["to_account": true, "amount": "5", "asset": 7, "owner_fingerprint": "F"], rand5]) + XCTAssertFalse(SignRows.amountOptional(PaymentRow(addr: "rand1new", amount: ""), summary: change)) + } + + func testTheRowKeyTellsEveryEditApart() { + let a = [PaymentRow(addr: "a", amount: "1"), PaymentRow(addr: "b", amount: "2")] + XCTAssertEqual(SignRows.key(a), SignRows.key(a)) + XCTAssertNotEqual(SignRows.key(a), SignRows.key([PaymentRow(addr: "a", amount: "1"), PaymentRow(addr: "b", amount: "3")])) + XCTAssertNotEqual(SignRows.key(a), SignRows.key(Array(a.dropLast()))) + // Moving a character from one field to the next is an edit too. + XCTAssertNotEqual(SignRows.key([PaymentRow(addr: "ab", amount: "1")]), SignRows.key([PaymentRow(addr: "a", amount: "b1")])) + } + + // MARK: stale tracking + + func testEditingAnyRowDisarmsSignUntilACheckOfTheCurrentRowsLands() { + var g = PaymentsGate() + XCTAssertFalse(g.stale) + g.rows[0] = PaymentRow(addr: "rand1a", amount: "5") + g.edited() + XCTAssertTrue(g.stale) + // Check payments: the entries adopted, the snapshot taken, the result lands with no edit between. + g.adopt(entries: ["rand1a:5"]) + let sent = g.sentKey + g.landed(sentKey: sent) + XCTAssertFalse(g.stale) + XCTAssertEqual(g.payees, ["rand1a:5"]) + } + + func testARowEditedWhileTheCheckIsInFlightStaysStale() { + var g = PaymentsGate() + g.rows[0] = PaymentRow(addr: "rand1a", amount: "5"); g.edited() + g.adopt(entries: ["rand1a:5"]) + let sent = g.sentKey + g.rows[0].amount = "6"; g.edited() // typed while the request was out + g.landed(sentKey: sent) + XCTAssertTrue(g.stale) + } + + func testTheTopCheckAndRetryCannotReArmSignAfterEdits() { + var g = PaymentsGate() + g.rows[0] = PaymentRow(addr: "rand1a", amount: "5"); g.edited() + g.adopt(entries: ["rand1a:5"]); g.landed(sentKey: g.sentKey) + XCTAssertFalse(g.stale) + g.rows[0].amount = "50"; g.edited() + // Retry / the top Check resend the last adopted payments, whose snapshot predates the edit. + g.landed(sentKey: g.sentKey) + XCTAssertTrue(g.stale) + XCTAssertEqual(g.payees, ["rand1a:5"]) + // Add and Remove are edits as well. + g.adopt(entries: ["rand1a:50"]); g.landed(sentKey: g.sentKey) + XCTAssertFalse(g.stale) + g.rows.append(PaymentRow()); g.edited() + g.landed(sentKey: g.sentKey) + XCTAssertTrue(g.stale) + } + + func testANewProposalStartsOverWithOneEmptyRow() { + var g = PaymentsGate() + g.rows = [PaymentRow(addr: "a", amount: "1"), PaymentRow(addr: "b", amount: "2")]; g.edited() + g.adopt(entries: ["a:1", "b:2"]) + g.reset() + XCTAssertEqual(g.rows, [PaymentRow()]) + XCTAssertEqual(g.payees, []) + XCTAssertFalse(g.stale) + } + + // MARK: the model: lines, confirmation, refusals + + func testTheModelShowsTheCoreLinesVerbatimOnTheRealVector() throws { + let policy = try fx("policy") + let proposalText = try fx("proposal-valid") + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(try fx("proposal-valid").utf8)) as? [String: Any]) + let line = try XCTUnwrap((json["summary"] as? [String])?.first) + let payee = try XCTUnwrap(line.split(separator: " ").last.map(String.init)) + let s = try RandCore.msauthCheck(proposal: proposalText, policy: policy, expectedRecipients: [payee + ":10"]) + let m = SignProposalModel(summary: s, verdict: .signable(blocksLeft: 100)) + XCTAssertEqual(m.lines, s.lines) + XCTAssertFalse(m.lines.isEmpty) + XCTAssertTrue(m.showsSummary) + XCTAssertEqual(m.payingTotal, 1) + XCTAssertEqual(m.payingMatched, 1) + XCTAssertNil(m.refusal) + } + + func testSignNeedsTheLastFourOfThePolicyFingerprint() { + let s = summary([rand5]) + let m = SignProposalModel(summary: s, verdict: .signable(blocksLeft: 100)) + XCTAssertFalse(m.canSign(typed: "", stale: false)) + XCTAssertFalse(m.canSign(typed: "e5k9", stale: false)) + XCTAssertFalse(m.canSign(typed: "aak", stale: false)) + XCTAssertTrue(m.canSign(typed: "AAK3", stale: false)) + XCTAssertTrue(m.canSign(typed: " aak3 ", stale: false)) + XCTAssertTrue(m.canSign(typed: "e5k9-aak3", stale: false) == false, "more than the last four is not the last four") + XCTAssertTrue(m.canSign(typed: "a-a-k-3", stale: false), "dashes are ignored") + } + + func testStaleDisarmsSignEvenWhenConfirmed() { + let m = SignProposalModel(summary: summary([rand5]), verdict: .signable(blocksLeft: 100)) + XCTAssertTrue(m.canSign(typed: "aak3", stale: false)) + XCTAssertFalse(m.canSign(typed: "aak3", stale: true)) + } + + func testARefusedVerdictNeverSigns() { + let s = summary([rand5]) + for v: MsauthVerdict in [.malformedSummary, .uncheckedRecipient, .noNode(url: "u"), .wrongChain(proposal: 1, wallet: 2), .wrongGenesis, .expired(head: 5, expiresAt: 3)] { + XCTAssertFalse(SignProposalModel(summary: s, verdict: v).canSign(typed: "aak3", stale: false), "\(v)") + XCTAssertNotNil(SignProposalModel(summary: s, verdict: v).refusal, "\(v)") + } + XCTAssertFalse(SignProposalModel(summary: nil, verdict: .signable(blocksLeft: 1)).canSign(typed: "aak3", stale: false)) + } + + func testEveryRefusalIsShownByItsMessage() { + let s = summary([rand5]) + let cases: [(MsauthVerdict, String)] = [ + (.malformedSummary, "The proposal check returned an incomplete answer, so this proposal is not signed."), + (.uncheckedRecipient, "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed."), + (.noNode(url: "http://node"), "No node answered at http://node, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable."), + (.wrongChain(proposal: 7, wallet: 13), "This proposal is for chain 7; this wallet is on chain 13."), + (.wrongGenesis, "This proposal is bound to another genesis than the one this wallet's node serves."), + (.expired(head: 300, expiresAt: 200), "This proposal expires at height 200 and the chain is at 300: too late to sign. Ask for a new proposal."), + ] + for (v, text) in cases { XCTAssertEqual(SignProposalModel(summary: s, verdict: v).refusal, text) } + XCTAssertTrue(SignProposalModel(summary: s, verdict: .noNode(url: "u")).isRetryable) + XCTAssertFalse(SignProposalModel(summary: s, verdict: .wrongGenesis).isRetryable) + } + + func testTheRefusalsComeFromTheRulesOnARealSummary() { + let s = summary([rand5]) + XCTAssertEqual(SignProposalModel(summary: s, verdict: verdict(s, head: nil)).refusal?.hasPrefix("No node answered at http://node"), true) + XCTAssertEqual(SignProposalModel(summary: s, verdict: verdict(s, head: 300)).refusal?.contains("too late to sign"), true) + XCTAssertEqual(SignProposalModel(summary: s, verdict: verdict(s, recipients: [])).refusal?.hasPrefix("Enter the address of every payee"), true) + XCTAssertEqual(SignProposalModel(summary: s, verdict: verdict(s, head: 1)).blocksLeft, 199) + } + + func testAMalformedSummaryShowsTheRefusalAndNoLines() { + for json: Any? in [nil, NSNull(), "text", [1], ["lines": ["x"]], ["outputs": []], ["lines": "x", "outputs": []]] { + let s = MsauthSummary(json: json) + let m = SignProposalModel(summary: s, verdict: verdict(s)) + XCTAssertFalse(m.verdict.isSignable) // malformed, or unchecked first when recipients_checked is absent + XCTAssertFalse(m.showsSummary) + XCTAssertNotNil(m.refusal) + XCTAssertFalse(m.canSign(typed: "aak3", stale: false)) + } + // A summary refused before the core was asked has none at all. + XCTAssertFalse(SignProposalModel(summary: nil, verdict: .uncheckedRecipient).showsSummary) + XCTAssertEqual(SignProposalModel(summary: nil, verdict: .uncheckedRecipient).lines, []) + } + + func testOnlyPayingOutputsAreCountedAsPayments() { + let change: [String: Any] = ["to_account": true, "amount": "9", "asset": 0] + let zero: [String: Any] = ["to_account": false, "amount": "0", "asset": 0] + let unmatched: [String: Any] = ["to_account": false, "amount": "3", "asset": 0, "matches": ""] + let m = SignProposalModel(summary: summary([rand5, change, zero, unmatched]), verdict: .uncheckedRecipient) + XCTAssertEqual(m.payingTotal, 2) + XCTAssertEqual(m.payingMatched, 1) + } + + // MARK: CheckSession (fix round 1) + + private func signable(_ outs: [[String: Any]]? = nil) -> MsauthCheck { + let s = summary(outs ?? [rand5]) + return MsauthCheck(summary: s, verdict: .signable(blocksLeft: 100)) + } + + func testSignIsDisabledOnceTheEditorTextDiffersFromTheCheckedProposalAndSignsTheCheckedText() { + var c = CheckSession() + let t = c.begin(proposal: "A") + c.land(t, signable()) + c.typed = "aak3" + XCTAssertTrue(c.canSign(editor: "A")) + XCTAssertEqual(c.reviewedProposal, "A", "what Sign signs is the checked text") + // Paste B over A: the review is dropped, and Sign is off even though A's lines were shown. + c.proposalEdited(to: "B") + XCTAssertFalse(c.canSign(editor: "B")) + XCTAssertNil(c.model) + XCTAssertNil(c.reviewedProposal) + XCTAssertEqual(c.typed, "") + // Even a model that survived would not arm Sign for a different editor text. + var d = CheckSession() + d.land(d.begin(proposal: "A"), signable()); d.typed = "aak3" + XCTAssertFalse(d.canSign(editor: "B")) + } + + func testARefusedCheckKeepsThePaymentRowsEditable() { + var c = CheckSession() + c.land(c.begin(proposal: "A"), signable([rand5, token])) + XCTAssertEqual(c.lastSummary?.outputs.count, 2) + c.gate.rows = [PaymentRow(addr: "typo", amount: "5")]; c.gate.edited() + let t = c.begin(proposal: "A") + XCTAssertEqual(c.gate.rows, [PaymentRow(addr: "typo", amount: "5")], "rows kept for the same proposal") + c.fail(t, message: "expected recipient typo: bad address") + XCTAssertNil(c.model) + XCTAssertEqual(c.error, "expected recipient typo: bad address") + XCTAssertEqual(c.lastSummary?.outputs.count, 2, "the last good summary still draws the rows") + // A new proposal text starts over. + _ = c.begin(proposal: "B") + XCTAssertNil(c.lastSummary) + XCTAssertEqual(c.gate.rows, [PaymentRow()]) + } + + func testAnOlderResultLandingLastIsIgnored() { + var c = CheckSession() + let old = c.begin(proposal: "A") + XCTAssertTrue(c.inFlight) + c.proposalEdited(to: "B") // the text moved on: the old check is superseded + XCTAssertFalse(c.inFlight) + let new = c.begin(proposal: "B") + XCTAssertTrue(c.land(new, signable())) + XCTAssertFalse(c.land(old, MsauthCheck(summary: summary([rand5]), verdict: .wrongGenesis)), "older result ignored") + XCTAssertFalse(c.fail(old, message: "x")) + XCTAssertEqual(c.model?.verdict, .signable(blocksLeft: 100)) + XCTAssertEqual(c.reviewedProposal, "B") + // A result landing while a later request is out does not clear inFlight. + let a = c.begin(proposal: "B"); let b = c.begin(proposal: "B") + XCTAssertFalse(c.land(a, signable())) + XCTAssertTrue(c.inFlight) + XCTAssertTrue(c.land(b, signable())) + XCTAssertFalse(c.inFlight) + } + + func testANewCheckClearsTheTypedConfirmation() { + var c = CheckSession() + c.land(c.begin(proposal: "A"), signable()); c.typed = "aak3" + c.land(c.begin(proposal: "A"), signable()) + XCTAssertEqual(c.typed, "") + } + + func testRowsKeepIdentityAndCompareByContent() { + let a = PaymentRow(addr: "x", amount: "1"), b = PaymentRow(addr: "x", amount: "1") + XCTAssertNotEqual(a.id, b.id) + XCTAssertEqual(a, b) + } + + func testAStalePaymentEditThroughTheSessionDisarmsSign() { + var c = CheckSession() + c.land(c.begin(proposal: "A"), signable()) // the first check of this proposal + c.gate.rows[0] = PaymentRow(addr: "a", amount: "5"); c.gate.edited() + XCTAssertTrue(c.gate.stale) + c.gate.adopt(entries: ["a:5"]) // Check payments + c.land(c.begin(proposal: "A"), signable()); c.typed = "aak3" + XCTAssertEqual(c.gate.payees, ["a:5"], "the rows survived the second begin") + XCTAssertFalse(c.gate.stale) + XCTAssertTrue(c.canSign(editor: "A")) + c.gate.rows[0].amount = "6"; c.gate.edited() + XCTAssertFalse(c.canSign(editor: "A")) + // Retry (the top Check) resends the old payments and cannot re-arm it. + c.land(c.begin(proposal: "A"), signable()); c.typed = "aak3" + XCTAssertFalse(c.canSign(editor: "A")) + } + + func testEditingToAnotherProposalDropsThePaymentsAndTheOldError() { + var c = CheckSession() + c.land(c.begin(proposal: "A"), signable([rand5, token])) + c.gate.rows[0] = PaymentRow(addr: "a", amount: "5"); c.gate.edited(); c.gate.adopt(entries: ["a:5"]) + c.fail(c.begin(proposal: "A"), message: "bad payee") + XCTAssertEqual(c.error, "bad payee") + c.proposalEdited(to: "A!") + XCTAssertNil(c.error) + XCTAssertNil(c.lastSummary) + XCTAssertEqual(c.gate.rows, [PaymentRow()]) + XCTAssertEqual(c.gate.payees, []) + // Typing back the checked text keeps the payments (the text is the one they were asked for). + var d = CheckSession() + d.land(d.begin(proposal: "A"), signable([rand5, token])) + d.proposalEdited(to: "A") + XCTAssertNotNil(d.lastSummary) + } + + func testAnOldCheckLandingAfterTheScreenWasLeftIsIgnored() { + var c = CheckSession() + let old = c.begin(proposal: "A") + c.close() + let fresh = c.begin(proposal: "A") + XCTAssertNotEqual(old.id, fresh.id, "the counter did not restart") + XCTAssertFalse(c.land(old, signable())) + XCTAssertNil(c.model) + XCTAssertTrue(c.land(fresh, signable())) + // Even straight after close, with no new request, the old ticket is dead. + var d = CheckSession() + let t = d.begin(proposal: "A"); d.close() + XCTAssertFalse(d.land(t, signable())) + XCTAssertFalse(d.fail(t, message: "x")) + } +} diff --git a/ios/l10n/ar.json b/ios/l10n/ar.json index ea76008..d61459c 100644 --- a/ios/l10n/ar.json +++ b/ios/l10n/ar.json @@ -446,7 +446,99 @@ "unknown parameter %@": "معامل غير معروف %@", "wallet is locked": "المحفظة مقفلة", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "تجزئة المعاملة ليست ارتباط الاقتراح", + "the proposal's signing_message is not its binding's": "قيمة signing_message في الاقتراح ليست قيمة ارتباطه", + "the proposal's auth_commit is not this policy's account": "قيمة auth_commit في الاقتراح ليست حساب هذه السياسة", + "the proposal's expected digest is not its bundle's": "الملخّص (digest) المتوقع للاقتراح ليس ملخّص حزمته", + "the transaction carries no bundle": "لا تحمل المعاملة أي حزمة", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "تحمل المعاملة إجراءً؛ والاقتراح الخاص متعدد التوقيع هو دفعة من حزمته وحدها", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "لا يمكن التحقق من قيمة الشاهد (witness): قيمة nk أو الملح فيه ليست لهذا الاقتراح", + "this key is not one of the policy's signers": "هذا المفتاح ليس من موقّعي السياسة", + "a share is index:hex": "حصة التوقيع بصيغة index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "مفتاح الموقّع هو ملف keygen من rand-node أو 64 حرفًا سداسيًا عشريًا", + "a signer seed is 64 hex characters": "بذرة الموقّع هي 64 حرفًا سداسيًا عشريًا", + "expected_recipients must be a list of addresses": "يجب أن يكون expected_recipients قائمة عناوين", + "the key file's public_key is not the one its seed derives": "قيمة public_key في ملف المفتاح ليست التي تشتقها بذرته", + "the key file's address is not the one its seed derives": "عنوان ملف المفتاح ليس العنوان الذي تشتقه بذرته", + "no expected recipient given: the payee was NOT checked": "لم يُحدَّد مستلم متوقع: لم يُتحقق من المستلم (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "هذا الاقتراح لسياسة أخرى: يذكر %@، وهذه السياسة هي %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "المخرج %@ لا يُفتح على أنه يخص هذا الحساب: رفض توقيع قيمة لا يمكن رؤيتها", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "قيمة الشاهد (witness) لا تطابق المعاملة: المدخل %@ لا يُبطَل إلى nullifier الحزمة", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "قيمة الشاهد (witness) لا تطابق المخرجات: للأصل %@ مدخل %@ ومخرج %@ مع الرسوم والحرق", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "ينتهي الاقتراح عند %@، خارج نافذة حزمته (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "المعاملة للسلسلة %@، بينما يذكر الاقتراح السلسلة %@", + "signer %@'s signature does not verify over this proposal": "توقيع الموقّع %@ غير صالح لهذا الاقتراح", + "signer %@ is not in this policy (it has %@ signers)": "الموقّع %@ ليس في هذه السياسة (فيها %@ موقّعين)", + "expected recipient %@: %@": "المستلم المتوقع %@: %@", + "the policy does not parse: %@": "تعذّر تحليل السياسة: %@", + "the proposal does not parse: %@": "تعذّر تحليل الاقتراح: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "أعاد التحقق من الاقتراح ردًّا ناقصًا، لذا لن يُوقَّع هذا الاقتراح.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "أدخل عنوان كل مستلم: لا يمكن توقيع مخرج لا يطابق العنوان الذي أدخلته أو عنوانه فارغ.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "لم تردّ أي عقدة على %@، لذا لا يمكن التحقق من انتهاء الصلاحية ولن يُوقَّع هذا الاقتراح. أعد المحاولة عندما تصبح العقدة متاحة.", + "This proposal is for chain %@; this wallet is on chain %@.": "هذا الاقتراح للسلسلة %@؛ وهذه المحفظة على السلسلة %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "هذا الاقتراح مرتبط بنشأة (genesis) مختلفة عن التي تخدمها عقدة هذه المحفظة.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "ينتهي هذا الاقتراح عند الارتفاع %@ والسلسلة عند %@: فات أوان التوقيع. اطلب اقتراحًا جديدًا.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "تعذّرت إزالة مفاتيح الموقّعين، لذا لم تُنسَ المحفظة. حاول مرة أخرى.", + "Manage multisig signers": "إدارة موقّعي التوقيع المتعدد", + "Multisig signers": "موقّعو التوقيع المتعدد", + "OK": "حسنًا", + "Enter the address of every payment.": "أدخل عنوان كل دفعة.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "أدخل مبلغ كل دفعة: فهو ما يمنع التجزئة أو تضخيم الدفعة.", + "An amount is a number of RAND, such as 1.5.": "المبلغ هو عدد من RAND، مثل 1.5.", + "Sign a proposal": "توقيع اقتراح", + "Proposal (proposal.json)": "الاقتراح (proposal.json)", + "Check a share": "التحقق من حصة توقيع", + "%@ of %@ · signer %@": "%@ من %@ · الموقّع %@", + "Open file": "فتح ملف", + "Checking…": "جارٍ التحقق…", + "Check proposal": "التحقق من الاقتراح", + "What this proposal does, as checked against your policy": "ما يفعله هذا الاقتراح، مُتحقَّقًا منه مقابل سياستك", + "Your share — send it to whoever proves the proposal": "حصة توقيعك — أرسلها إلى من يُثبت الاقتراح", + "index:hex": "index:hex", + "Check share": "التحقق من حصة التوقيع", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "حصة توقيع الموقّع %@ صحيحة. لديك %@ من %@: الموقّعون %@.", + "That file is larger than a proposal, a policy or a key file can be.": "هذا الملف أكبر مما يمكن أن يكون عليه اقتراح أو سياسة أو ملف مفتاح.", + "Add payment": "إضافة دفعة", + "Check payments": "التحقق من المدفوعات", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "لا يمكن التحقق من المبالغ في المدفوعات بغير RAND: اترك المبلغ فارغًا لها.", + "Payee address": "عنوان المستلم", + "Remove": "إزالة", + "Amount in RAND": "المبلغ بعملة RAND", + "Intended payments": "المدفوعات المقصودة", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "أدخل كل دفعة تنوي إجراءها: العنوان والمبلغ بعملة RAND. الدفعة إلى العنوان نفسه مرتين تُدخل مرة واحدة بالمجموع. لا يمكن توقيع مخرج لا يطابق أيًّا منها.", + "Fee: %@ RAND · expires at height %@": "الرسوم: %@ RAND · تنتهي عند الارتفاع %@", + "%@ of %@ payments matched.": "تطابقت %@ من أصل %@ من المدفوعات.", + "Retry": "إعادة المحاولة", + "%@ blocks left to prove it.": "بقي %@ كتلة لإثباته.", + "Payments changed: check them again before signing.": "تغيّرت المدفوعات: تحقق منها مجددًا قبل التوقيع.", + "Signing…": "جارٍ التوقيع…", + "Sign": "توقيع", + "Type the last four characters of the account fingerprint": "اكتب آخر أربعة أحرف من بصمة الحساب", + "Save as file": "حفظ كملف", + "Sign a multisig proposal": "توقيع اقتراح متعدد التوقيع", + "this signer is already saved as %@": "هذا الموقّع محفوظ مسبقًا باسم %@", + "a signer label is 1-64 characters": "تسمية الموقّع من 1-64 حرفًا", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "عيّن رمز دخول للجهاز أولًا: لا يُقرأ مفتاح الموقّع إلا بعد Face ID أو Touch ID أو رمز الدخول.", + "No such signer profile.": "لا يوجد ملف موقّع كهذا.", + "This signer profile has no key on this device: remove it and add it again.": "ملف الموقّع هذا لا مفتاح له على هذا الجهاز: أزله وأضفه من جديد.", + "No signer profiles yet": "لا توجد ملفات موقّعين بعد", + "Add the policy file and your signer key to sign for a private multisig account.": "أضف ملف السياسة ومفتاح الموقّع الخاص بك للتوقيع عن حساب متعدد التوقيع خاص.", + "Remove this signer": "إزالة هذا الموقّع", + "Remove a multisig signer": "إزالة موقّع متعدد التوقيع", + "Add a signer": "إضافة موقّع", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "يُحفظ ملف الموقّع في سلسلة مفاتيح (Keychain) هذا الهاتف. يطلب كل توقيع Face ID أو Touch ID أو رمز الدخول من جديد.", + "Policy file": "ملف السياسة", + "Signer key file, or its 64-hex seed": "ملف مفتاح الموقّع، أو بذرته من 64 حرفًا سداسيًا عشريًا", + "Account fingerprint — compare it with every other signer before you save": "بصمة الحساب — قارنها مع كل موقّع آخر قبل أن تحفظ", + "Label": "التسمية", + "Check": "تحقق", + "%@ of %@ · this key is signer %@": "%@ من %@ · هذا المفتاح هو الموقّع %@", + "Save signer": "حفظ الموقّع", + "That file could not be read.": "تعذّرت قراءة هذا الملف.", + "Remove %@?": "إزالة %@؟", + "You need the signer's key file to add this signer back.": "تحتاج إلى ملف مفتاح الموقّع لإضافته مرة أخرى.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "يتلقى هذا المستفيد RAND ورمزًا معًا: مدفوعات RAND والرموز إلى مستفيد واحد تحتاج إلى مقترحات منفصلة." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/de.json b/ios/l10n/de.json index a423356..2a7d8d3 100644 --- a/ios/l10n/de.json +++ b/ios/l10n/de.json @@ -434,7 +434,99 @@ "unknown parameter %@": "unbekannter Parameter %@", "wallet is locked": "die Wallet ist gesperrt", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "der Hash der Transaktion ist nicht die Bindung des Vorschlags", + "the proposal's signing_message is not its binding's": "die signing_message des Vorschlags ist nicht die seiner Bindung", + "the proposal's auth_commit is not this policy's account": "das auth_commit des Vorschlags ist nicht das Konto dieser Richtlinie", + "the proposal's expected digest is not its bundle's": "der erwartete Digest des Vorschlags ist nicht der seines Bundles", + "the transaction carries no bundle": "die Transaktion enthält kein Bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "die Transaktion enthält eine Aktion; ein privater Multisig-Vorschlag ist allein eine Zahlung aus seinem Bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "der Wert des Witness lässt sich nicht prüfen: sein nk oder Salt gehört nicht zu diesem Vorschlag", + "this key is not one of the policy's signers": "dieser Schlüssel gehört nicht zu den Unterzeichnern der Richtlinie", + "a share is index:hex": "ein Signaturanteil ist index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "ein Unterzeichner-Schlüssel ist eine rand-node-Keygen-Datei oder 64 Hex-Zeichen", + "a signer seed is 64 hex characters": "ein Unterzeichner-Seed besteht aus 64 Hex-Zeichen", + "expected_recipients must be a list of addresses": "expected_recipients muss eine Liste von Adressen sein", + "the key file's public_key is not the one its seed derives": "der public_key der Schlüsseldatei ist nicht der, den ihr Seed ableitet", + "the key file's address is not the one its seed derives": "die Adresse der Schlüsseldatei ist nicht die, die ihr Seed ableitet", + "no expected recipient given: the payee was NOT checked": "kein erwarteter Empfänger angegeben: der Zahlungsempfänger wurde NICHT geprüft", + "this proposal is for another policy: it names %@, this policy is %@": "dieser Vorschlag gehört zu einer anderen Richtlinie: er nennt %@, diese Richtlinie ist %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "Ausgabe %@ lässt sich nicht als die dieses Kontos öffnen: Signatur eines Werts, den sie nicht sehen kann, wird verweigert", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "der Wert des Witness passt nicht zur Transaktion: Eingang %@ ergibt nicht den Nullifier des Bundles", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "der Wert des Witness passt nicht zu den Ausgaben: Asset %@ hat %@ Eingang und %@ Ausgang mit Gebühr und Burns", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "der Vorschlag läuft bei %@ ab, außerhalb des Fensters seines Bundles (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "die Transaktion gilt für Chain %@, der Vorschlag nennt Chain %@", + "signer %@'s signature does not verify over this proposal": "die Signatur von Unterzeichner %@ ist für diesen Vorschlag nicht gültig", + "signer %@ is not in this policy (it has %@ signers)": "Unterzeichner %@ gehört nicht zu dieser Richtlinie (sie hat %@ Unterzeichner)", + "expected recipient %@: %@": "erwarteter Empfänger %@: %@", + "the policy does not parse: %@": "die Richtlinie lässt sich nicht parsen: %@", + "the proposal does not parse: %@": "der Vorschlag lässt sich nicht parsen: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Die Vorschlagsprüfung hat eine unvollständige Antwort geliefert, daher wird dieser Vorschlag nicht signiert.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Gib die Adresse jedes Zahlungsempfängers ein: Eine Ausgabe, die nicht zur eingegebenen Adresse passt oder deren Adresse leer ist, kann nicht signiert werden.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Unter %@ hat kein Knoten geantwortet, daher lässt sich der Ablauf nicht prüfen und dieser Vorschlag wird nicht signiert. Versuche es erneut, wenn der Knoten erreichbar ist.", + "This proposal is for chain %@; this wallet is on chain %@.": "Dieser Vorschlag gilt für Chain %@; diese Wallet ist auf Chain %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Dieser Vorschlag ist an ein anderes Genesis gebunden als das, das der Knoten dieser Wallet bedient.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Dieser Vorschlag läuft bei Höhe %@ ab und die Chain steht bei %@: zu spät zum Signieren. Fordere einen neuen Vorschlag an.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Die Unterzeichner-Schlüssel konnten nicht entfernt werden, daher wurde die Wallet nicht vergessen. Versuche es erneut.", + "Manage multisig signers": "Multisig-Unterzeichner verwalten", + "Multisig signers": "Multisig-Unterzeichner", + "OK": "OK", + "Enter the address of every payment.": "Gib die Adresse jeder Zahlung ein.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Gib den Betrag jeder Zahlung ein: Er verhindert eine Aufteilung oder eine überhöhte Zahlung.", + "An amount is a number of RAND, such as 1.5.": "Ein Betrag ist eine Anzahl RAND, zum Beispiel 1.5.", + "Sign a proposal": "Einen Vorschlag signieren", + "Proposal (proposal.json)": "Vorschlag (proposal.json)", + "Check a share": "Einen Signaturanteil prüfen", + "%@ of %@ · signer %@": "%@ von %@ · Unterzeichner %@", + "Open file": "Datei öffnen", + "Checking…": "Prüfe …", + "Check proposal": "Vorschlag prüfen", + "What this proposal does, as checked against your policy": "Was dieser Vorschlag tut, geprüft gegen deine Richtlinie", + "Your share — send it to whoever proves the proposal": "Dein Signaturanteil — schicke ihn an die Person, die den Vorschlag beweist", + "index:hex": "index:hex", + "Check share": "Signaturanteil prüfen", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "Der Signaturanteil von Unterzeichner %@ ist gültig. %@ von %@ liegen vor: Unterzeichner %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Diese Datei ist größer, als ein Vorschlag, eine Richtlinie oder eine Schlüsseldatei sein kann.", + "Add payment": "Zahlung hinzufügen", + "Check payments": "Zahlungen prüfen", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Beträge können bei Zahlungen, die nicht in RAND erfolgen, nicht geprüft werden: Lass den Betrag dort leer.", + "Payee address": "Adresse des Zahlungsempfängers", + "Remove": "Entfernen", + "Amount in RAND": "Betrag in RAND", + "Intended payments": "Vorgesehene Zahlungen", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Gib jede Zahlung ein, die du leisten willst: die Adresse und den Betrag in RAND. Eine Zahlung, die zweimal an dieselbe Adresse geht, wird einmal mit der Summe eingetragen. Eine Ausgabe, die zu keiner davon passt, kann nicht signiert werden.", + "Fee: %@ RAND · expires at height %@": "Gebühr: %@ RAND · läuft bei Höhe %@ ab", + "%@ of %@ payments matched.": "%@ von %@ Zahlungen stimmen überein.", + "Retry": "Erneut versuchen", + "%@ blocks left to prove it.": "Noch %@ Blöcke, um es zu beweisen.", + "Payments changed: check them again before signing.": "Zahlungen geändert: Prüfe sie vor dem Signieren erneut.", + "Signing…": "Signiere …", + "Sign": "Signieren", + "Type the last four characters of the account fingerprint": "Gib die letzten vier Zeichen des Konto-Fingerabdrucks ein", + "Save as file": "Als Datei speichern", + "Sign a multisig proposal": "Einen Multisig-Vorschlag signieren", + "this signer is already saved as %@": "dieser Unterzeichner ist bereits als %@ gespeichert", + "a signer label is 1-64 characters": "eine Unterzeichner-Bezeichnung hat 1-64 Zeichen", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Lege zuerst einen Geräte-Code fest: Ein Unterzeichner-Schlüssel wird erst nach Face ID, Touch ID oder dem Code gelesen.", + "No such signer profile.": "Dieses Unterzeichnerprofil gibt es nicht.", + "This signer profile has no key on this device: remove it and add it again.": "Dieses Unterzeichnerprofil hat auf diesem Gerät keinen Schlüssel: Entferne es und füge es erneut hinzu.", + "No signer profiles yet": "Noch keine Unterzeichnerprofile", + "Add the policy file and your signer key to sign for a private multisig account.": "Füge die Richtliniendatei und deinen Unterzeichner-Schlüssel hinzu, um für ein privates Multisig-Konto zu signieren.", + "Remove this signer": "Diesen Unterzeichner entfernen", + "Remove a multisig signer": "Einen Multisig-Unterzeichner entfernen", + "Add a signer": "Unterzeichner hinzufügen", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Ein Unterzeichnerprofil wird im Schlüsselbund (Keychain) dieses iPhones gespeichert. Jede Signatur verlangt erneut Face ID, Touch ID oder deinen Code.", + "Policy file": "Richtliniendatei", + "Signer key file, or its 64-hex seed": "Unterzeichner-Schlüsseldatei oder ihr Seed aus 64 Hex-Zeichen", + "Account fingerprint — compare it with every other signer before you save": "Konto-Fingerabdruck — vergleiche ihn mit jedem anderen Unterzeichner, bevor du speicherst", + "Label": "Bezeichnung", + "Check": "Prüfen", + "%@ of %@ · this key is signer %@": "%@ von %@ · dieser Schlüssel ist Unterzeichner %@", + "Save signer": "Unterzeichner speichern", + "That file could not be read.": "Diese Datei konnte nicht gelesen werden.", + "Remove %@?": "%@ entfernen?", + "You need the signer's key file to add this signer back.": "Du brauchst die Schlüsseldatei des Unterzeichners, um ihn wieder hinzuzufügen.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Dieser Empfänger erhält sowohl RAND als auch einen Token: RAND- und Token-Zahlungen an einen Empfänger brauchen getrennte Vorschläge." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/es.json b/ios/l10n/es.json index 8ea77fe..ee06a62 100644 --- a/ios/l10n/es.json +++ b/ios/l10n/es.json @@ -437,7 +437,99 @@ "unknown parameter %@": "parámetro desconocido %@", "wallet is locked": "la billetera está bloqueada", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "el hash de la transacción no es la vinculación de la propuesta", + "the proposal's signing_message is not its binding's": "el signing_message de la propuesta no es el de su vinculación", + "the proposal's auth_commit is not this policy's account": "el auth_commit de la propuesta no es la cuenta de esta política", + "the proposal's expected digest is not its bundle's": "el digest esperado de la propuesta no es el de su bundle", + "the transaction carries no bundle": "la transacción no lleva ningún bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transacción lleva una acción; una propuesta multifirma privada es un pago solo desde su bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "no se puede comprobar el valor del witness: su nk o su salt no son los de esta propuesta", + "this key is not one of the policy's signers": "esta clave no es de ninguno de los firmantes de la política", + "a share is index:hex": "una parte de firma es index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "una clave de firmante es un archivo keygen de rand-node o 64 caracteres hexadecimales", + "a signer seed is 64 hex characters": "una semilla de firmante son 64 caracteres hexadecimales", + "expected_recipients must be a list of addresses": "expected_recipients debe ser una lista de direcciones", + "the key file's public_key is not the one its seed derives": "el public_key del archivo de clave no es el que deriva su semilla", + "the key file's address is not the one its seed derives": "la dirección del archivo de clave no es la que deriva su semilla", + "no expected recipient given: the payee was NOT checked": "no se indicó ningún destinatario esperado: el beneficiario NO se comprobó", + "this proposal is for another policy: it names %@, this policy is %@": "esta propuesta es de otra política: nombra %@, esta política es %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "la salida %@ no se abre como de esta cuenta: se rechaza firmar un valor que no puede ver", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "el valor del witness no coincide con la transacción: la entrada %@ no genera el nullifier del bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "el valor del witness no coincide con las salidas: el activo %@ tiene %@ de entrada y %@ de salida con la comisión y las quemas", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "la propuesta caduca en %@, fuera de la ventana de su bundle (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "la transacción es para la cadena %@, la propuesta dice la cadena %@", + "signer %@'s signature does not verify over this proposal": "la firma del firmante %@ no es válida para esta propuesta", + "signer %@ is not in this policy (it has %@ signers)": "el firmante %@ no está en esta política (tiene %@ firmantes)", + "expected recipient %@: %@": "destinatario esperado %@: %@", + "the policy does not parse: %@": "la política no se puede interpretar: %@", + "the proposal does not parse: %@": "la propuesta no se puede interpretar: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La comprobación de la propuesta devolvió una respuesta incompleta, así que esta propuesta no se firma.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Introduce la dirección de cada beneficiario: una salida que no coincide con la dirección introducida, o cuya dirección está vacía, no se puede firmar.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Ningún nodo respondió en %@, así que no se puede comprobar la caducidad y esta propuesta no se firma. Reintenta cuando el nodo esté accesible.", + "This proposal is for chain %@; this wallet is on chain %@.": "Esta propuesta es para la cadena %@; esta billetera está en la cadena %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Esta propuesta está ligada a un génesis distinto del que sirve el nodo de esta billetera.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Esta propuesta caduca en la altura %@ y la cadena va por %@: es tarde para firmar. Pide una propuesta nueva.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "No se pudieron quitar las claves de firmante, así que no se olvidó la billetera. Inténtalo de nuevo.", + "Manage multisig signers": "Gestionar firmantes multifirma", + "Multisig signers": "Firmantes multifirma", + "OK": "OK", + "Enter the address of every payment.": "Introduce la dirección de cada pago.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Introduce el monto de cada pago: es lo que impide una división o un pago inflado.", + "An amount is a number of RAND, such as 1.5.": "Un monto es una cantidad de RAND, como 1.5.", + "Sign a proposal": "Firmar una propuesta", + "Proposal (proposal.json)": "Propuesta (proposal.json)", + "Check a share": "Comprobar una parte de firma", + "%@ of %@ · signer %@": "%@ de %@ · firmante %@", + "Open file": "Abrir archivo", + "Checking…": "Comprobando…", + "Check proposal": "Comprobar propuesta", + "What this proposal does, as checked against your policy": "Lo que hace esta propuesta, comprobado contra tu política", + "Your share — send it to whoever proves the proposal": "Tu parte de firma — envíasela a quien pruebe la propuesta", + "index:hex": "index:hex", + "Check share": "Comprobar parte de firma", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "La parte de firma del firmante %@ es válida. Tienes %@ de %@: firmantes %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Ese archivo es mayor de lo que puede ser una propuesta, una política o un archivo de clave.", + "Add payment": "Añadir pago", + "Check payments": "Comprobar pagos", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Los montos no se pueden comprobar en pagos que no son en RAND: deja el monto vacío en esos.", + "Payee address": "Dirección del beneficiario", + "Remove": "Eliminar", + "Amount in RAND": "Monto en RAND", + "Intended payments": "Pagos previstos", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Introduce cada pago que piensas hacer: la dirección y el monto en RAND. Un pago a la misma dirección dos veces se introduce una sola vez, con el total. Una salida que no coincide con ninguno no se puede firmar.", + "Fee: %@ RAND · expires at height %@": "Comisión: %@ RAND · caduca en la altura %@", + "%@ of %@ payments matched.": "%@ de %@ pagos coinciden.", + "Retry": "Reintentar", + "%@ blocks left to prove it.": "Quedan %@ bloques para probarlo.", + "Payments changed: check them again before signing.": "Los pagos cambiaron: compruébalos de nuevo antes de firmar.", + "Signing…": "Firmando…", + "Sign": "Firmar", + "Type the last four characters of the account fingerprint": "Escribe los últimos cuatro caracteres de la huella de la cuenta", + "Save as file": "Guardar como archivo", + "Sign a multisig proposal": "Firmar una propuesta multifirma", + "this signer is already saved as %@": "este firmante ya está guardado como %@", + "a signer label is 1-64 characters": "la etiqueta de un firmante tiene 1-64 caracteres", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Primero configura un código en el dispositivo: la clave de firmante solo se lee tras Face ID, Touch ID o el código.", + "No such signer profile.": "No existe ese perfil de firmante.", + "This signer profile has no key on this device: remove it and add it again.": "Este perfil de firmante no tiene clave en este dispositivo: quítalo y vuelve a añadirlo.", + "No signer profiles yet": "Aún no hay perfiles de firmante", + "Add the policy file and your signer key to sign for a private multisig account.": "Añade el archivo de política y tu clave de firmante para firmar por una cuenta multifirma privada.", + "Remove this signer": "Quitar este firmante", + "Remove a multisig signer": "Quitar un firmante multifirma", + "Add a signer": "Añadir un firmante", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Un perfil de firmante se guarda en el llavero (Keychain) de este teléfono. Cada firma vuelve a pedir Face ID, Touch ID o tu código.", + "Policy file": "Archivo de política", + "Signer key file, or its 64-hex seed": "Archivo de clave de firmante, o su semilla de 64 hex", + "Account fingerprint — compare it with every other signer before you save": "Huella de la cuenta — compárala con la de cada otro firmante antes de guardar", + "Label": "Etiqueta", + "Check": "Comprobar", + "%@ of %@ · this key is signer %@": "%@ de %@ · esta clave es el firmante %@", + "Save signer": "Guardar firmante", + "That file could not be read.": "No se pudo leer ese archivo.", + "Remove %@?": "¿Quitar %@?", + "You need the signer's key file to add this signer back.": "Necesitas el archivo de clave del firmante para volver a añadirlo.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Este beneficiario recibe RAND y también un token: los pagos en RAND y en tokens a un mismo beneficiario necesitan propuestas separadas." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/fa.json b/ios/l10n/fa.json index 002e040..a28b94e 100644 --- a/ios/l10n/fa.json +++ b/ios/l10n/fa.json @@ -425,7 +425,99 @@ "unknown parameter %@": "پارامتر ناشناخته %@", "wallet is locked": "کیف پول قفل است", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "هش تراکنش همان binding پیشنهاد نیست", + "the proposal's signing_message is not its binding's": "signing_message پیشنهاد همان signing_message مربوط به binding آن نیست", + "the proposal's auth_commit is not this policy's account": "auth_commit پیشنهاد حساب این سیاست نیست", + "the proposal's expected digest is not its bundle's": "digest مورد انتظار پیشنهاد همان digest مربوط به bundle آن نیست", + "the transaction carries no bundle": "تراکنش هیچ bundle ای ندارد", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "تراکنش یک اقدام دارد؛ پیشنهاد چندامضایی خصوصی فقط پرداختی از bundle خودش است", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "مقدار witness قابل بررسی نیست: nk یا salt آن مال این پیشنهاد نیست", + "this key is not one of the policy's signers": "این کلید یکی از امضاکنندگان سیاست نیست", + "a share is index:hex": "سهم امضا به شکل index:hex است", + "a signer key is a rand-node keygen file or 64 hex characters": "کلید امضاکننده یک فایل keygen از rand-node یا ۶۴ نویسه هگز است", + "a signer seed is 64 hex characters": "seed امضاکننده ۶۴ نویسه هگز است", + "expected_recipients must be a list of addresses": "expected_recipients باید فهرستی از آدرس‌ها باشد", + "the key file's public_key is not the one its seed derives": "public_key فایل کلید همان نیست که seed آن مشتق می‌کند", + "the key file's address is not the one its seed derives": "آدرس فایل کلید همان نیست که seed آن مشتق می‌کند", + "no expected recipient given: the payee was NOT checked": "هیچ دریافت‌کننده مورد انتظاری داده نشد: دریافت‌کننده بررسی نشد (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "این پیشنهاد برای سیاست دیگری است: %@ را نام می‌برد، این سیاست %@ است", + "output %@ does not open as this account's: refusing to sign value it cannot see": "خروجی %@ به‌عنوان خروجی این حساب باز نمی‌شود: امضای مقداری که دیده نمی‌شود رد می‌شود", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "مقدار witness با تراکنش نمی‌خواند: ورودی %@ به nullifier مربوط به bundle نمی‌رسد", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "مقدار witness با خروجی‌ها نمی‌خواند: دارایی %@ ورودی %@ و خروجی %@ با کارمزد و سوزاندن‌ها دارد", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "پیشنهاد در %@ منقضی می‌شود، بیرون از بازه bundle خود (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "تراکنش برای شبکه %@ است، پیشنهاد شبکه %@ را می‌گوید", + "signer %@'s signature does not verify over this proposal": "امضای امضاکننده %@ برای این پیشنهاد معتبر نیست", + "signer %@ is not in this policy (it has %@ signers)": "امضاکننده %@ در این سیاست نیست (این سیاست %@ امضاکننده دارد)", + "expected recipient %@: %@": "دریافت‌کننده مورد انتظار %@: %@", + "the policy does not parse: %@": "سیاست تجزیه نمی‌شود: %@", + "the proposal does not parse: %@": "پیشنهاد تجزیه نمی‌شود: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "بررسی پیشنهاد پاسخ ناقصی برگرداند، پس این پیشنهاد امضا نمی‌شود.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "آدرس هر دریافت‌کننده را وارد کنید: خروجی‌ای که با آدرس واردشده مطابقت ندارد یا آدرسش خالی است قابل امضا نیست.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "هیچ گره‌ای در %@ پاسخ نداد، پس انقضا قابل بررسی نیست و این پیشنهاد امضا نمی‌شود. وقتی گره در دسترس بود دوباره تلاش کنید.", + "This proposal is for chain %@; this wallet is on chain %@.": "این پیشنهاد برای شبکه %@ است؛ این کیف پول روی شبکه %@ است.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "این پیشنهاد به genesis دیگری بسته شده است، نه آنچه گره این کیف پول ارائه می‌دهد.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "این پیشنهاد در ارتفاع %@ منقضی می‌شود و شبکه در %@ است: برای امضا دیر شده است. پیشنهاد جدید بخواهید.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "کلیدهای امضاکننده حذف نشدند، پس کیف پول فراموش نشد. دوباره تلاش کنید.", + "Manage multisig signers": "مدیریت امضاکنندگان چندامضایی", + "Multisig signers": "امضاکنندگان چندامضایی", + "OK": "تأیید", + "Enter the address of every payment.": "آدرس هر پرداخت را وارد کنید.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "مقدار هر پرداخت را وارد کنید: همین است که جلوی تقسیم یا پرداخت باد‌شده را می‌گیرد.", + "An amount is a number of RAND, such as 1.5.": "مقدار یک عدد RAND است، مانند 1.5.", + "Sign a proposal": "امضای یک پیشنهاد", + "Proposal (proposal.json)": "پیشنهاد (proposal.json)", + "Check a share": "بررسی یک سهم امضا", + "%@ of %@ · signer %@": "%@ از %@ · امضاکننده %@", + "Open file": "باز کردن فایل", + "Checking…": "در حال بررسی…", + "Check proposal": "بررسی پیشنهاد", + "What this proposal does, as checked against your policy": "این پیشنهاد چه می‌کند، بررسی‌شده در برابر سیاست شما", + "Your share — send it to whoever proves the proposal": "سهم امضای شما — آن را برای کسی بفرستید که پیشنهاد را اثبات می‌کند", + "index:hex": "index:hex", + "Check share": "بررسی سهم امضا", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "سهم امضای امضاکننده %@ معتبر است. %@ از %@ در دست است: امضاکنندگان %@.", + "That file is larger than a proposal, a policy or a key file can be.": "آن فایل از اندازه‌ای که یک پیشنهاد، سیاست یا فایل کلید می‌تواند داشته باشد بزرگ‌تر است.", + "Add payment": "افزودن پرداخت", + "Check payments": "بررسی پرداخت‌ها", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "مقدار پرداخت‌های غیر RAND قابل بررسی نیست: مقدار را برای آن‌ها خالی بگذارید.", + "Payee address": "آدرس دریافت‌کننده", + "Remove": "حذف", + "Amount in RAND": "مقدار به RAND", + "Intended payments": "پرداخت‌های موردنظر", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "هر پرداختی را که قصد انجامش را دارید وارد کنید: آدرس و مقدار به RAND. دو پرداخت به یک آدرس یک‌بار و با مجموع وارد می‌شود. خروجی‌ای که با هیچ‌کدام مطابقت نداشته باشد قابل امضا نیست.", + "Fee: %@ RAND · expires at height %@": "کارمزد: %@ RAND · در ارتفاع %@ منقضی می‌شود", + "%@ of %@ payments matched.": "%@ از %@ پرداخت مطابقت دارد.", + "Retry": "تلاش دوباره", + "%@ blocks left to prove it.": "%@ بلوک مانده تا اثبات شود.", + "Payments changed: check them again before signing.": "پرداخت‌ها تغییر کردند: پیش از امضا دوباره بررسی‌شان کنید.", + "Signing…": "در حال امضا…", + "Sign": "امضا", + "Type the last four characters of the account fingerprint": "چهار نویسه آخر اثر انگشت حساب را بنویسید", + "Save as file": "ذخیره به‌صورت فایل", + "Sign a multisig proposal": "امضای یک پیشنهاد چندامضایی", + "this signer is already saved as %@": "این امضاکننده از قبل با نام %@ ذخیره شده است", + "a signer label is 1-64 characters": "برچسب امضاکننده ۱-۶۴ نویسه است", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "ابتدا برای دستگاه کد عبور تنظیم کنید: کلید امضاکننده فقط پس از Face ID، Touch ID یا کد عبور خوانده می‌شود.", + "No such signer profile.": "چنین نمایه امضاکننده‌ای وجود ندارد.", + "This signer profile has no key on this device: remove it and add it again.": "این نمایه امضاکننده روی این دستگاه کلیدی ندارد: آن را حذف کنید و دوباره اضافه کنید.", + "No signer profiles yet": "هنوز نمایه امضاکننده‌ای نیست", + "Add the policy file and your signer key to sign for a private multisig account.": "فایل سیاست و کلید امضاکننده خود را اضافه کنید تا برای یک حساب چندامضایی خصوصی امضا کنید.", + "Remove this signer": "حذف این امضاکننده", + "Remove a multisig signer": "حذف یک امضاکننده چندامضایی", + "Add a signer": "افزودن امضاکننده", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "نمایهٔ امضاکننده در Keychain این گوشی نگه داشته می‌شود. هر امضا دوباره Face ID، Touch ID یا کد عبور شما را می‌خواهد.", + "Policy file": "فایل سیاست", + "Signer key file, or its 64-hex seed": "فایل کلید امضاکننده، یا seed ۶۴ نویسه‌ای هگز آن", + "Account fingerprint — compare it with every other signer before you save": "اثر انگشت حساب — پیش از ذخیره، آن را با هر امضاکننده دیگر مقایسه کنید", + "Label": "برچسب", + "Check": "بررسی", + "%@ of %@ · this key is signer %@": "%@ از %@ · این کلید امضاکننده %@ است", + "Save signer": "ذخیره امضاکننده", + "That file could not be read.": "این فایل خوانده نشد.", + "Remove %@?": "%@ حذف شود؟", + "You need the signer's key file to add this signer back.": "برای افزودن دوبارهٔ این امضاکننده به فایل کلید او نیاز دارید.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "این گیرنده هم RAND و هم یک توکن دریافت می‌کند: پرداخت‌های RAND و توکن به یک گیرنده به پیشنهادهای جداگانه نیاز دارند." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/fr.json b/ios/l10n/fr.json index 6847a2f..80e2be3 100644 --- a/ios/l10n/fr.json +++ b/ios/l10n/fr.json @@ -449,7 +449,99 @@ "unknown parameter %@": "paramètre inconnu %@", "wallet is locked": "le portefeuille est verrouillé", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "le hash de la transaction n'est pas la liaison de la proposition", + "the proposal's signing_message is not its binding's": "le signing_message de la proposition n'est pas celui de sa liaison", + "the proposal's auth_commit is not this policy's account": "l'auth_commit de la proposition n'est pas le compte de cette politique", + "the proposal's expected digest is not its bundle's": "le digest attendu de la proposition n'est pas celui de son lot", + "the transaction carries no bundle": "la transaction ne porte aucun lot", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transaction porte une action ; une proposition multisig privée est un paiement depuis son seul lot", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "la valeur du witness ne peut pas être vérifiée : son nk ou son sel n'est pas celui de cette proposition", + "this key is not one of the policy's signers": "cette clé n'est pas l'un des signataires de la politique", + "a share is index:hex": "une part de signature est index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "une clé de signataire est un fichier keygen de rand-node ou 64 caractères hexadécimaux", + "a signer seed is 64 hex characters": "une graine de signataire compte 64 caractères hexadécimaux", + "expected_recipients must be a list of addresses": "expected_recipients doit être une liste d'adresses", + "the key file's public_key is not the one its seed derives": "le public_key du fichier de clé n'est pas celui que dérive sa graine", + "the key file's address is not the one its seed derives": "l'adresse du fichier de clé n'est pas celle que dérive sa graine", + "no expected recipient given: the payee was NOT checked": "aucun destinataire attendu n'a été indiqué : le bénéficiaire n'a PAS été vérifié", + "this proposal is for another policy: it names %@, this policy is %@": "cette proposition est pour une autre politique : elle nomme %@, cette politique est %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "la sortie %@ ne s'ouvre pas comme celle de ce compte : refus de signer une valeur qu'elle ne peut pas voir", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "la valeur du witness ne correspond pas à la transaction : l'entrée %@ n'annule pas vers le nullifier du lot", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "la valeur du witness ne correspond pas aux sorties : l'actif %@ a %@ en entrée et %@ en sortie avec les frais et les brûlages", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "la proposition expire à %@, hors de la fenêtre de son lot (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "la transaction est pour la chaîne %@, la proposition indique la chaîne %@", + "signer %@'s signature does not verify over this proposal": "la signature du signataire %@ n'est pas valide pour cette proposition", + "signer %@ is not in this policy (it has %@ signers)": "le signataire %@ n'est pas dans cette politique (elle compte %@ signataires)", + "expected recipient %@: %@": "destinataire attendu %@ : %@", + "the policy does not parse: %@": "la politique ne peut pas être analysée : %@", + "the proposal does not parse: %@": "la proposition ne peut pas être analysée : %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La vérification de la proposition a renvoyé une réponse incomplète, cette proposition n'est donc pas signée.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Saisissez l'adresse de chaque bénéficiaire : une sortie qui ne correspond pas à l'adresse saisie, ou dont l'adresse est vide, ne peut pas être signée.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Aucun nœud n'a répondu à %@, l'expiration ne peut donc pas être vérifiée et cette proposition n'est pas signée. Réessayez quand le nœud sera joignable.", + "This proposal is for chain %@; this wallet is on chain %@.": "Cette proposition est pour la chaîne %@ ; ce portefeuille est sur la chaîne %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Cette proposition est liée à un autre genesis que celui que sert le nœud de ce portefeuille.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Cette proposition expire à la hauteur %@ et la chaîne en est à %@ : trop tard pour signer. Demandez une nouvelle proposition.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Les clés de signataire n'ont pas pu être retirées, donc le portefeuille n'a pas été oublié. Réessayez.", + "Manage multisig signers": "Gérer les signataires multisig", + "Multisig signers": "Signataires multisig", + "OK": "OK", + "Enter the address of every payment.": "Saisissez l'adresse de chaque paiement.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Saisissez le montant de chaque paiement : c'est ce qui empêche un fractionnement ou un paiement gonflé.", + "An amount is a number of RAND, such as 1.5.": "Un montant est un nombre de RAND, comme 1.5.", + "Sign a proposal": "Signer une proposition", + "Proposal (proposal.json)": "Proposition (proposal.json)", + "Check a share": "Vérifier une part de signature", + "%@ of %@ · signer %@": "%@ sur %@ · signataire %@", + "Open file": "Ouvrir un fichier", + "Checking…": "Vérification…", + "Check proposal": "Vérifier la proposition", + "What this proposal does, as checked against your policy": "Ce que fait cette proposition, vérifié par rapport à votre politique", + "Your share — send it to whoever proves the proposal": "Votre part de signature — envoyez-la à qui prouve la proposition", + "index:hex": "index:hex", + "Check share": "Vérifier la part de signature", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "La part de signature du signataire %@ est valide. %@ sur %@ réunies : signataires %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Ce fichier est plus grand que ne peut l'être une proposition, une politique ou un fichier de clé.", + "Add payment": "Ajouter un paiement", + "Check payments": "Vérifier les paiements", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Les montants ne peuvent pas être vérifiés pour les paiements hors RAND : laissez le montant vide pour ceux-là.", + "Payee address": "Adresse du bénéficiaire", + "Remove": "Supprimer", + "Amount in RAND": "Montant en RAND", + "Intended payments": "Paiements prévus", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Saisissez chaque paiement que vous comptez faire : l'adresse et le montant en RAND. Un paiement à la même adresse deux fois est saisi une seule fois, avec le total. Une sortie qui ne correspond à aucun ne peut pas être signée.", + "Fee: %@ RAND · expires at height %@": "Frais : %@ RAND · expire à la hauteur %@", + "%@ of %@ payments matched.": "%@ paiements sur %@ correspondent.", + "Retry": "Réessayer", + "%@ blocks left to prove it.": "Il reste %@ blocs pour la prouver.", + "Payments changed: check them again before signing.": "Les paiements ont changé : vérifiez-les de nouveau avant de signer.", + "Signing…": "Signature…", + "Sign": "Signer", + "Type the last four characters of the account fingerprint": "Saisissez les quatre derniers caractères de l'empreinte du compte", + "Save as file": "Enregistrer comme fichier", + "Sign a multisig proposal": "Signer une proposition multisig", + "this signer is already saved as %@": "ce signataire est déjà enregistré sous le nom %@", + "a signer label is 1-64 characters": "une étiquette de signataire compte 1-64 caractères", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Définissez d'abord un code sur l'appareil : une clé de signataire n'est lue qu'après Face ID, Touch ID ou le code.", + "No such signer profile.": "Ce profil de signataire n'existe pas.", + "This signer profile has no key on this device: remove it and add it again.": "Ce profil de signataire n'a pas de clé sur cet appareil : retirez-le et ajoutez-le de nouveau.", + "No signer profiles yet": "Aucun profil de signataire pour l'instant", + "Add the policy file and your signer key to sign for a private multisig account.": "Ajoutez le fichier de politique et votre clé de signataire pour signer pour un compte multisig privé.", + "Remove this signer": "Retirer ce signataire", + "Remove a multisig signer": "Retirer un signataire multisig", + "Add a signer": "Ajouter un signataire", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Un profil de signataire est conservé dans le trousseau (Keychain) de ce téléphone. Chaque signature redemande Face ID, Touch ID ou votre code.", + "Policy file": "Fichier de politique", + "Signer key file, or its 64-hex seed": "Fichier de clé de signataire, ou sa graine de 64 hex", + "Account fingerprint — compare it with every other signer before you save": "Empreinte du compte — comparez-la avec celle de chaque autre signataire avant d'enregistrer", + "Label": "Étiquette", + "Check": "Vérifier", + "%@ of %@ · this key is signer %@": "%@ sur %@ · cette clé est le signataire %@", + "Save signer": "Enregistrer le signataire", + "That file could not be read.": "Ce fichier n'a pas pu être lu.", + "Remove %@?": "Retirer %@ ?", + "You need the signer's key file to add this signer back.": "Il faut le fichier de clé du signataire pour le rajouter.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Ce bénéficiaire reçoit à la fois des RAND et un jeton : les paiements en RAND et en jetons à un même bénéficiaire exigent des propositions séparées." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/hi.json b/ios/l10n/hi.json index d813295..8786c9b 100644 --- a/ios/l10n/hi.json +++ b/ios/l10n/hi.json @@ -434,11 +434,103 @@ "unknown parameter %@": "अज्ञात पैरामीटर %@", "wallet is locked": "वॉलेट लॉक है", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "लेन-देन का हैश प्रस्ताव की बाइंडिंग का नहीं है", + "the proposal's signing_message is not its binding's": "प्रस्ताव का signing_message उसकी बाइंडिंग का नहीं है", + "the proposal's auth_commit is not this policy's account": "प्रस्ताव का auth_commit इस पॉलिसी का खाता नहीं है", + "the proposal's expected digest is not its bundle's": "प्रस्ताव का अपेक्षित डाइजेस्ट उसके बंडल का नहीं है", + "the transaction carries no bundle": "लेन-देन में कोई बंडल नहीं है", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "लेन-देन में एक एक्शन है; निजी मल्टीसिग प्रस्ताव केवल अपने बंडल से किया गया भुगतान होता है", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "विटनेस का मान जाँचा नहीं जा सकता: उसका nk या सॉल्ट इस प्रस्ताव का नहीं है", + "this key is not one of the policy's signers": "यह की पॉलिसी के हस्ताक्षरकर्ताओं में से नहीं है", + "a share is index:hex": "हस्ताक्षर अंश index:hex रूप में होता है", + "a signer key is a rand-node keygen file or 64 hex characters": "हस्ताक्षरकर्ता की एक rand-node keygen फ़ाइल या 64 हेक्स अक्षर होती है", + "a signer seed is 64 hex characters": "हस्ताक्षरकर्ता का सीड 64 हेक्स अक्षरों का होता है", + "expected_recipients must be a list of addresses": "expected_recipients पतों की सूची होनी चाहिए", + "the key file's public_key is not the one its seed derives": "की फ़ाइल की public_key वह नहीं है जो उसका सीड व्युत्पन्न करता है", + "the key file's address is not the one its seed derives": "की फ़ाइल का पता वह नहीं है जो उसका सीड व्युत्पन्न करता है", + "no expected recipient given: the payee was NOT checked": "कोई अपेक्षित प्राप्तकर्ता नहीं दिया गया: प्राप्तकर्ता की जाँच नहीं हुई (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "यह प्रस्ताव किसी और पॉलिसी का है: इसमें %@ है, जबकि यह पॉलिसी %@ है", + "output %@ does not open as this account's: refusing to sign value it cannot see": "आउटपुट %@ इस खाते के रूप में नहीं खुलता: जो मान दिखता नहीं उस पर हस्ताक्षर से इनकार", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "विटनेस का मान लेन-देन से मेल नहीं खाता: इनपुट %@ बंडल के नलिफ़ायर तक नहीं पहुँचता", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "विटनेस का मान आउटपुट से मेल नहीं खाता: एसेट %@ में %@ इनपुट और शुल्क व बर्न सहित %@ आउटपुट है", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "प्रस्ताव %@ पर समाप्त होता है, जो उसके बंडल की सीमा (%@, %@] से बाहर है", + "the transaction is for chain %@, the proposal says chain %@": "लेन-देन चेन %@ के लिए है, प्रस्ताव चेन %@ बताता है", + "signer %@'s signature does not verify over this proposal": "हस्ताक्षरकर्ता %@ का हस्ताक्षर इस प्रस्ताव के लिए मान्य नहीं है", + "signer %@ is not in this policy (it has %@ signers)": "हस्ताक्षरकर्ता %@ इस पॉलिसी में नहीं है (इसमें %@ हस्ताक्षरकर्ता हैं)", + "expected recipient %@: %@": "अपेक्षित प्राप्तकर्ता %@: %@", + "the policy does not parse: %@": "पॉलिसी पार्स नहीं होती: %@", + "the proposal does not parse: %@": "प्रस्ताव पार्स नहीं होता: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "प्रस्ताव की जाँच ने अधूरा उत्तर लौटाया, इसलिए इस प्रस्ताव पर हस्ताक्षर नहीं किए गए।", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "हर प्राप्तकर्ता का पता दर्ज करें: जो आउटपुट दर्ज किए पते से मेल नहीं खाता, या जिसका पता खाली है, उस पर हस्ताक्षर नहीं हो सकते।", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@ पर किसी नोड ने जवाब नहीं दिया, इसलिए समाप्ति की जाँच नहीं हो सकती और इस प्रस्ताव पर हस्ताक्षर नहीं किए गए। नोड उपलब्ध होने पर फिर कोशिश करें।", + "This proposal is for chain %@; this wallet is on chain %@.": "यह प्रस्ताव चेन %@ के लिए है; यह वॉलेट चेन %@ पर है।", + "This proposal is bound to another genesis than the one this wallet's node serves.": "यह प्रस्ताव उस जेनेसिस से अलग जेनेसिस से बँधा है जिसे इस वॉलेट का नोड परोसता है।", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "यह प्रस्ताव ऊँचाई %@ पर समाप्त होता है और चेन %@ पर है: हस्ताक्षर के लिए बहुत देर हो चुकी। नया प्रस्ताव माँगें।", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "हस्ताक्षरकर्ता की कुंजियाँ हटाई नहीं जा सकीं, इसलिए वॉलेट को भुलाया नहीं गया। फिर से कोशिश करें।", + "Manage multisig signers": "मल्टीसिग हस्ताक्षरकर्ता प्रबंधित करें", + "Multisig signers": "मल्टीसिग हस्ताक्षरकर्ता", + "OK": "ठीक है", + "Enter the address of every payment.": "हर भुगतान का पता दर्ज करें।", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "हर भुगतान की राशि दर्ज करें: यही भुगतान को बाँटने या बढ़ाए जाने से रोकती है।", + "An amount is a number of RAND, such as 1.5.": "राशि RAND की एक संख्या होती है, जैसे 1.5।", + "Sign a proposal": "प्रस्ताव पर हस्ताक्षर करें", + "Proposal (proposal.json)": "प्रस्ताव (proposal.json)", + "Check a share": "हस्ताक्षर अंश जाँचें", + "%@ of %@ · signer %@": "%2$@ में से %1$@ · हस्ताक्षरकर्ता %3$@", + "Open file": "फ़ाइल खोलें", + "Checking…": "जाँच हो रही है…", + "Check proposal": "प्रस्ताव जाँचें", + "What this proposal does, as checked against your policy": "यह प्रस्ताव क्या करता है, आपकी पॉलिसी के विरुद्ध जाँचा गया", + "Your share — send it to whoever proves the proposal": "आपका हस्ताक्षर अंश — इसे उसे भेजें जो प्रस्ताव प्रूव करता है", + "index:hex": "index:hex", + "Check share": "हस्ताक्षर अंश जाँचें", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "हस्ताक्षरकर्ता %1$@ का हस्ताक्षर अंश मान्य है। %3$@ में से %2$@ हाथ में हैं: हस्ताक्षरकर्ता %4$@।", + "That file is larger than a proposal, a policy or a key file can be.": "वह फ़ाइल किसी प्रस्ताव, पॉलिसी या की फ़ाइल के संभव आकार से बड़ी है।", + "Add payment": "भुगतान जोड़ें", + "Check payments": "भुगतान जाँचें", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND के अलावा अन्य भुगतानों की राशि जाँची नहीं जा सकती: उनके लिए राशि खाली छोड़ें।", + "Payee address": "प्राप्तकर्ता का पता", + "Remove": "हटाएँ", + "Amount in RAND": "राशि (RAND में)", + "Intended payments": "इच्छित भुगतान", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "जो भी भुगतान आप करना चाहते हैं, हर एक दर्ज करें: पता और RAND में राशि। एक ही पते पर दो बार भुगतान कुल राशि के साथ एक बार दर्ज होता है। जो आउटपुट इनमें से किसी से मेल नहीं खाता, उस पर हस्ताक्षर नहीं हो सकते।", + "Fee: %@ RAND · expires at height %@": "शुल्क: %@ RAND · ऊँचाई %@ पर समाप्त", + "%@ of %@ payments matched.": "%2$@ में से %1$@ भुगतान मेल खाते हैं।", + "Retry": "दोबारा कोशिश करें", + "%@ blocks left to prove it.": "इसे प्रूव करने के लिए %@ ब्लॉक बाकी हैं।", + "Payments changed: check them again before signing.": "भुगतान बदल गए: हस्ताक्षर से पहले उन्हें फिर जाँचें।", + "Signing…": "हस्ताक्षर हो रहा है…", + "Sign": "हस्ताक्षर करें", + "Type the last four characters of the account fingerprint": "खाते के फ़िंगरप्रिंट के अंतिम चार अक्षर लिखें", + "Save as file": "फ़ाइल के रूप में सहेजें", + "Sign a multisig proposal": "मल्टीसिग प्रस्ताव पर हस्ताक्षर करें", + "this signer is already saved as %@": "यह हस्ताक्षरकर्ता पहले से %@ के रूप में सहेजा गया है", + "a signer label is 1-64 characters": "हस्ताक्षरकर्ता का लेबल 1-64 अक्षरों का होता है", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "पहले डिवाइस का पासकोड सेट करें: हस्ताक्षरकर्ता की कुंजी Face ID, Touch ID या पासकोड के बाद ही पढ़ी जाती है।", + "No such signer profile.": "ऐसी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं है।", + "This signer profile has no key on this device: remove it and add it again.": "इस हस्ताक्षरकर्ता प्रोफ़ाइल की इस डिवाइस पर कोई की नहीं है: इसे हटाकर फिर जोड़ें।", + "No signer profiles yet": "अभी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं", + "Add the policy file and your signer key to sign for a private multisig account.": "निजी मल्टीसिग खाते के लिए हस्ताक्षर करने हेतु पॉलिसी फ़ाइल और अपनी हस्ताक्षरकर्ता की जोड़ें।", + "Remove this signer": "इस हस्ताक्षरकर्ता को हटाएँ", + "Remove a multisig signer": "मल्टीसिग हस्ताक्षरकर्ता हटाएँ", + "Add a signer": "हस्ताक्षरकर्ता जोड़ें", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "हस्ताक्षरकर्ता प्रोफ़ाइल इस फ़ोन की Keychain में रखी जाती है। हर हस्ताक्षर पर फिर से Face ID, Touch ID या आपका पासकोड माँगा जाता है।", + "Policy file": "पॉलिसी फ़ाइल", + "Signer key file, or its 64-hex seed": "हस्ताक्षरकर्ता की फ़ाइल, या उसका 64-हेक्स सीड", + "Account fingerprint — compare it with every other signer before you save": "खाते का फ़िंगरप्रिंट — सहेजने से पहले इसे हर दूसरे हस्ताक्षरकर्ता से मिलाएँ", + "Label": "लेबल", + "Check": "जाँचें", + "%@ of %@ · this key is signer %@": "%2$@ में से %1$@ · यह की हस्ताक्षरकर्ता %3$@ है", + "Save signer": "हस्ताक्षरकर्ता सहेजें", + "That file could not be read.": "वह फ़ाइल पढ़ी नहीं जा सकी।", + "Remove %@?": "%@ को हटाएँ?", + "You need the signer's key file to add this signer back.": "इस हस्ताक्षरकर्ता को दोबारा जोड़ने के लिए आपको उसकी कुंजी फ़ाइल चाहिए होगी।", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "यह प्राप्तकर्ता RAND और एक टोकन दोनों प्राप्त करता है: एक ही प्राप्तकर्ता को RAND और टोकन भुगतानों के लिए अलग-अलग प्रस्ताव चाहिए।" }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", "NSCameraUsageDescription": "Rand Wallet प्राप्तकर्ता के पते या भुगतान लिंक का QR कोड स्कैन करने के लिए कैमरे का उपयोग करता है।", "NSFaceIDUsageDescription": "Rand Wallet आपका वॉलेट अनलॉक करने के लिए Face ID का उपयोग करता है।" } -} \ No newline at end of file +} diff --git a/ios/l10n/id.json b/ios/l10n/id.json index f857497..4811345 100644 --- a/ios/l10n/id.json +++ b/ios/l10n/id.json @@ -425,7 +425,99 @@ "unknown parameter %@": "parameter tidak dikenal %@", "wallet is locked": "dompet terkunci", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "hash transaksi bukan binding proposal", + "the proposal's signing_message is not its binding's": "signing_message proposal bukan milik binding-nya", + "the proposal's auth_commit is not this policy's account": "auth_commit proposal bukan akun kebijakan ini", + "the proposal's expected digest is not its bundle's": "digest yang diharapkan proposal bukan milik bundle-nya", + "the transaction carries no bundle": "transaksi tidak membawa bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transaksi membawa sebuah aksi; proposal multisig privat adalah pembayaran dari bundle-nya saja", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "nilai witness tidak dapat diperiksa: nk atau salt-nya bukan milik proposal ini", + "this key is not one of the policy's signers": "kunci ini bukan salah satu penanda tangan kebijakan", + "a share is index:hex": "sebuah bagian tanda tangan berformat index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "kunci penanda tangan adalah berkas keygen rand-node atau 64 karakter hex", + "a signer seed is 64 hex characters": "seed penanda tangan terdiri dari 64 karakter hex", + "expected_recipients must be a list of addresses": "expected_recipients harus berupa daftar alamat", + "the key file's public_key is not the one its seed derives": "public_key berkas kunci bukan yang diturunkan seed-nya", + "the key file's address is not the one its seed derives": "alamat berkas kunci bukan yang diturunkan seed-nya", + "no expected recipient given: the payee was NOT checked": "tidak ada penerima yang diharapkan: penerima pembayaran TIDAK diperiksa", + "this proposal is for another policy: it names %@, this policy is %@": "proposal ini untuk kebijakan lain: menyebut %@, kebijakan ini adalah %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "output %@ tidak terbuka sebagai milik akun ini: menolak menandatangani nilai yang tidak dapat dilihatnya", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "nilai witness tidak cocok dengan transaksi: input %@ tidak menghasilkan nullifier bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "nilai witness tidak cocok dengan output: aset %@ memiliki %@ masuk dan %@ keluar beserta biaya dan pembakaran", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "proposal kedaluwarsa pada %@, di luar jendela bundle-nya (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "transaksi ini untuk chain %@, proposal menyebut chain %@", + "signer %@'s signature does not verify over this proposal": "tanda tangan penanda tangan %@ tidak valid untuk proposal ini", + "signer %@ is not in this policy (it has %@ signers)": "penanda tangan %@ tidak ada dalam kebijakan ini (kebijakan ini memiliki %@ penanda tangan)", + "expected recipient %@: %@": "penerima yang diharapkan %@: %@", + "the policy does not parse: %@": "kebijakan tidak dapat diurai: %@", + "the proposal does not parse: %@": "proposal tidak dapat diurai: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Pemeriksaan proposal mengembalikan jawaban yang tidak lengkap, sehingga proposal ini tidak ditandatangani.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Masukkan alamat setiap penerima pembayaran: output yang tidak cocok dengan alamat yang Anda masukkan, atau yang alamatnya kosong, tidak dapat ditandatangani.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Tidak ada node yang menjawab di %@, sehingga masa berlaku tidak dapat diperiksa dan proposal ini tidak ditandatangani. Coba lagi saat node dapat dijangkau.", + "This proposal is for chain %@; this wallet is on chain %@.": "Proposal ini untuk chain %@; dompet ini berada di chain %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Proposal ini terikat pada genesis yang berbeda dari yang dilayani node dompet ini.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Proposal ini kedaluwarsa pada ketinggian %@ dan chain berada di %@: terlambat untuk menandatangani. Minta proposal baru.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Kunci penanda tangan tidak dapat dihapus, jadi dompet tidak dilupakan. Coba lagi.", + "Manage multisig signers": "Kelola penanda tangan multisig", + "Multisig signers": "Penanda tangan multisig", + "OK": "OK", + "Enter the address of every payment.": "Masukkan alamat setiap pembayaran.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Masukkan jumlah setiap pembayaran: inilah yang mencegah pemecahan atau pembayaran yang digelembungkan.", + "An amount is a number of RAND, such as 1.5.": "Jumlah adalah bilangan RAND, misalnya 1.5.", + "Sign a proposal": "Tandatangani proposal", + "Proposal (proposal.json)": "Proposal (proposal.json)", + "Check a share": "Periksa bagian tanda tangan", + "%@ of %@ · signer %@": "%@ dari %@ · penanda tangan %@", + "Open file": "Buka berkas", + "Checking…": "Memeriksa…", + "Check proposal": "Periksa proposal", + "What this proposal does, as checked against your policy": "Apa yang dilakukan proposal ini, diperiksa terhadap kebijakan Anda", + "Your share — send it to whoever proves the proposal": "Bagian tanda tangan Anda — kirim ke yang membuktikan proposal", + "index:hex": "index:hex", + "Check share": "Periksa bagian tanda tangan", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "Bagian tanda tangan penanda tangan %@ valid. %@ dari %@ sudah terkumpul: penanda tangan %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Berkas itu lebih besar dari yang mungkin untuk sebuah proposal, kebijakan, atau berkas kunci.", + "Add payment": "Tambah pembayaran", + "Check payments": "Periksa pembayaran", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Jumlah tidak dapat diperiksa untuk pembayaran non-RAND: kosongkan jumlahnya untuk pembayaran tersebut.", + "Payee address": "Alamat penerima pembayaran", + "Remove": "Hapus", + "Amount in RAND": "Jumlah dalam RAND", + "Intended payments": "Pembayaran yang dimaksud", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Masukkan setiap pembayaran yang akan Anda lakukan: alamat dan jumlah dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan satu kali, dengan totalnya. Output yang tidak cocok dengan satu pun tidak dapat ditandatangani.", + "Fee: %@ RAND · expires at height %@": "Biaya: %@ RAND · kedaluwarsa pada ketinggian %@", + "%@ of %@ payments matched.": "%@ dari %@ pembayaran cocok.", + "Retry": "Coba lagi", + "%@ blocks left to prove it.": "Tersisa %@ blok untuk membuktikannya.", + "Payments changed: check them again before signing.": "Pembayaran berubah: periksa lagi sebelum menandatangani.", + "Signing…": "Menandatangani…", + "Sign": "Tandatangani", + "Type the last four characters of the account fingerprint": "Ketik empat karakter terakhir sidik jari akun", + "Save as file": "Simpan sebagai berkas", + "Sign a multisig proposal": "Tandatangani proposal multisig", + "this signer is already saved as %@": "penanda tangan ini sudah tersimpan sebagai %@", + "a signer label is 1-64 characters": "label penanda tangan terdiri dari 1-64 karakter", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Setel kode sandi perangkat terlebih dahulu: kunci penanda tangan hanya dibaca setelah Face ID, Touch ID, atau kode sandi.", + "No such signer profile.": "Profil penanda tangan itu tidak ada.", + "This signer profile has no key on this device: remove it and add it again.": "Profil penanda tangan ini tidak memiliki kunci di perangkat ini: hapus lalu tambahkan lagi.", + "No signer profiles yet": "Belum ada profil penanda tangan", + "Add the policy file and your signer key to sign for a private multisig account.": "Tambahkan berkas kebijakan dan kunci penanda tangan Anda untuk menandatangani akun multisig privat.", + "Remove this signer": "Hapus penanda tangan ini", + "Remove a multisig signer": "Hapus penanda tangan multisig", + "Add a signer": "Tambah penanda tangan", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Profil penanda tangan disimpan di Keychain ponsel ini. Setiap tanda tangan meminta Face ID, Touch ID, atau kode sandi Anda lagi.", + "Policy file": "Berkas kebijakan", + "Signer key file, or its 64-hex seed": "Berkas kunci penanda tangan, atau seed 64 hex-nya", + "Account fingerprint — compare it with every other signer before you save": "Sidik jari akun — bandingkan dengan setiap penanda tangan lain sebelum menyimpan", + "Label": "Label", + "Check": "Periksa", + "%@ of %@ · this key is signer %@": "%@ dari %@ · kunci ini adalah penanda tangan %@", + "Save signer": "Simpan penanda tangan", + "That file could not be read.": "File itu tidak dapat dibaca.", + "Remove %@?": "Hapus %@?", + "You need the signer's key file to add this signer back.": "Anda memerlukan file kunci penanda tangan untuk menambahkannya kembali.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Penerima ini menerima RAND sekaligus token: pembayaran RAND dan token ke satu penerima memerlukan proposal terpisah." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/it.json b/ios/l10n/it.json index 13a43f4..23fc5f0 100644 --- a/ios/l10n/it.json +++ b/ios/l10n/it.json @@ -449,7 +449,99 @@ "unknown parameter %@": "parametro sconosciuto %@", "wallet is locked": "il portafoglio è bloccato", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "la transazione non ha come hash il binding della proposta", + "the proposal's signing_message is not its binding's": "il signing_message della proposta non è quello del suo binding", + "the proposal's auth_commit is not this policy's account": "l'auth_commit della proposta non è l'account di questa policy", + "the proposal's expected digest is not its bundle's": "il digest atteso della proposta non è quello del suo bundle", + "the transaction carries no bundle": "la transazione non porta alcun bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transazione porta un'azione; una proposta multisig privata è un pagamento dal solo suo bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "il valore del witness non si può verificare: il suo nk o il suo salt non sono quelli di questa proposta", + "this key is not one of the policy's signers": "questa chiave non è uno dei firmatari della policy", + "a share is index:hex": "una quota di firma è index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "una chiave di firmatario è un file keygen di rand-node o 64 caratteri esadecimali", + "a signer seed is 64 hex characters": "un seed di firmatario è di 64 caratteri esadecimali", + "expected_recipients must be a list of addresses": "expected_recipients deve essere un elenco di indirizzi", + "the key file's public_key is not the one its seed derives": "il public_key del file di chiave non è quello che il suo seed deriva", + "the key file's address is not the one its seed derives": "l'indirizzo del file di chiave non è quello che il suo seed deriva", + "no expected recipient given: the payee was NOT checked": "nessun destinatario atteso indicato: il beneficiario NON è stato verificato", + "this proposal is for another policy: it names %@, this policy is %@": "questa proposta è per un'altra policy: nomina %@, questa policy è %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "l'output %@ non si apre come di questo account: rifiuto di firmare un valore che non può vedere", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "il valore del witness non corrisponde alla transazione: l'input %@ non produce il nullifier del bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "il valore del witness non corrisponde agli output: l'asset %@ ha %@ in ingresso e %@ in uscita con la commissione e i burn", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "la proposta scade a %@, fuori dalla finestra del suo bundle (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "la transazione è per la catena %@, la proposta indica la catena %@", + "signer %@'s signature does not verify over this proposal": "la firma del firmatario %@ non è valida per questa proposta", + "signer %@ is not in this policy (it has %@ signers)": "il firmatario %@ non è in questa policy (ha %@ firmatari)", + "expected recipient %@: %@": "destinatario atteso %@: %@", + "the policy does not parse: %@": "la policy non si può analizzare: %@", + "the proposal does not parse: %@": "la proposta non si può analizzare: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La verifica della proposta ha restituito una risposta incompleta, quindi questa proposta non viene firmata.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Inserisci l'indirizzo di ogni beneficiario: un output che non corrisponde all'indirizzo inserito, o il cui indirizzo è vuoto, non può essere firmato.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Nessun nodo ha risposto a %@, quindi la scadenza non si può verificare e questa proposta non viene firmata. Riprova quando il nodo è raggiungibile.", + "This proposal is for chain %@; this wallet is on chain %@.": "Questa proposta è per la catena %@; questo portafoglio è sulla catena %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Questa proposta è legata a un genesis diverso da quello che serve il nodo di questo portafoglio.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Questa proposta scade all'altezza %@ e la catena è a %@: troppo tardi per firmare. Chiedi una nuova proposta.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Non è stato possibile rimuovere le chiavi del firmatario, quindi il portafoglio non è stato dimenticato. Riprova.", + "Manage multisig signers": "Gestisci i firmatari multisig", + "Multisig signers": "Firmatari multisig", + "OK": "OK", + "Enter the address of every payment.": "Inserisci l'indirizzo di ogni pagamento.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Inserisci l'importo di ogni pagamento: è ciò che impedisce una divisione o un pagamento gonfiato.", + "An amount is a number of RAND, such as 1.5.": "Un importo è un numero di RAND, come 1.5.", + "Sign a proposal": "Firma una proposta", + "Proposal (proposal.json)": "Proposta (proposal.json)", + "Check a share": "Verifica una quota di firma", + "%@ of %@ · signer %@": "%@ di %@ · firmatario %@", + "Open file": "Apri file", + "Checking…": "Verifica in corso…", + "Check proposal": "Verifica la proposta", + "What this proposal does, as checked against your policy": "Cosa fa questa proposta, verificato rispetto alla tua policy", + "Your share — send it to whoever proves the proposal": "La tua quota di firma — mandala a chi dimostra la proposta", + "index:hex": "index:hex", + "Check share": "Verifica la quota di firma", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "La quota di firma del firmatario %@ è valida. %@ di %@ in mano: firmatari %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Quel file è più grande di quanto possa essere una proposta, una policy o un file di chiave.", + "Add payment": "Aggiungi pagamento", + "Check payments": "Verifica i pagamenti", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Gli importi non si possono verificare per i pagamenti non in RAND: lascia vuoto l'importo per quelli.", + "Payee address": "Indirizzo del beneficiario", + "Remove": "Rimuovi", + "Amount in RAND": "Importo in RAND", + "Intended payments": "Pagamenti previsti", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Inserisci ogni pagamento che intendi fare: l'indirizzo e l'importo in RAND. Un pagamento allo stesso indirizzo due volte si inserisce una volta sola, con il totale. Un output che non corrisponde a nessuno non può essere firmato.", + "Fee: %@ RAND · expires at height %@": "Commissione: %@ RAND · scade all'altezza %@", + "%@ of %@ payments matched.": "%@ pagamenti su %@ corrispondono.", + "Retry": "Riprova", + "%@ blocks left to prove it.": "Mancano %@ blocchi per dimostrarlo.", + "Payments changed: check them again before signing.": "I pagamenti sono cambiati: verificali di nuovo prima di firmare.", + "Signing…": "Firma in corso…", + "Sign": "Firma", + "Type the last four characters of the account fingerprint": "Digita gli ultimi quattro caratteri dell'impronta dell'account", + "Save as file": "Salva come file", + "Sign a multisig proposal": "Firma una proposta multisig", + "this signer is already saved as %@": "questo firmatario è già salvato come %@", + "a signer label is 1-64 characters": "l'etichetta di un firmatario è di 1-64 caratteri", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Imposta prima un codice sul dispositivo: la chiave di un firmatario viene letta solo dopo Face ID, Touch ID o il codice.", + "No such signer profile.": "Nessun profilo di firmatario del genere.", + "This signer profile has no key on this device: remove it and add it again.": "Questo profilo di firmatario non ha una chiave su questo dispositivo: rimuovilo e aggiungilo di nuovo.", + "No signer profiles yet": "Ancora nessun profilo di firmatario", + "Add the policy file and your signer key to sign for a private multisig account.": "Aggiungi il file della policy e la tua chiave di firmatario per firmare per un account multisig privato.", + "Remove this signer": "Rimuovi questo firmatario", + "Remove a multisig signer": "Rimuovi un firmatario multisig", + "Add a signer": "Aggiungi un firmatario", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Un profilo di firmatario è conservato nel portachiavi (Keychain) di questo telefono. Ogni firma chiede di nuovo Face ID, Touch ID o il tuo codice.", + "Policy file": "File della policy", + "Signer key file, or its 64-hex seed": "File della chiave del firmatario, o il suo seed di 64 hex", + "Account fingerprint — compare it with every other signer before you save": "Impronta dell'account — confrontala con quella di ogni altro firmatario prima di salvare", + "Label": "Etichetta", + "Check": "Verifica", + "%@ of %@ · this key is signer %@": "%@ di %@ · questa chiave è il firmatario %@", + "Save signer": "Salva firmatario", + "That file could not be read.": "Impossibile leggere quel file.", + "Remove %@?": "Rimuovere %@?", + "You need the signer's key file to add this signer back.": "Serve il file della chiave del firmatario per aggiungerlo di nuovo.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Questo beneficiario riceve sia RAND sia un token: i pagamenti in RAND e in token a uno stesso beneficiario richiedono proposte separate." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ja.json b/ios/l10n/ja.json index a111bca..2ec5dfc 100644 --- a/ios/l10n/ja.json +++ b/ios/l10n/ja.json @@ -425,7 +425,99 @@ "unknown parameter %@": "不明なパラメーター %@", "wallet is locked": "ウォレットがロックされています", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "トランザクションのハッシュが提案のバインディングと一致しません", + "the proposal's signing_message is not its binding's": "提案の signing_message が、そのバインディングのものと一致しません", + "the proposal's auth_commit is not this policy's account": "提案の auth_commit が、このポリシーのアカウントと一致しません", + "the proposal's expected digest is not its bundle's": "提案の想定ダイジェストが、そのバンドルのものと一致しません", + "the transaction carries no bundle": "トランザクションにバンドルが含まれていません", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "トランザクションにアクションが含まれています。プライベートなマルチシグ提案は、そのバンドルだけからの支払いです", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "ウィットネスの値を確認できません: その nk またはソルトがこの提案のものではありません", + "this key is not one of the policy's signers": "このキーはポリシーの署名者ではありません", + "a share is index:hex": "署名シェアは index:hex 形式です", + "a signer key is a rand-node keygen file or 64 hex characters": "署名者キーは rand-node の keygen ファイル、または 64 桁の16進文字です", + "a signer seed is 64 hex characters": "署名者のシードは 64 桁の16進文字です", + "expected_recipients must be a list of addresses": "expected_recipients はアドレスのリストでなければなりません", + "the key file's public_key is not the one its seed derives": "キーファイルの public_key が、そのシードから導出されるものと一致しません", + "the key file's address is not the one its seed derives": "キーファイルのアドレスが、そのシードから導出されるものと一致しません", + "no expected recipient given: the payee was NOT checked": "想定される受取人が指定されていません: 受取人は確認されていません (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "この提案は別のポリシー用です: %@ を指していますが、このポリシーは %@ です", + "output %@ does not open as this account's: refusing to sign value it cannot see": "出力 %@ はこのアカウントのものとして開けません: 見えない値への署名を拒否します", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "ウィットネスの値がトランザクションと一致しません: 入力 %@ はバンドルのヌリファイアに対応しません", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "ウィットネスの値が出力と一致しません: アセット %@ は、入力 %@ に対し、手数料とバーンを含めた出力が %@ です", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "提案の期限 %@ は、そのバンドルの範囲 (%@, %@] の外です", + "the transaction is for chain %@, the proposal says chain %@": "トランザクションはチェーン %@ 用ですが、提案はチェーン %@ を指しています", + "signer %@'s signature does not verify over this proposal": "署名者 %@ の署名は、この提案に対して有効ではありません", + "signer %@ is not in this policy (it has %@ signers)": "署名者 %@ はこのポリシーに含まれていません (署名者は %@ 人です)", + "expected recipient %@: %@": "想定される受取人 %@: %@", + "the policy does not parse: %@": "ポリシーを解析できません: %@", + "the proposal does not parse: %@": "提案を解析できません: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案の確認が不完全な応答を返したため、この提案には署名しません。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "すべての受取人のアドレスを入力してください。入力したアドレスと一致しない出力、またはアドレスが空の出力には署名できません。", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@ でノードから応答がなかったため、有効期限を確認できず、この提案には署名しません。ノードに接続できるようになってから再試行してください。", + "This proposal is for chain %@; this wallet is on chain %@.": "この提案はチェーン %@ 用ですが、このウォレットはチェーン %@ にあります。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "この提案は、このウォレットのノードが提供するものとは別のジェネシスに結び付けられています。", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "この提案は高さ %@ で期限切れとなり、チェーンは %@ にあります: 署名には遅すぎます。新しい提案を依頼してください。", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "署名者の鍵を削除できなかったため、ウォレットは削除されませんでした。もう一度お試しください。", + "Manage multisig signers": "マルチシグ署名者を管理", + "Multisig signers": "マルチシグ署名者", + "OK": "OK", + "Enter the address of every payment.": "すべての支払いのアドレスを入力してください。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "すべての支払いの金額を入力してください。支払いの分割や水増しを防ぐのは金額です。", + "An amount is a number of RAND, such as 1.5.": "金額は RAND の数値です。例: 1.5", + "Sign a proposal": "提案に署名", + "Proposal (proposal.json)": "提案 (proposal.json)", + "Check a share": "署名シェアを確認", + "%@ of %@ · signer %@": "%@/%@ · 署名者 %@", + "Open file": "ファイルを開く", + "Checking…": "確認中…", + "Check proposal": "提案を確認", + "What this proposal does, as checked against your policy": "この提案が行うこと (あなたのポリシーと照合済み)", + "Your share — send it to whoever proves the proposal": "あなたの署名シェア — 提案を証明する人に送ってください", + "index:hex": "index:hex", + "Check share": "署名シェアを確認", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "署名者 %1$@ の署名シェアは有効です。%3$@ 件中 %2$@ 件が揃っています: 署名者 %4$@。", + "That file is larger than a proposal, a policy or a key file can be.": "そのファイルは、提案・ポリシー・キーファイルとして許容されるサイズを超えています。", + "Add payment": "支払いを追加", + "Check payments": "支払いを確認", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND 以外の支払いは金額を確認できません。その場合は金額を空欄にしてください。", + "Payee address": "受取人のアドレス", + "Remove": "削除", + "Amount in RAND": "金額 (RAND)", + "Intended payments": "予定している支払い", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "行う予定の支払いをすべて入力してください。アドレスと RAND 建ての金額です。同じアドレスへの支払いが2回ある場合は、合計額で1件として入力します。どれにも一致しない出力には署名できません。", + "Fee: %@ RAND · expires at height %@": "手数料: %@ RAND · 高さ %@ で期限切れ", + "%@ of %@ payments matched.": "%2$@ 件中 %1$@ 件の支払いが一致しました。", + "Retry": "再試行", + "%@ blocks left to prove it.": "証明まであと %@ ブロックです。", + "Payments changed: check them again before signing.": "支払いが変更されました。署名する前にもう一度確認してください。", + "Signing…": "署名中…", + "Sign": "署名", + "Type the last four characters of the account fingerprint": "アカウントのフィンガープリントの末尾4文字を入力してください", + "Save as file": "ファイルとして保存", + "Sign a multisig proposal": "マルチシグの提案に署名", + "this signer is already saved as %@": "この署名者はすでに %@ として保存されています", + "a signer label is 1-64 characters": "署名者のラベルは 1-64 文字です", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "先にデバイスのパスコードを設定してください。署名者の鍵は Face ID、Touch ID、またはパスコードの後にのみ読み取られます。", + "No such signer profile.": "そのような署名者プロファイルはありません。", + "This signer profile has no key on this device: remove it and add it again.": "この署名者プロファイルには、この端末にキーがありません。削除してから追加し直してください。", + "No signer profiles yet": "署名者プロファイルはまだありません", + "Add the policy file and your signer key to sign for a private multisig account.": "プライベートなマルチシグアカウントで署名するには、ポリシーファイルと署名者キーを追加してください。", + "Remove this signer": "この署名者を削除", + "Remove a multisig signer": "マルチシグの署名者を削除", + "Add a signer": "署名者を追加", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "署名者プロファイルはこのiPhoneのキーチェーン(Keychain)に保存されます。署名のたびに Face ID、Touch ID、またはパスコードを再度求めます。", + "Policy file": "ポリシーファイル", + "Signer key file, or its 64-hex seed": "署名者キーファイル、または 64 桁16進のシード", + "Account fingerprint — compare it with every other signer before you save": "アカウントのフィンガープリント — 保存する前に、他のすべての署名者と照合してください", + "Label": "ラベル", + "Check": "確認", + "%@ of %@ · this key is signer %@": "%@/%@ · このキーは署名者 %@ です", + "Save signer": "署名者を保存", + "That file could not be read.": "そのファイルを読み込めませんでした。", + "Remove %@?": "%@を削除しますか?", + "You need the signer's key file to add this signer back.": "この署名者を再び追加するには、署名者の鍵ファイルが必要です。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "この受取人は RAND とトークンの両方を受け取ります。同じ受取人への RAND とトークンの支払いは、別々の提案にする必要があります。" }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ko.json b/ios/l10n/ko.json index c10e793..3c80d61 100644 --- a/ios/l10n/ko.json +++ b/ios/l10n/ko.json @@ -425,7 +425,99 @@ "unknown parameter %@": "알 수 없는 매개변수 %@", "wallet is locked": "지갑이 잠겨 있습니다", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "트랜잭션의 해시가 제안의 바인딩과 다릅니다", + "the proposal's signing_message is not its binding's": "제안의 signing_message가 바인딩의 것과 다릅니다", + "the proposal's auth_commit is not this policy's account": "제안의 auth_commit이 이 정책의 계정이 아닙니다", + "the proposal's expected digest is not its bundle's": "제안의 예상 다이제스트가 번들의 것과 다릅니다", + "the transaction carries no bundle": "트랜잭션에 번들이 없습니다", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "트랜잭션에 액션이 포함되어 있습니다. 비공개 멀티시그 제안은 번들만으로 이루어진 결제입니다", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "증인(witness) 값을 확인할 수 없습니다: nk 또는 솔트가 이 제안의 것이 아닙니다", + "this key is not one of the policy's signers": "이 키는 정책의 서명자가 아닙니다", + "a share is index:hex": "서명 조각은 index:hex 형식입니다", + "a signer key is a rand-node keygen file or 64 hex characters": "서명자 키는 rand-node keygen 파일 또는 64자리 16진수 문자입니다", + "a signer seed is 64 hex characters": "서명자 시드는 64자리 16진수 문자입니다", + "expected_recipients must be a list of addresses": "expected_recipients는 주소 목록이어야 합니다", + "the key file's public_key is not the one its seed derives": "키 파일의 public_key가 시드에서 도출된 값과 다릅니다", + "the key file's address is not the one its seed derives": "키 파일의 주소가 시드에서 도출된 주소와 다릅니다", + "no expected recipient given: the payee was NOT checked": "예상 수취인이 지정되지 않았습니다: 수취인을 확인하지 않았습니다 (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "이 제안은 다른 정책용입니다: %@을(를) 가리키며 이 정책은 %@입니다", + "output %@ does not open as this account's: refusing to sign value it cannot see": "출력 %@이(가) 이 계정의 것으로 열리지 않습니다: 볼 수 없는 값에는 서명을 거부합니다", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "증인(witness) 값이 트랜잭션과 일치하지 않습니다: 입력 %@이(가) 번들의 널리파이어로 이어지지 않습니다", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "증인(witness) 값이 출력과 일치하지 않습니다: 자산 %@의 입력은 %@, 수수료와 소각을 포함한 출력은 %@입니다", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "제안의 만료 %@이(가) 번들의 구간 (%@, %@] 밖에 있습니다", + "the transaction is for chain %@, the proposal says chain %@": "트랜잭션은 체인 %@용이지만 제안은 체인 %@을(를) 가리킵니다", + "signer %@'s signature does not verify over this proposal": "서명자 %@의 서명이 이 제안에 대해 유효하지 않습니다", + "signer %@ is not in this policy (it has %@ signers)": "서명자 %@은(는) 이 정책에 없습니다 (서명자는 %@명입니다)", + "expected recipient %@: %@": "예상 수취인 %@: %@", + "the policy does not parse: %@": "정책을 해석할 수 없습니다: %@", + "the proposal does not parse: %@": "제안을 해석할 수 없습니다: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "제안 확인이 불완전한 응답을 반환했으므로 이 제안에는 서명하지 않습니다.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "모든 수취인의 주소를 입력하세요. 입력한 주소와 일치하지 않거나 주소가 비어 있는 출력은 서명할 수 없습니다.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@에서 응답한 노드가 없어 만료를 확인할 수 없으므로 이 제안에는 서명하지 않습니다. 노드에 연결할 수 있을 때 다시 시도하세요.", + "This proposal is for chain %@; this wallet is on chain %@.": "이 제안은 체인 %@용이며, 이 지갑은 체인 %@에 있습니다.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "이 제안은 이 지갑의 노드가 제공하는 것과 다른 제네시스에 묶여 있습니다.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "이 제안은 높이 %@에서 만료되며 체인은 %@에 있습니다: 서명하기에는 너무 늦었습니다. 새 제안을 요청하세요.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "서명자 키를 제거하지 못해 지갑을 삭제하지 않았습니다. 다시 시도하세요.", + "Manage multisig signers": "멀티시그 서명자 관리", + "Multisig signers": "멀티시그 서명자", + "OK": "확인", + "Enter the address of every payment.": "모든 결제의 주소를 입력하세요.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "모든 결제의 금액을 입력하세요. 결제를 쪼개거나 부풀리는 것을 막아 주는 것이 금액입니다.", + "An amount is a number of RAND, such as 1.5.": "금액은 RAND 단위의 숫자입니다. 예: 1.5", + "Sign a proposal": "제안에 서명", + "Proposal (proposal.json)": "제안 (proposal.json)", + "Check a share": "서명 조각 확인", + "%@ of %@ · signer %@": "%@/%@ · 서명자 %@", + "Open file": "파일 열기", + "Checking…": "확인하는 중…", + "Check proposal": "제안 확인", + "What this proposal does, as checked against your policy": "이 제안이 하는 일 (정책과 대조해 확인됨)", + "Your share — send it to whoever proves the proposal": "내 서명 조각 — 제안을 증명하는 사람에게 보내세요", + "index:hex": "index:hex", + "Check share": "서명 조각 확인", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "서명자 %1$@의 서명 조각이 유효합니다. %3$@개 중 %2$@개 확보: 서명자 %4$@.", + "That file is larger than a proposal, a policy or a key file can be.": "파일이 제안, 정책, 키 파일로 허용되는 크기보다 큽니다.", + "Add payment": "결제 추가", + "Check payments": "결제 확인", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND가 아닌 결제는 금액을 확인할 수 없습니다. 이 경우 금액을 비워 두세요.", + "Payee address": "수취인 주소", + "Remove": "삭제", + "Amount in RAND": "금액 (RAND)", + "Intended payments": "예정된 결제", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "보내려는 모든 결제를 입력하세요: 주소와 RAND 금액입니다. 같은 주소로 두 번 결제하는 경우 합계로 한 번만 입력합니다. 어느 것과도 일치하지 않는 출력은 서명할 수 없습니다.", + "Fee: %@ RAND · expires at height %@": "수수료: %@ RAND · 높이 %@에서 만료", + "%@ of %@ payments matched.": "결제 %2$@건 중 %1$@건이 일치합니다.", + "Retry": "다시 시도", + "%@ blocks left to prove it.": "증명까지 %@ 블록 남았습니다.", + "Payments changed: check them again before signing.": "결제가 변경되었습니다. 서명하기 전에 다시 확인하세요.", + "Signing…": "서명하는 중…", + "Sign": "서명", + "Type the last four characters of the account fingerprint": "계정 지문의 마지막 네 글자를 입력하세요", + "Save as file": "파일로 저장", + "Sign a multisig proposal": "멀티시그 제안에 서명", + "this signer is already saved as %@": "이 서명자는 이미 %@(으)로 저장되어 있습니다", + "a signer label is 1-64 characters": "서명자 레이블은 1-64자입니다", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "먼저 기기 암호를 설정하세요. 서명자 키는 Face ID, Touch ID 또는 암호 확인 후에만 읽습니다.", + "No such signer profile.": "해당 서명자 프로필이 없습니다.", + "This signer profile has no key on this device: remove it and add it again.": "이 서명자 프로필에는 이 기기에 키가 없습니다. 제거한 뒤 다시 추가하세요.", + "No signer profiles yet": "서명자 프로필이 아직 없습니다", + "Add the policy file and your signer key to sign for a private multisig account.": "비공개 멀티시그 계정으로 서명하려면 정책 파일과 서명자 키를 추가하세요.", + "Remove this signer": "이 서명자 제거", + "Remove a multisig signer": "멀티시그 서명자 제거", + "Add a signer": "서명자 추가", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "서명자 프로필은 이 휴대폰의 키체인(Keychain)에 보관됩니다. 서명할 때마다 Face ID, Touch ID 또는 암호를 다시 요청합니다.", + "Policy file": "정책 파일", + "Signer key file, or its 64-hex seed": "서명자 키 파일 또는 64자리 16진수 시드", + "Account fingerprint — compare it with every other signer before you save": "계정 지문 — 저장하기 전에 다른 모든 서명자와 비교하세요", + "Label": "레이블", + "Check": "확인", + "%@ of %@ · this key is signer %@": "%@/%@ · 이 키는 서명자 %@입니다", + "Save signer": "서명자 저장", + "That file could not be read.": "해당 파일을 읽을 수 없습니다.", + "Remove %@?": "%@ 서명자를 제거할까요?", + "You need the signer's key file to add this signer back.": "이 서명자를 다시 추가하려면 서명자의 키 파일이 필요합니다.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "이 수취인은 RAND와 토큰을 모두 받습니다. 한 수취인에게 보내는 RAND 결제와 토큰 결제는 별도의 제안이 필요합니다." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ms.json b/ios/l10n/ms.json index 635f0b4..61e7c8d 100644 --- a/ios/l10n/ms.json +++ b/ios/l10n/ms.json @@ -425,7 +425,99 @@ "unknown parameter %@": "parameter tidak diketahui %@", "wallet is locked": "dompet dikunci", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "hash transaksi bukan binding cadangan", + "the proposal's signing_message is not its binding's": "signing_message cadangan bukan milik binding-nya", + "the proposal's auth_commit is not this policy's account": "auth_commit cadangan bukan akaun dasar ini", + "the proposal's expected digest is not its bundle's": "digest dijangka cadangan bukan milik bundle-nya", + "the transaction carries no bundle": "transaksi tidak membawa bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transaksi membawa satu tindakan; cadangan multisig persendirian ialah pembayaran daripada bundle-nya sahaja", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "nilai witness tidak dapat disemak: nk atau salt-nya bukan milik cadangan ini", + "this key is not one of the policy's signers": "kunci ini bukan salah satu penandatangan dasar", + "a share is index:hex": "bahagian tandatangan ialah index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "kunci penandatangan ialah fail keygen rand-node atau 64 aksara hex", + "a signer seed is 64 hex characters": "seed penandatangan ialah 64 aksara hex", + "expected_recipients must be a list of addresses": "expected_recipients mestilah senarai alamat", + "the key file's public_key is not the one its seed derives": "public_key fail kunci bukan yang diterbitkan oleh seed-nya", + "the key file's address is not the one its seed derives": "alamat fail kunci bukan yang diterbitkan oleh seed-nya", + "no expected recipient given: the payee was NOT checked": "tiada penerima dijangka diberikan: penerima bayaran TIDAK disemak", + "this proposal is for another policy: it names %@, this policy is %@": "cadangan ini untuk dasar lain: ia menyebut %@, dasar ini ialah %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "output %@ tidak terbuka sebagai milik akaun ini: enggan menandatangani nilai yang tidak dapat dilihatnya", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "nilai witness tidak sepadan dengan transaksi: input %@ tidak menjadi nullifier bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "nilai witness tidak sepadan dengan output: aset %@ mempunyai %@ masuk dan %@ keluar dengan yuran dan pembakaran", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "cadangan tamat tempoh pada %@, di luar tetingkap bundle-nya (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "transaksi ini untuk rantaian %@, cadangan menyebut rantaian %@", + "signer %@'s signature does not verify over this proposal": "tandatangan penandatangan %@ tidak sah untuk cadangan ini", + "signer %@ is not in this policy (it has %@ signers)": "penandatangan %@ tiada dalam dasar ini (ia mempunyai %@ penandatangan)", + "expected recipient %@: %@": "penerima dijangka %@: %@", + "the policy does not parse: %@": "dasar tidak dapat dihuraikan: %@", + "the proposal does not parse: %@": "cadangan tidak dapat dihuraikan: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Semakan cadangan memulangkan jawapan yang tidak lengkap, jadi cadangan ini tidak ditandatangani.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Masukkan alamat setiap penerima bayaran: output yang tidak sepadan dengan alamat yang anda masukkan, atau yang alamatnya kosong, tidak boleh ditandatangani.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Tiada nod menjawab di %@, jadi tempoh tamat tidak dapat disemak dan cadangan ini tidak ditandatangani. Cuba lagi apabila nod boleh dicapai.", + "This proposal is for chain %@; this wallet is on chain %@.": "Cadangan ini untuk rantaian %@; dompet ini berada pada rantaian %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Cadangan ini terikat pada genesis yang berbeza daripada yang dilayani nod dompet ini.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Cadangan ini tamat tempoh pada ketinggian %@ dan rantaian berada di %@: terlambat untuk menandatangani. Minta cadangan baharu.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Kunci penandatangan tidak dapat dibuang, jadi dompet tidak dilupakan. Cuba lagi.", + "Manage multisig signers": "Urus penandatangan multisig", + "Multisig signers": "Penandatangan multisig", + "OK": "OK", + "Enter the address of every payment.": "Masukkan alamat setiap pembayaran.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Masukkan amaun setiap pembayaran: ia menghalang pemecahan atau pembayaran yang dibengkakkan.", + "An amount is a number of RAND, such as 1.5.": "Amaun ialah bilangan RAND, seperti 1.5.", + "Sign a proposal": "Tandatangani cadangan", + "Proposal (proposal.json)": "Cadangan (proposal.json)", + "Check a share": "Semak bahagian tandatangan", + "%@ of %@ · signer %@": "%@ daripada %@ · penandatangan %@", + "Open file": "Buka fail", + "Checking…": "Menyemak…", + "Check proposal": "Semak cadangan", + "What this proposal does, as checked against your policy": "Apa yang cadangan ini lakukan, disemak terhadap dasar anda", + "Your share — send it to whoever proves the proposal": "Bahagian tandatangan anda — hantar kepada yang membuktikan cadangan", + "index:hex": "index:hex", + "Check share": "Semak bahagian tandatangan", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "Bahagian tandatangan penandatangan %@ sah. %@ daripada %@ sudah ada: penandatangan %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Fail itu lebih besar daripada yang boleh bagi sesebuah cadangan, dasar atau fail kunci.", + "Add payment": "Tambah pembayaran", + "Check payments": "Semak pembayaran", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Amaun tidak boleh disemak untuk pembayaran bukan RAND: biarkan amaun kosong bagi pembayaran itu.", + "Payee address": "Alamat penerima bayaran", + "Remove": "Buang", + "Amount in RAND": "Amaun dalam RAND", + "Intended payments": "Pembayaran yang dimaksudkan", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Masukkan setiap pembayaran yang anda ingin buat: alamat dan amaun dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan sekali, dengan jumlahnya. Output yang tidak sepadan dengan mana-mana tidak boleh ditandatangani.", + "Fee: %@ RAND · expires at height %@": "Yuran: %@ RAND · tamat tempoh pada ketinggian %@", + "%@ of %@ payments matched.": "%@ daripada %@ pembayaran sepadan.", + "Retry": "Cuba lagi", + "%@ blocks left to prove it.": "Tinggal %@ blok lagi untuk membuktikannya.", + "Payments changed: check them again before signing.": "Pembayaran berubah: semak semula sebelum menandatangani.", + "Signing…": "Menandatangani…", + "Sign": "Tandatangan", + "Type the last four characters of the account fingerprint": "Taip empat aksara terakhir cap jari akaun", + "Save as file": "Simpan sebagai fail", + "Sign a multisig proposal": "Tandatangani cadangan multisig", + "this signer is already saved as %@": "penandatangan ini sudah disimpan sebagai %@", + "a signer label is 1-64 characters": "label penandatangan ialah 1-64 aksara", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Tetapkan kod laluan peranti dahulu: kunci penandatangan hanya dibaca selepas Face ID, Touch ID atau kod laluan.", + "No such signer profile.": "Tiada profil penandatangan sedemikian.", + "This signer profile has no key on this device: remove it and add it again.": "Profil penandatangan ini tiada kunci pada peranti ini: buangnya dan tambah semula.", + "No signer profiles yet": "Belum ada profil penandatangan", + "Add the policy file and your signer key to sign for a private multisig account.": "Tambah fail dasar dan kunci penandatangan anda untuk menandatangani bagi akaun multisig persendirian.", + "Remove this signer": "Buang penandatangan ini", + "Remove a multisig signer": "Buang penandatangan multisig", + "Add a signer": "Tambah penandatangan", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Profil penandatangan disimpan dalam Keychain telefon ini. Setiap tandatangan meminta Face ID, Touch ID atau kod laluan anda sekali lagi.", + "Policy file": "Fail dasar", + "Signer key file, or its 64-hex seed": "Fail kunci penandatangan, atau seed 64-hex-nya", + "Account fingerprint — compare it with every other signer before you save": "Cap jari akaun — bandingkan dengan setiap penandatangan lain sebelum menyimpan", + "Label": "Label", + "Check": "Semak", + "%@ of %@ · this key is signer %@": "%@ daripada %@ · kunci ini ialah penandatangan %@", + "Save signer": "Simpan penandatangan", + "That file could not be read.": "Fail itu tidak dapat dibaca.", + "Remove %@?": "Buang %@?", + "You need the signer's key file to add this signer back.": "Anda memerlukan fail kunci penandatangan untuk menambahnya semula.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Penerima ini menerima RAND dan juga token: pembayaran RAND dan token kepada satu penerima memerlukan cadangan berasingan." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/pl.json b/ios/l10n/pl.json index bd54afa..f93ada0 100644 --- a/ios/l10n/pl.json +++ b/ios/l10n/pl.json @@ -455,7 +455,99 @@ "unknown parameter %@": "nieznany parametr %@", "wallet is locked": "portfel jest zablokowany", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "skrót transakcji nie jest powiązaniem propozycji", + "the proposal's signing_message is not its binding's": "signing_message propozycji nie jest signing_message jej powiązania", + "the proposal's auth_commit is not this policy's account": "auth_commit propozycji nie jest kontem tej polityki", + "the proposal's expected digest is not its bundle's": "oczekiwany digest propozycji nie jest digestem jej bundle", + "the transaction carries no bundle": "transakcja nie zawiera bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transakcja zawiera akcję; prywatna propozycja multisig to płatność wyłącznie z jej bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "wartości witness nie można sprawdzić: jego nk lub salt nie pochodzi z tej propozycji", + "this key is not one of the policy's signers": "ten klucz nie należy do sygnatariuszy polityki", + "a share is index:hex": "udział podpisu ma postać index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "klucz sygnatariusza to plik keygen rand-node lub 64 znaki szesnastkowe", + "a signer seed is 64 hex characters": "seed sygnatariusza to 64 znaki szesnastkowe", + "expected_recipients must be a list of addresses": "expected_recipients musi być listą adresów", + "the key file's public_key is not the one its seed derives": "public_key z pliku klucza nie jest tym, który wyprowadza jego seed", + "the key file's address is not the one its seed derives": "adres z pliku klucza nie jest tym, który wyprowadza jego seed", + "no expected recipient given: the payee was NOT checked": "nie podano oczekiwanego odbiorcy: odbiorca NIE został sprawdzony", + "this proposal is for another policy: it names %@, this policy is %@": "ta propozycja dotyczy innej polityki: wskazuje %@, ta polityka to %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "wyjście %@ nie otwiera się jako należące do tego konta: odmowa podpisania wartości, której nie widzi", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "wartość witness nie zgadza się z transakcją: wejście %@ nie daje nullifiera bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "wartość witness nie zgadza się z wyjściami: aktywo %@ ma %@ na wejściu i %@ na wyjściu z opłatą i spaleniami", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "propozycja wygasa na %@, poza oknem swojego bundle (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "transakcja jest dla łańcucha %@, propozycja wskazuje łańcuch %@", + "signer %@'s signature does not verify over this proposal": "podpis sygnatariusza %@ nie jest poprawny dla tej propozycji", + "signer %@ is not in this policy (it has %@ signers)": "sygnatariusza %@ nie ma w tej polityce (ma ona %@ sygnatariuszy)", + "expected recipient %@: %@": "oczekiwany odbiorca %@: %@", + "the policy does not parse: %@": "nie można odczytać polityki: %@", + "the proposal does not parse: %@": "nie można odczytać propozycji: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Sprawdzenie propozycji zwróciło niepełną odpowiedź, więc ta propozycja nie zostaje podpisana.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Wpisz adres każdego odbiorcy: wyjścia niepasującego do wpisanego adresu lub mającego pusty adres nie można podpisać.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Żaden węzeł nie odpowiedział pod %@, więc nie można sprawdzić wygaśnięcia i ta propozycja nie zostaje podpisana. Spróbuj ponownie, gdy węzeł będzie osiągalny.", + "This proposal is for chain %@; this wallet is on chain %@.": "Ta propozycja jest dla łańcucha %@; ten portfel jest w łańcuchu %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Ta propozycja jest związana z innym genesis niż ten, który obsługuje węzeł tego portfela.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Ta propozycja wygasa na wysokości %@, a łańcuch jest na %@: za późno na podpis. Poproś o nową propozycję.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Nie udało się usunąć kluczy sygnatariusza, więc portfel nie został zapomniany. Spróbuj ponownie.", + "Manage multisig signers": "Zarządzaj sygnatariuszami multisig", + "Multisig signers": "Sygnatariusze multisig", + "OK": "OK", + "Enter the address of every payment.": "Wpisz adres każdej płatności.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Wpisz kwotę każdej płatności: to ona chroni przed podziałem lub zawyżoną płatnością.", + "An amount is a number of RAND, such as 1.5.": "Kwota to liczba RAND, na przykład 1.5.", + "Sign a proposal": "Podpisz propozycję", + "Proposal (proposal.json)": "Propozycja (proposal.json)", + "Check a share": "Sprawdź udział podpisu", + "%@ of %@ · signer %@": "%@ z %@ · sygnatariusz %@", + "Open file": "Otwórz plik", + "Checking…": "Sprawdzanie…", + "Check proposal": "Sprawdź propozycję", + "What this proposal does, as checked against your policy": "Co robi ta propozycja, sprawdzone względem Twojej polityki", + "Your share — send it to whoever proves the proposal": "Twój udział podpisu — wyślij go temu, kto dowodzi propozycji", + "index:hex": "index:hex", + "Check share": "Sprawdź udział podpisu", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "Udział podpisu sygnatariusza %@ jest poprawny. Masz %@ z %@: sygnatariusze %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Ten plik jest większy, niż może być propozycja, polityka lub plik klucza.", + "Add payment": "Dodaj płatność", + "Check payments": "Sprawdź płatności", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Kwot nie można sprawdzić dla płatności innych niż w RAND: zostaw dla nich kwotę pustą.", + "Payee address": "Adres odbiorcy", + "Remove": "Usuń", + "Amount in RAND": "Kwota w RAND", + "Intended payments": "Zamierzone płatności", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Wpisz każdą płatność, którą zamierzasz wykonać: adres i kwotę w RAND. Płatność dwa razy na ten sam adres wpisuje się raz, z sumą. Wyjścia niepasującego do żadnej z nich nie można podpisać.", + "Fee: %@ RAND · expires at height %@": "Opłata: %@ RAND · wygasa na wysokości %@", + "%@ of %@ payments matched.": "Zgodnych płatności: %@ z %@.", + "Retry": "Ponów", + "%@ blocks left to prove it.": "Zostało %@ bloków, aby to udowodnić.", + "Payments changed: check them again before signing.": "Płatności się zmieniły: sprawdź je ponownie przed podpisaniem.", + "Signing…": "Podpisywanie…", + "Sign": "Podpisz", + "Type the last four characters of the account fingerprint": "Wpisz ostatnie cztery znaki odcisku konta", + "Save as file": "Zapisz jako plik", + "Sign a multisig proposal": "Podpisz propozycję multisig", + "this signer is already saved as %@": "ten sygnatariusz jest już zapisany jako %@", + "a signer label is 1-64 characters": "etykieta sygnatariusza ma 1-64 znaki", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Najpierw ustaw kod urządzenia: klucz sygnatariusza jest odczytywany dopiero po Face ID, Touch ID lub kodzie.", + "No such signer profile.": "Nie ma takiego profilu sygnatariusza.", + "This signer profile has no key on this device: remove it and add it again.": "Ten profil sygnatariusza nie ma klucza na tym urządzeniu: usuń go i dodaj ponownie.", + "No signer profiles yet": "Brak profili sygnatariuszy", + "Add the policy file and your signer key to sign for a private multisig account.": "Dodaj plik polityki i swój klucz sygnatariusza, aby podpisywać dla prywatnego konta multisig.", + "Remove this signer": "Usuń tego sygnatariusza", + "Remove a multisig signer": "Usuń sygnatariusza multisig", + "Add a signer": "Dodaj sygnatariusza", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Profil sygnatariusza jest przechowywany w pęku kluczy (Keychain) tego telefonu. Każdy podpis ponownie wymaga Face ID, Touch ID lub kodu.", + "Policy file": "Plik polityki", + "Signer key file, or its 64-hex seed": "Plik klucza sygnatariusza lub jego seed w 64 znakach hex", + "Account fingerprint — compare it with every other signer before you save": "Odcisk konta — porównaj go z odciskiem każdego innego sygnatariusza, zanim zapiszesz", + "Label": "Etykieta", + "Check": "Sprawdź", + "%@ of %@ · this key is signer %@": "%@ z %@ · ten klucz to sygnatariusz %@", + "Save signer": "Zapisz sygnatariusza", + "That file could not be read.": "Nie udało się odczytać tego pliku.", + "Remove %@?": "Usunąć %@?", + "You need the signer's key file to add this signer back.": "Aby ponownie dodać tego sygnatariusza, potrzebujesz jego pliku klucza.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Ten odbiorca otrzymuje zarówno RAND, jak i token: płatności w RAND i w tokenach do jednego odbiorcy wymagają osobnych propozycji." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ps.json b/ios/l10n/ps.json index 4de681e..07ec332 100644 --- a/ios/l10n/ps.json +++ b/ios/l10n/ps.json @@ -446,7 +446,99 @@ "unknown parameter %@": "ناپېژندل شوی پارامیټر %@", "wallet is locked": "والټ قلف دی", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "د راکړې ورکړې hash د وړاندیز binding نه دی", + "the proposal's signing_message is not its binding's": "د وړاندیز signing_message د هغه د binding نه دی", + "the proposal's auth_commit is not this policy's account": "د وړاندیز auth_commit د دې پالیسۍ حساب نه دی", + "the proposal's expected digest is not its bundle's": "د وړاندیز تمه شوی digest د هغه د bundle نه دی", + "the transaction carries no bundle": "راکړه ورکړه هیڅ bundle نه لري", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "راکړه ورکړه یو عمل لري؛ خصوصي ملټي‌سېګ وړاندیز یوازې د خپل bundle څخه تادیه ده", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "د witness ارزښت نشي څېړل کېدای: د هغه nk یا salt د دې وړاندیز نه دی", + "this key is not one of the policy's signers": "دا کیلي د پالیسۍ له لاسلیک کوونکو څخه نه ده", + "a share is index:hex": "د لاسلیک برخه index:hex ده", + "a signer key is a rand-node keygen file or 64 hex characters": "د لاسلیک کوونکي کیلي د rand-node keygen فایل یا ۶۴ هېکس توري دي", + "a signer seed is 64 hex characters": "د لاسلیک کوونکي seed ۶۴ هېکس توري دي", + "expected_recipients must be a list of addresses": "expected_recipients باید د پتو لیست وي", + "the key file's public_key is not the one its seed derives": "د کیلي فایل public_key هغه نه دی چې seed یې راباسي", + "the key file's address is not the one its seed derives": "د کیلي فایل پته هغه نه ده چې seed یې راباسي", + "no expected recipient given: the payee was NOT checked": "هیڅ تمه شوی ترلاسه کوونکی نه دی ورکړل شوی: ترلاسه کوونکی ونه څېړل شو (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "دا وړاندیز د بلې پالیسۍ لپاره دی: %@ یادوي، دا پالیسي %@ ده", + "output %@ does not open as this account's: refusing to sign value it cannot see": "محصول %@ د دې حساب په توګه نه خلاصېږي: د هغه ارزښت لاسلیک ردېږي چې نشي لیدل کېدای", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "د witness ارزښت له راکړې ورکړې سره نه برابرېږي: ننوتنه %@ د bundle nullifier ته نه رسېږي", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "د witness ارزښت له محصولاتو سره نه برابرېږي: شتمنۍ %@ %@ راتلونکی او %@ وتلونکی لري، له فیس او سوځولو سره", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "وړاندیز په %@ کې پای ته رسېږي، د خپل bundle له کړکۍ (%@, %@] بهر", + "the transaction is for chain %@, the proposal says chain %@": "راکړه ورکړه د زنځیر %@ لپاره ده، وړاندیز زنځیر %@ وايي", + "signer %@'s signature does not verify over this proposal": "د لاسلیک کوونکي %@ لاسلیک د دې وړاندیز لپاره سم نه دی", + "signer %@ is not in this policy (it has %@ signers)": "لاسلیک کوونکی %@ په دې پالیسۍ کې نشته (دا %@ لاسلیک کوونکي لري)", + "expected recipient %@: %@": "تمه شوی ترلاسه کوونکی %@: %@", + "the policy does not parse: %@": "پالیسي نشي تحلیل کېدای: %@", + "the proposal does not parse: %@": "وړاندیز نشي تحلیل کېدای: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "د وړاندیز څېړنې ناقص ځواب راستون کړ، نو دا وړاندیز نه لاسلیک کېږي.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "د هر تادیه ترلاسه کوونکي پته ولیکئ: هغه محصول چې له لیکل شوې پتې سره نه برابرېږي، یا پته یې خالي ده، لاسلیک نشي کېدای.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "په %@ کې هیڅ نوډ ځواب نه کړ، نو پای ته رسېدل نشي څېړل کېدای او دا وړاندیز نه لاسلیک کېږي. کله چې نوډ ترلاسه وي بیا هڅه وکړئ.", + "This proposal is for chain %@; this wallet is on chain %@.": "دا وړاندیز د زنځیر %@ لپاره دی؛ دا والټ په زنځیر %@ کې دی.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "دا وړاندیز له هغه genesis پرته بل ته تړل شوی چې د دې والټ نوډ یې وړاندې کوي.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "دا وړاندیز په %@ لوړوالي کې پای ته رسېږي او زنځیر په %@ کې دی: د لاسلیک لپاره ډېر ناوخته دی. نوی وړاندیز وغواړئ.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "د لاسلیک کوونکي کلیګانې لرې نشوې، نو بټوه هېره نشوه. بیا هڅه وکړئ.", + "Manage multisig signers": "د ملټي‌سېګ لاسلیک کوونکي اداره کړئ", + "Multisig signers": "د ملټي‌سېګ لاسلیک کوونکي", + "OK": "سمه ده", + "Enter the address of every payment.": "د هرې تادیې پته ولیکئ.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "د هرې تادیې اندازه ولیکئ: همدا هغه څه دي چې ویش یا پړسول شوې تادیه ځنډوي.", + "An amount is a number of RAND, such as 1.5.": "اندازه د RAND شمېره ده، لکه 1.5.", + "Sign a proposal": "وړاندیز لاسلیک کړئ", + "Proposal (proposal.json)": "وړاندیز (proposal.json)", + "Check a share": "د لاسلیک برخه وڅېړئ", + "%@ of %@ · signer %@": "د %2$@ څخه %1$@ · لاسلیک کوونکی %3$@", + "Open file": "فایل خلاص کړئ", + "Checking…": "څېړل کېږي…", + "Check proposal": "وړاندیز وڅېړئ", + "What this proposal does, as checked against your policy": "دا وړاندیز څه کوي، ستاسو د پالیسۍ پر وړاندې څېړل شوی", + "Your share — send it to whoever proves the proposal": "ستاسو د لاسلیک برخه — هغه چا ته یې ولېږئ چې وړاندیز ثابتوي", + "index:hex": "index:hex", + "Check share": "د لاسلیک برخه وڅېړئ", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "د لاسلیک کوونکي %1$@ د لاسلیک برخه سمه ده. د %3$@ څخه %2$@ په لاس کې دي: لاسلیک کوونکي %4$@.", + "That file is larger than a proposal, a policy or a key file can be.": "دا فایل له هغه څخه لوی دی چې یو وړاندیز، پالیسي یا د کیلي فایل کېدای شي.", + "Add payment": "تادیه زیاته کړئ", + "Check payments": "تادیات وڅېړئ", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "د غیر RAND تادیاتو اندازه نشي څېړل کېدای: د هغوی لپاره اندازه خالي پرېږدئ.", + "Payee address": "د ترلاسه کوونکي پته", + "Remove": "لرې یې کړئ", + "Amount in RAND": "په RAND کې اندازه", + "Intended payments": "موخه شوې تادیات", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "هره تادیه چې کول یې غواړئ ولیکئ: پته او په RAND کې اندازه. هماغې پتې ته دوه ځله تادیه یو ځل د ټولیز مجموعې سره لیکل کېږي. هغه محصول چې له هیڅ یوې سره نه برابرېږي لاسلیک نشي کېدای.", + "Fee: %@ RAND · expires at height %@": "فیس: %@ RAND · په %@ لوړوالي کې پای ته رسېږي", + "%@ of %@ payments matched.": "د %2$@ تادیاتو څخه %1$@ سره برابر شول.", + "Retry": "بیا هڅه وکړئ", + "%@ blocks left to prove it.": "د ثبوت لپاره %@ بلاکونه پاتې دي.", + "Payments changed: check them again before signing.": "تادیات بدل شول: مخکې له لاسلیک بیا یې وڅېړئ.", + "Signing…": "لاسلیک کېږي…", + "Sign": "لاسلیک", + "Type the last four characters of the account fingerprint": "د حساب د ګوتنښې وروستي څلور توري ولیکئ", + "Save as file": "د فایل په توګه خوندي کړئ", + "Sign a multisig proposal": "د ملټي‌سېګ وړاندیز لاسلیک کړئ", + "this signer is already saved as %@": "دا لاسلیک کوونکی لا دمخه د %@ په نامه خوندي دی", + "a signer label is 1-64 characters": "د لاسلیک کوونکي نښه ۱-۶۴ توري ده", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "لومړی د وسیلې پاس کوډ وټاکئ: د لاسلیک کوونکي کلیځ یوازې د Face ID، Touch ID یا پاس کوډ وروسته لوستل کېږي.", + "No such signer profile.": "داسې د لاسلیک کوونکي پروفایل نشته.", + "This signer profile has no key on this device: remove it and add it again.": "دا د لاسلیک کوونکي پروفایل په دې وسیله کې کیلي نه لري: لرې یې کړئ او بیا یې زیات کړئ.", + "No signer profiles yet": "تر اوسه د لاسلیک کوونکي پروفایلونه نشته", + "Add the policy file and your signer key to sign for a private multisig account.": "د خصوصي ملټي‌سېګ حساب لپاره د لاسلیک کولو لپاره د پالیسۍ فایل او خپله د لاسلیک کوونکي کیلي زیاته کړئ.", + "Remove this signer": "دا لاسلیک کوونکی لرې کړئ", + "Remove a multisig signer": "د ملټي‌سېګ لاسلیک کوونکی لرې کړئ", + "Add a signer": "لاسلیک کوونکی زیات کړئ", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "د لاسلیک کوونکي پروفایل د دې تلیفون په Keychain کې ساتل کېږي. هر لاسلیک بیا Face ID، Touch ID یا ستاسو پاس کوډ غواړي.", + "Policy file": "د پالیسۍ فایل", + "Signer key file, or its 64-hex seed": "د لاسلیک کوونکي د کیلي فایل، یا د هغه ۶۴ هېکس seed", + "Account fingerprint — compare it with every other signer before you save": "د حساب ګوتنښه — مخکې له خوندي کولو یې له هر بل لاسلیک کوونکي سره پرتله کړئ", + "Label": "نښه", + "Check": "وڅېړئ", + "%@ of %@ · this key is signer %@": "د %2$@ څخه %1$@ · دا کیلي لاسلیک کوونکی %3$@ دی", + "Save signer": "لاسلیک کوونکی خوندي کړئ", + "That file could not be read.": "دا فایل ونه لوستل شو.", + "Remove %@?": "%@ لرې کړئ؟", + "You need the signer's key file to add this signer back.": "د دې لاسلیک کوونکي د بیا زیاتولو لپاره ستاسو د هغه د کلیځ فایل ته اړتیا ده.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "دا ترلاسه کوونکی RAND او یو ټوکن دواړه ترلاسه کوي: یو ترلاسه کوونکي ته د RAND او ټوکن تادیات جلا وړاندیزونو ته اړتیا لري." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/pt-BR.json b/ios/l10n/pt-BR.json index 1db4c20..5522abd 100644 --- a/ios/l10n/pt-BR.json +++ b/ios/l10n/pt-BR.json @@ -437,7 +437,99 @@ "unknown parameter %@": "parâmetro desconhecido %@", "wallet is locked": "a carteira está bloqueada", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "o hash da transação não é o vínculo da proposta", + "the proposal's signing_message is not its binding's": "o signing_message da proposta não é o do seu vínculo", + "the proposal's auth_commit is not this policy's account": "o auth_commit da proposta não é a conta desta política", + "the proposal's expected digest is not its bundle's": "o digest esperado da proposta não é o do seu bundle", + "the transaction carries no bundle": "a transação não carrega nenhum bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "a transação carrega uma ação; uma proposta multiassinatura privada é um pagamento apenas do seu bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "o valor do witness não pode ser verificado: seu nk ou salt não é o desta proposta", + "this key is not one of the policy's signers": "esta chave não é de nenhum dos signatários da política", + "a share is index:hex": "uma parte da assinatura é index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "uma chave de signatário é um arquivo keygen do rand-node ou 64 caracteres hexadecimais", + "a signer seed is 64 hex characters": "uma seed de signatário tem 64 caracteres hexadecimais", + "expected_recipients must be a list of addresses": "expected_recipients deve ser uma lista de endereços", + "the key file's public_key is not the one its seed derives": "o public_key do arquivo de chave não é o que sua seed deriva", + "the key file's address is not the one its seed derives": "o endereço do arquivo de chave não é o que sua seed deriva", + "no expected recipient given: the payee was NOT checked": "nenhum destinatário esperado informado: o beneficiário NÃO foi verificado", + "this proposal is for another policy: it names %@, this policy is %@": "esta proposta é de outra política: nomeia %@, esta política é %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "a saída %@ não abre como desta conta: recusa-se a assinar um valor que não consegue ver", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "o valor do witness não confere com a transação: a entrada %@ não gera o nullifier do bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "o valor do witness não confere com as saídas: o ativo %@ tem %@ de entrada e %@ de saída com a taxa e as queimas", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "a proposta expira em %@, fora da janela do seu bundle (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "a transação é para a cadeia %@, a proposta diz a cadeia %@", + "signer %@'s signature does not verify over this proposal": "a assinatura do signatário %@ não é válida para esta proposta", + "signer %@ is not in this policy (it has %@ signers)": "o signatário %@ não está nesta política (ela tem %@ signatários)", + "expected recipient %@: %@": "destinatário esperado %@: %@", + "the policy does not parse: %@": "a política não pôde ser lida: %@", + "the proposal does not parse: %@": "a proposta não pôde ser lida: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "A verificação da proposta devolveu uma resposta incompleta, então esta proposta não é assinada.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Informe o endereço de cada beneficiário: uma saída que não corresponde ao endereço informado, ou cujo endereço está vazio, não pode ser assinada.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Nenhum nó respondeu em %@, então a expiração não pode ser verificada e esta proposta não é assinada. Tente de novo quando o nó estiver acessível.", + "This proposal is for chain %@; this wallet is on chain %@.": "Esta proposta é para a cadeia %@; esta carteira está na cadeia %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Esta proposta está vinculada a um genesis diferente do que o nó desta carteira serve.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Esta proposta expira na altura %@ e a cadeia está em %@: tarde demais para assinar. Peça uma nova proposta.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Não foi possível remover as chaves de signatário, então a carteira não foi esquecida. Tente de novo.", + "Manage multisig signers": "Gerenciar signatários multiassinatura", + "Multisig signers": "Signatários multiassinatura", + "OK": "OK", + "Enter the address of every payment.": "Informe o endereço de cada pagamento.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Informe o valor de cada pagamento: é o que impede uma divisão ou um pagamento inflado.", + "An amount is a number of RAND, such as 1.5.": "Um valor é um número de RAND, como 1.5.", + "Sign a proposal": "Assinar uma proposta", + "Proposal (proposal.json)": "Proposta (proposal.json)", + "Check a share": "Verificar uma parte da assinatura", + "%@ of %@ · signer %@": "%@ de %@ · signatário %@", + "Open file": "Abrir arquivo", + "Checking…": "Verificando…", + "Check proposal": "Verificar proposta", + "What this proposal does, as checked against your policy": "O que esta proposta faz, verificado contra a sua política", + "Your share — send it to whoever proves the proposal": "Sua parte da assinatura — envie-a a quem prova a proposta", + "index:hex": "index:hex", + "Check share": "Verificar parte da assinatura", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "A parte da assinatura do signatário %@ é válida. %@ de %@ em mãos: signatários %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Esse arquivo é maior do que uma proposta, uma política ou um arquivo de chave pode ser.", + "Add payment": "Adicionar pagamento", + "Check payments": "Verificar pagamentos", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Os valores não podem ser verificados em pagamentos que não são em RAND: deixe o valor vazio nesses.", + "Payee address": "Endereço do beneficiário", + "Remove": "Remover", + "Amount in RAND": "Valor em RAND", + "Intended payments": "Pagamentos pretendidos", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Informe cada pagamento que você pretende fazer: o endereço e o valor em RAND. Um pagamento ao mesmo endereço duas vezes é informado uma vez só, com o total. Uma saída que não corresponde a nenhum não pode ser assinada.", + "Fee: %@ RAND · expires at height %@": "Taxa: %@ RAND · expira na altura %@", + "%@ of %@ payments matched.": "%@ de %@ pagamentos conferem.", + "Retry": "Tentar de novo", + "%@ blocks left to prove it.": "Faltam %@ blocos para prová-la.", + "Payments changed: check them again before signing.": "Os pagamentos mudaram: verifique-os de novo antes de assinar.", + "Signing…": "Assinando…", + "Sign": "Assinar", + "Type the last four characters of the account fingerprint": "Digite os últimos quatro caracteres da impressão digital da conta", + "Save as file": "Salvar como arquivo", + "Sign a multisig proposal": "Assinar uma proposta multiassinatura", + "this signer is already saved as %@": "este signatário já está salvo como %@", + "a signer label is 1-64 characters": "o rótulo de um signatário tem 1-64 caracteres", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Defina primeiro um código no dispositivo: a chave de signatário só é lida depois do Face ID, do Touch ID ou do código.", + "No such signer profile.": "Não existe esse perfil de signatário.", + "This signer profile has no key on this device: remove it and add it again.": "Este perfil de signatário não tem chave neste dispositivo: remova-o e adicione-o de novo.", + "No signer profiles yet": "Ainda não há perfis de signatário", + "Add the policy file and your signer key to sign for a private multisig account.": "Adicione o arquivo de política e sua chave de signatário para assinar por uma conta multiassinatura privada.", + "Remove this signer": "Remover este signatário", + "Remove a multisig signer": "Remover um signatário multiassinatura", + "Add a signer": "Adicionar um signatário", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Um perfil de signatário fica guardado no chaveiro (Keychain) deste telefone. Cada assinatura pede Face ID, Touch ID ou seu código de novo.", + "Policy file": "Arquivo de política", + "Signer key file, or its 64-hex seed": "Arquivo de chave do signatário, ou sua seed de 64 hex", + "Account fingerprint — compare it with every other signer before you save": "Impressão digital da conta — compare-a com a de cada outro signatário antes de salvar", + "Label": "Rótulo", + "Check": "Verificar", + "%@ of %@ · this key is signer %@": "%@ de %@ · esta chave é o signatário %@", + "Save signer": "Salvar signatário", + "That file could not be read.": "Não foi possível ler esse arquivo.", + "Remove %@?": "Remover %@?", + "You need the signer's key file to add this signer back.": "Você precisa do arquivo de chave do signatário para adicioná-lo de novo.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Este beneficiário recebe RAND e também um token: pagamentos em RAND e em tokens a um mesmo beneficiário precisam de propostas separadas." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ru.json b/ios/l10n/ru.json index 686cceb..75ac40e 100644 --- a/ios/l10n/ru.json +++ b/ios/l10n/ru.json @@ -440,7 +440,99 @@ "unknown parameter %@": "неизвестный параметр %@", "wallet is locked": "кошелёк заблокирован", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "хеш транзакции — не привязка предложения", + "the proposal's signing_message is not its binding's": "signing_message предложения — не signing_message его привязки", + "the proposal's auth_commit is not this policy's account": "auth_commit предложения — не аккаунт этой политики", + "the proposal's expected digest is not its bundle's": "ожидаемый digest предложения — не digest его bundle", + "the transaction carries no bundle": "транзакция не несёт bundle", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "транзакция несёт действие; приватное мультиподписное предложение — это платёж только из его bundle", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "значение witness проверить нельзя: его nk или salt — не от этого предложения", + "this key is not one of the policy's signers": "этот ключ не входит в число подписантов политики", + "a share is index:hex": "доля подписи имеет вид index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "ключ подписанта — это файл keygen от rand-node или 64 шестнадцатеричных символа", + "a signer seed is 64 hex characters": "seed подписанта — 64 шестнадцатеричных символа", + "expected_recipients must be a list of addresses": "expected_recipients должен быть списком адресов", + "the key file's public_key is not the one its seed derives": "public_key в файле ключа не тот, что выводится из его seed", + "the key file's address is not the one its seed derives": "адрес в файле ключа не тот, что выводится из его seed", + "no expected recipient given: the payee was NOT checked": "ожидаемый получатель не указан: получатель НЕ проверялся", + "this proposal is for another policy: it names %@, this policy is %@": "это предложение для другой политики: в нём указано %@, эта политика — %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "выход %@ не открывается как принадлежащий этому аккаунту: отказ подписывать значение, которое нельзя увидеть", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "значение witness не совпадает с транзакцией: вход %@ не даёт nullifier bundle", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "значение witness не совпадает с выходами: у актива %@ на входе %@, на выходе %@ с комиссией и сжиганиями", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "предложение истекает на %@, вне окна его bundle (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "транзакция предназначена для сети %@, а предложение указывает сеть %@", + "signer %@'s signature does not verify over this proposal": "подпись подписанта %@ не верна для этого предложения", + "signer %@ is not in this policy (it has %@ signers)": "подписанта %@ нет в этой политике (в ней %@ подписантов)", + "expected recipient %@: %@": "ожидаемый получатель %@: %@", + "the policy does not parse: %@": "политику не удалось разобрать: %@", + "the proposal does not parse: %@": "предложение не удалось разобрать: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Проверка предложения вернула неполный ответ, поэтому это предложение не подписано.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Введите адрес каждого получателя: выход, не совпадающий с введённым адресом или с пустым адресом, подписать нельзя.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Ни один узел не ответил по адресу %@, поэтому срок действия проверить нельзя, и это предложение не подписано. Повторите, когда узел станет доступен.", + "This proposal is for chain %@; this wallet is on chain %@.": "Это предложение для сети %@; этот кошелёк в сети %@.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Это предложение привязано к другому genesis, чем тот, который обслуживает узел этого кошелька.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "Это предложение истекает на высоте %@, а сеть на %@: подписывать поздно. Запросите новое предложение.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "Не удалось удалить ключи подписанта, поэтому кошелёк не был забыт. Попробуйте снова.", + "Manage multisig signers": "Управление подписантами мультиподписи", + "Multisig signers": "Подписанты мультиподписи", + "OK": "OK", + "Enter the address of every payment.": "Введите адрес каждого платежа.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Введите сумму каждого платежа: именно она не даёт разбить платёж или завысить его.", + "An amount is a number of RAND, such as 1.5.": "Сумма — это число RAND, например 1.5.", + "Sign a proposal": "Подписать предложение", + "Proposal (proposal.json)": "Предложение (proposal.json)", + "Check a share": "Проверить долю подписи", + "%@ of %@ · signer %@": "%@ из %@ · подписант %@", + "Open file": "Открыть файл", + "Checking…": "Проверка…", + "Check proposal": "Проверить предложение", + "What this proposal does, as checked against your policy": "Что делает это предложение — проверено по вашей политике", + "Your share — send it to whoever proves the proposal": "Ваша доля подписи — отправьте её тому, кто доказывает предложение", + "index:hex": "index:hex", + "Check share": "Проверить долю подписи", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "Доля подписи подписанта %@ верна. Собрано %@ из %@: подписанты %@.", + "That file is larger than a proposal, a policy or a key file can be.": "Этот файл больше, чем может быть предложение, политика или файл ключа.", + "Add payment": "Добавить платёж", + "Check payments": "Проверить платежи", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Суммы нельзя проверить для платежей не в RAND: оставьте для них сумму пустой.", + "Payee address": "Адрес получателя", + "Remove": "Удалить", + "Amount in RAND": "Сумма в RAND", + "Intended payments": "Предполагаемые платежи", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Введите каждый платёж, который вы собираетесь сделать: адрес и сумму в RAND. Платёж на один и тот же адрес дважды вводится один раз, с итоговой суммой. Выход, не совпадающий ни с одним из них, подписать нельзя.", + "Fee: %@ RAND · expires at height %@": "Комиссия: %@ RAND · истекает на высоте %@", + "%@ of %@ payments matched.": "Совпало платежей: %@ из %@.", + "Retry": "Повторить", + "%@ blocks left to prove it.": "Осталось %@ блоков, чтобы доказать это.", + "Payments changed: check them again before signing.": "Платежи изменились: проверьте их снова перед подписью.", + "Signing…": "Подписание…", + "Sign": "Подписать", + "Type the last four characters of the account fingerprint": "Введите последние четыре символа отпечатка аккаунта", + "Save as file": "Сохранить как файл", + "Sign a multisig proposal": "Подписать предложение мультиподписи", + "this signer is already saved as %@": "этот подписант уже сохранён как %@", + "a signer label is 1-64 characters": "метка подписанта — 1-64 символа", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "Сначала задайте код-пароль устройства: ключ подписанта читается только после Face ID, Touch ID или код-пароля.", + "No such signer profile.": "Такого профиля подписанта нет.", + "This signer profile has no key on this device: remove it and add it again.": "У этого профиля подписанта нет ключа на этом устройстве: удалите его и добавьте снова.", + "No signer profiles yet": "Профилей подписантов пока нет", + "Add the policy file and your signer key to sign for a private multisig account.": "Добавьте файл политики и ключ подписанта, чтобы подписывать за приватный мультиподписной аккаунт.", + "Remove this signer": "Удалить этого подписанта", + "Remove a multisig signer": "Удалить подписанта мультиподписи", + "Add a signer": "Добавить подписанта", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "Профиль подписанта хранится в связке ключей (Keychain) этого телефона. Каждая подпись снова запрашивает Face ID, Touch ID или ваш код-пароль.", + "Policy file": "Файл политики", + "Signer key file, or its 64-hex seed": "Файл ключа подписанта или его seed из 64 hex-символов", + "Account fingerprint — compare it with every other signer before you save": "Отпечаток аккаунта — сверьте его с каждым другим подписантом, прежде чем сохранять", + "Label": "Метка", + "Check": "Проверить", + "%@ of %@ · this key is signer %@": "%@ из %@ · этот ключ — подписант %@", + "Save signer": "Сохранить подписанта", + "That file could not be read.": "Не удалось прочитать этот файл.", + "Remove %@?": "Удалить %@?", + "You need the signer's key file to add this signer back.": "Чтобы снова добавить этого подписанта, нужен его файл ключа.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Этот получатель получает и RAND, и токен: платежи в RAND и в токенах одному получателю требуют отдельных предложений." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ta.json b/ios/l10n/ta.json index 65ad3cc..298daab 100644 --- a/ios/l10n/ta.json +++ b/ios/l10n/ta.json @@ -443,7 +443,99 @@ "unknown parameter %@": "அறியப்படாத அளவுரு %@", "wallet is locked": "வாலட் பூட்டப்பட்டுள்ளது", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "பரிவர்த்தனையின் ஹாஷ் முன்மொழிவின் binding அல்ல", + "the proposal's signing_message is not its binding's": "முன்மொழிவின் signing_message அதன் binding-இனுடையது அல்ல", + "the proposal's auth_commit is not this policy's account": "முன்மொழிவின் auth_commit இந்தக் கொள்கையின் கணக்கு அல்ல", + "the proposal's expected digest is not its bundle's": "முன்மொழிவின் எதிர்பார்க்கப்படும் digest அதன் bundle-இனுடையது அல்ல", + "the transaction carries no bundle": "பரிவர்த்தனையில் bundle எதுவும் இல்லை", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "பரிவர்த்தனை ஒரு செயலைக் கொண்டுள்ளது; தனிப்பட்ட மல்டிசிக் முன்மொழிவு என்பது அதன் bundle-இலிருந்து மட்டும் செய்யும் கட்டணம்", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "witness-இன் மதிப்பைச் சரிபார்க்க முடியவில்லை: அதன் nk அல்லது salt இந்த முன்மொழிவினுடையது அல்ல", + "this key is not one of the policy's signers": "இந்த விசை கொள்கையின் கையொப்பமிடுபவர்களில் ஒன்றல்ல", + "a share is index:hex": "கையொப்பப் பங்கு index:hex வடிவில் இருக்கும்", + "a signer key is a rand-node keygen file or 64 hex characters": "கையொப்பமிடுபவர் விசை என்பது rand-node keygen கோப்பு அல்லது 64 ஹெக்ஸ் எழுத்துகள்", + "a signer seed is 64 hex characters": "கையொப்பமிடுபவர் சீட் 64 ஹெக்ஸ் எழுத்துகள்", + "expected_recipients must be a list of addresses": "expected_recipients முகவரிகளின் பட்டியலாக இருக்க வேண்டும்", + "the key file's public_key is not the one its seed derives": "விசைக் கோப்பின் public_key அதன் சீட் வருவிப்பதல்ல", + "the key file's address is not the one its seed derives": "விசைக் கோப்பின் முகவரி அதன் சீட் வருவிப்பதல்ல", + "no expected recipient given: the payee was NOT checked": "எதிர்பார்க்கப்படும் பெறுநர் கொடுக்கப்படவில்லை: பெறுநர் சரிபார்க்கப்படவில்லை (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "இந்த முன்மொழிவு வேறொரு கொள்கைக்கானது: அது %@ என்கிறது, இந்தக் கொள்கை %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "வெளியீடு %@ இந்தக் கணக்கினுடையதாகத் திறக்கவில்லை: பார்க்க முடியாத மதிப்புக்குக் கையொப்பமிட மறுக்கப்படுகிறது", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "witness-இன் மதிப்பு பரிவர்த்தனையுடன் பொருந்தவில்லை: உள்ளீடு %@ bundle-இன் nullifier-க்கு வரவில்லை", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "witness-இன் மதிப்பு வெளியீடுகளுடன் பொருந்தவில்லை: சொத்து %@-இல் உள்ளீடு %@, கட்டணம் மற்றும் எரிப்புகளுடன் வெளியீடு %@", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "முன்மொழிவு %@-இல் காலாவதியாகிறது, அதன் bundle-இன் சாளரத்துக்கு (%@, %@] வெளியே", + "the transaction is for chain %@, the proposal says chain %@": "பரிவர்த்தனை செயின் %@-க்கானது, முன்மொழிவு செயின் %@ என்கிறது", + "signer %@'s signature does not verify over this proposal": "கையொப்பமிடுபவர் %@-இன் கையொப்பம் இந்த முன்மொழிவுக்குச் சரியானதல்ல", + "signer %@ is not in this policy (it has %@ signers)": "கையொப்பமிடுபவர் %@ இந்தக் கொள்கையில் இல்லை (இதில் %@ கையொப்பமிடுபவர்கள் உள்ளனர்)", + "expected recipient %@: %@": "எதிர்பார்க்கப்படும் பெறுநர் %@: %@", + "the policy does not parse: %@": "கொள்கையைப் பாகுபடுத்த முடியவில்லை: %@", + "the proposal does not parse: %@": "முன்மொழிவைப் பாகுபடுத்த முடியவில்லை: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "முன்மொழிவுச் சரிபார்ப்பு முழுமையற்ற பதிலைத் தந்தது, எனவே இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "பெறுபவர் ஒவ்வொருவரின் முகவரியையும் உள்ளிடுங்கள்: நீங்கள் உள்ளிட்ட முகவரியுடன் பொருந்தாத அல்லது முகவரி வெறுமையாக உள்ள வெளியீட்டில் கையொப்பமிட முடியாது.", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@-இல் எந்த நோட்டும் பதிலளிக்கவில்லை, எனவே காலாவதியைச் சரிபார்க்க முடியாது; இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை. நோட் எட்டக்கூடியதாகும்போது மீண்டும் முயலுங்கள்.", + "This proposal is for chain %@; this wallet is on chain %@.": "இந்த முன்மொழிவு செயின் %@-க்கானது; இந்த வாலட் செயின் %@-இல் உள்ளது.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "இந்த முன்மொழிவு, இந்த வாலட்டின் நோட் வழங்கும் genesis-ஐ விட வேறொன்றுடன் பிணைக்கப்பட்டுள்ளது.", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "இந்த முன்மொழிவு உயரம் %@-இல் காலாவதியாகும், செயின் %@-இல் உள்ளது: கையொப்பமிட மிகவும் தாமதம். புதிய முன்மொழிவைக் கேளுங்கள்.", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "கையொப்பமிடுபவர் விசைகளை நீக்க முடியவில்லை, எனவே பணப்பை மறக்கப்படவில்லை. மீண்டும் முயலவும்.", + "Manage multisig signers": "மல்டிசிக் கையொப்பமிடுபவர்களை நிர்வகி", + "Multisig signers": "மல்டிசிக் கையொப்பமிடுபவர்கள்", + "OK": "சரி", + "Enter the address of every payment.": "ஒவ்வொரு கட்டணத்தின் முகவரியையும் உள்ளிடுங்கள்.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "ஒவ்வொரு கட்டணத்தின் தொகையையும் உள்ளிடுங்கள்: பிரித்தலையோ பெருக்கிய கட்டணத்தையோ தடுப்பது இதுவே.", + "An amount is a number of RAND, such as 1.5.": "தொகை என்பது RAND-இன் எண், எடுத்துக்காட்டாக 1.5.", + "Sign a proposal": "முன்மொழிவில் கையொப்பமிடு", + "Proposal (proposal.json)": "முன்மொழிவு (proposal.json)", + "Check a share": "கையொப்பப் பங்கைச் சரிபார்", + "%@ of %@ · signer %@": "%2$@-இல் %1$@ · கையொப்பமிடுபவர் %3$@", + "Open file": "கோப்பைத் திற", + "Checking…": "சரிபார்க்கிறது…", + "Check proposal": "முன்மொழிவைச் சரிபார்", + "What this proposal does, as checked against your policy": "இந்த முன்மொழிவு என்ன செய்கிறது, உங்கள் கொள்கைக்கு எதிராகச் சரிபார்க்கப்பட்டது", + "Your share — send it to whoever proves the proposal": "உங்கள் கையொப்பப் பங்கு — முன்மொழிவை நிரூபிப்பவருக்கு அனுப்புங்கள்", + "index:hex": "index:hex", + "Check share": "கையொப்பப் பங்கைச் சரிபார்", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "கையொப்பமிடுபவர் %1$@-இன் கையொப்பப் பங்கு சரியானது. %3$@-இல் %2$@ கையில் உள்ளன: கையொப்பமிடுபவர்கள் %4$@.", + "That file is larger than a proposal, a policy or a key file can be.": "அந்தக் கோப்பு ஒரு முன்மொழிவு, கொள்கை அல்லது விசைக் கோப்பு இருக்கக்கூடியதை விடப் பெரியது.", + "Add payment": "கட்டணத்தைச் சேர்", + "Check payments": "கட்டணங்களைச் சரிபார்", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND அல்லாத கட்டணங்களின் தொகையைச் சரிபார்க்க முடியாது: அவற்றுக்குத் தொகையை வெறுமையாக விடுங்கள்.", + "Payee address": "பெறுபவர் முகவரி", + "Remove": "நீக்கு", + "Amount in RAND": "RAND-இல் தொகை", + "Intended payments": "நோக்கிய கட்டணங்கள்", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "நீங்கள் செய்யவிருக்கும் ஒவ்வொரு கட்டணத்தையும் உள்ளிடுங்கள்: முகவரியும் RAND-இல் தொகையும். ஒரே முகவரிக்கு இருமுறை செலுத்தும் கட்டணம் மொத்தத்துடன் ஒருமுறை உள்ளிடப்படும். இவற்றில் எதனுடனும் பொருந்தாத வெளியீட்டில் கையொப்பமிட முடியாது.", + "Fee: %@ RAND · expires at height %@": "கட்டணம்: %@ RAND · உயரம் %@-இல் காலாவதியாகும்", + "%@ of %@ payments matched.": "%2$@ கட்டணங்களில் %1$@ பொருந்துகின்றன.", + "Retry": "மீண்டும் முயல்", + "%@ blocks left to prove it.": "இதை நிரூபிக்க இன்னும் %@ பிளாக்குகள் உள்ளன.", + "Payments changed: check them again before signing.": "கட்டணங்கள் மாறின: கையொப்பமிடும் முன் மீண்டும் சரிபாருங்கள்.", + "Signing…": "கையொப்பமிடுகிறது…", + "Sign": "கையொப்பமிடு", + "Type the last four characters of the account fingerprint": "கணக்கு கைரேகையின் கடைசி நான்கு எழுத்துகளை உள்ளிடுங்கள்", + "Save as file": "கோப்பாகச் சேமி", + "Sign a multisig proposal": "மல்டிசிக் முன்மொழிவில் கையொப்பமிடு", + "this signer is already saved as %@": "இந்தக் கையொப்பமிடுபவர் ஏற்கெனவே %@ என்று சேமிக்கப்பட்டுள்ளார்", + "a signer label is 1-64 characters": "கையொப்பமிடுபவர் லேபிள் 1-64 எழுத்துகள்", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "முதலில் சாதனக் கடவுக்குறியீட்டை அமைக்கவும்: கையொப்பமிடுபவர் விசை Face ID, Touch ID அல்லது கடவுக்குறியீட்டுக்குப் பிறகே படிக்கப்படும்.", + "No such signer profile.": "அப்படி ஒரு கையொப்பமிடுபவர் சுயவிவரம் இல்லை.", + "This signer profile has no key on this device: remove it and add it again.": "இந்தக் கையொப்பமிடுபவர் சுயவிவரத்துக்கு இந்தச் சாதனத்தில் விசை இல்லை: அதை நீக்கி மீண்டும் சேருங்கள்.", + "No signer profiles yet": "கையொப்பமிடுபவர் சுயவிவரங்கள் இன்னும் இல்லை", + "Add the policy file and your signer key to sign for a private multisig account.": "தனிப்பட்ட மல்டிசிக் கணக்கிற்குக் கையொப்பமிட, கொள்கைக் கோப்பையும் உங்கள் கையொப்பமிடுபவர் விசையையும் சேருங்கள்.", + "Remove this signer": "இந்தக் கையொப்பமிடுபவரை நீக்கு", + "Remove a multisig signer": "மல்டிசிக் கையொப்பமிடுபவரை நீக்கு", + "Add a signer": "கையொப்பமிடுபவரைச் சேர்", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "கையொப்பமிடுபவர் சுயவிவரம் இந்தத் தொலைபேசியின் Keychain-இல் வைக்கப்படுகிறது. ஒவ்வொரு கையொப்பமும் மீண்டும் Face ID, Touch ID அல்லது உங்கள் கடவுக்குறியீட்டைக் கேட்கும்.", + "Policy file": "கொள்கைக் கோப்பு", + "Signer key file, or its 64-hex seed": "கையொப்பமிடுபவர் விசைக் கோப்பு, அல்லது அதன் 64-ஹெக்ஸ் சீட்", + "Account fingerprint — compare it with every other signer before you save": "கணக்கு கைரேகை — சேமிப்பதற்கு முன் மற்ற ஒவ்வொரு கையொப்பமிடுபவருடனும் ஒப்பிடுங்கள்", + "Label": "லேபிள்", + "Check": "சரிபார்", + "%@ of %@ · this key is signer %@": "%2$@-இல் %1$@ · இந்த விசை கையொப்பமிடுபவர் %3$@", + "Save signer": "கையொப்பமிடுபவரைச் சேமி", + "That file could not be read.": "அந்தக் கோப்பைப் படிக்க முடியவில்லை.", + "Remove %@?": "%@ ஐ நீக்கவா?", + "You need the signer's key file to add this signer back.": "இந்தக் கையொப்பமிடுபவரை மீண்டும் சேர்க்க அவருடைய விசைக் கோப்பு தேவை.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "இந்தப் பெறுநர் RAND மற்றும் ஒரு டோக்கன் இரண்டையும் பெறுகிறார்: ஒரே பெறுநருக்கான RAND மற்றும் டோக்கன் கொடுப்பனவுகளுக்குத் தனித்தனி முன்மொழிவுகள் தேவை." }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/ur.json b/ios/l10n/ur.json index 59f12e5..0383186 100644 --- a/ios/l10n/ur.json +++ b/ios/l10n/ur.json @@ -428,7 +428,99 @@ "unknown parameter %@": "نامعلوم پیرامیٹر %@", "wallet is locked": "والیٹ لاک ہے", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "لین دین کا ہیش تجویز کی بائنڈنگ کا نہیں ہے", + "the proposal's signing_message is not its binding's": "تجویز کا signing_message اس کی بائنڈنگ کا نہیں ہے", + "the proposal's auth_commit is not this policy's account": "تجویز کا auth_commit اس پالیسی کا اکاؤنٹ نہیں ہے", + "the proposal's expected digest is not its bundle's": "تجویز کا متوقع ڈائجسٹ اس کے بنڈل کا نہیں ہے", + "the transaction carries no bundle": "لین دین میں کوئی بنڈل نہیں ہے", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "لین دین میں ایک عمل ہے؛ نجی ملٹی سگ تجویز صرف اپنے بنڈل سے ادائیگی ہوتی ہے", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "ویٹنس کی قدر جانچی نہیں جا سکتی: اس کا nk یا سالٹ اس تجویز کا نہیں ہے", + "this key is not one of the policy's signers": "یہ کی پالیسی کے دستخط کنندگان میں سے نہیں ہے", + "a share is index:hex": "دستخطی حصہ index:hex ہوتا ہے", + "a signer key is a rand-node keygen file or 64 hex characters": "دستخط کنندہ کی ایک rand-node keygen فائل یا 64 ہیکس حروف ہوتی ہے", + "a signer seed is 64 hex characters": "دستخط کنندہ کا سیڈ 64 ہیکس حروف کا ہوتا ہے", + "expected_recipients must be a list of addresses": "expected_recipients پتوں کی فہرست ہونی چاہیے", + "the key file's public_key is not the one its seed derives": "کی فائل کی public_key وہ نہیں جو اس کا سیڈ اخذ کرتا ہے", + "the key file's address is not the one its seed derives": "کی فائل کا پتہ وہ نہیں جو اس کا سیڈ اخذ کرتا ہے", + "no expected recipient given: the payee was NOT checked": "کوئی متوقع وصول کنندہ نہیں دیا گیا: وصول کنندہ کی جانچ نہیں ہوئی (NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "یہ تجویز کسی اور پالیسی کی ہے: اس میں %@ ہے، جبکہ یہ پالیسی %@ ہے", + "output %@ does not open as this account's: refusing to sign value it cannot see": "آؤٹ پٹ %@ اس اکاؤنٹ کے طور پر نہیں کھلتا: جو قدر نظر نہیں آتی اس پر دستخط سے انکار", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "ویٹنس کی قدر لین دین سے مطابقت نہیں رکھتی: ان پٹ %@ بنڈل کے نلیفائر تک نہیں پہنچتا", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "ویٹنس کی قدر آؤٹ پٹس سے مطابقت نہیں رکھتی: اثاثہ %@ میں %@ ان پٹ اور فیس و برن کے ساتھ %@ آؤٹ پٹ ہے", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "تجویز %@ پر ختم ہوتی ہے، جو اس کے بنڈل کی حد (%@, %@] سے باہر ہے", + "the transaction is for chain %@, the proposal says chain %@": "لین دین چین %@ کے لیے ہے، تجویز چین %@ بتاتی ہے", + "signer %@'s signature does not verify over this proposal": "دستخط کنندہ %@ کا دستخط اس تجویز کے لیے درست نہیں", + "signer %@ is not in this policy (it has %@ signers)": "دستخط کنندہ %@ اس پالیسی میں نہیں ہے (اس میں %@ دستخط کنندگان ہیں)", + "expected recipient %@: %@": "متوقع وصول کنندہ %@: %@", + "the policy does not parse: %@": "پالیسی پارس نہیں ہوتی: %@", + "the proposal does not parse: %@": "تجویز پارس نہیں ہوتی: %@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "تجویز کی جانچ نے ادھورا جواب لوٹایا، اس لیے اس تجویز پر دستخط نہیں کیے گئے۔", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "ہر وصول کنندہ کا پتہ درج کریں: جو آؤٹ پٹ درج کیے گئے پتے سے مطابقت نہیں رکھتا، یا جس کا پتہ خالی ہے، اس پر دستخط نہیں ہو سکتے۔", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@ پر کسی نوڈ نے جواب نہیں دیا، اس لیے میعاد کی جانچ نہیں ہو سکتی اور اس تجویز پر دستخط نہیں کیے گئے۔ نوڈ دستیاب ہونے پر دوبارہ کوشش کریں۔", + "This proposal is for chain %@; this wallet is on chain %@.": "یہ تجویز چین %@ کے لیے ہے؛ یہ والیٹ چین %@ پر ہے۔", + "This proposal is bound to another genesis than the one this wallet's node serves.": "یہ تجویز اس جینیسس سے مختلف جینیسس سے بندھی ہے جو اس والیٹ کا نوڈ فراہم کرتا ہے۔", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "یہ تجویز اونچائی %@ پر ختم ہوتی ہے اور چین %@ پر ہے: دستخط کے لیے بہت دیر ہو چکی۔ نئی تجویز مانگیں۔", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "دستخط کنندہ کی کلیدیں ہٹائی نہ جا سکیں، اس لیے والیٹ نہیں بھولا گیا۔ دوبارہ کوشش کریں۔", + "Manage multisig signers": "ملٹی سگ دستخط کنندگان کا انتظام کریں", + "Multisig signers": "ملٹی سگ دستخط کنندگان", + "OK": "ٹھیک ہے", + "Enter the address of every payment.": "ہر ادائیگی کا پتہ درج کریں۔", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "ہر ادائیگی کی رقم درج کریں: یہی ادائیگی کو بانٹنے یا پھلانے سے روکتی ہے۔", + "An amount is a number of RAND, such as 1.5.": "رقم RAND کی ایک عددی مقدار ہے، جیسے 1.5۔", + "Sign a proposal": "تجویز پر دستخط کریں", + "Proposal (proposal.json)": "تجویز (proposal.json)", + "Check a share": "دستخطی حصہ جانچیں", + "%@ of %@ · signer %@": "%2$@ میں سے %1$@ · دستخط کنندہ %3$@", + "Open file": "فائل کھولیں", + "Checking…": "جانچ ہو رہی ہے…", + "Check proposal": "تجویز جانچیں", + "What this proposal does, as checked against your policy": "یہ تجویز کیا کرتی ہے، آپ کی پالیسی کے مقابل جانچا گیا", + "Your share — send it to whoever proves the proposal": "آپ کا دستخطی حصہ — اسے اس کو بھیجیں جو تجویز ثابت کرتا ہے", + "index:hex": "index:hex", + "Check share": "دستخطی حصہ جانچیں", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "دستخط کنندہ %1$@ کا دستخطی حصہ درست ہے۔ %3$@ میں سے %2$@ ہاتھ میں ہیں: دستخط کنندگان %4$@۔", + "That file is larger than a proposal, a policy or a key file can be.": "وہ فائل کسی تجویز، پالیسی یا کی فائل کے ممکنہ سائز سے بڑی ہے۔", + "Add payment": "ادائیگی شامل کریں", + "Check payments": "ادائیگیاں جانچیں", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "غیر RAND ادائیگیوں کی رقم جانچی نہیں جا سکتی: ان کے لیے رقم خالی چھوڑیں۔", + "Payee address": "وصول کنندہ کا پتہ", + "Remove": "ہٹائیں", + "Amount in RAND": "رقم (RAND میں)", + "Intended payments": "مطلوبہ ادائیگیاں", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "جو بھی ادائیگی آپ کرنا چاہتے ہیں ہر ایک درج کریں: پتہ اور RAND میں رقم۔ ایک ہی پتے پر دو بار ادائیگی کل رقم کے ساتھ ایک بار درج ہوتی ہے۔ جو آؤٹ پٹ ان میں سے کسی سے مطابقت نہیں رکھتا اس پر دستخط نہیں ہو سکتے۔", + "Fee: %@ RAND · expires at height %@": "فیس: %@ RAND · اونچائی %@ پر ختم", + "%@ of %@ payments matched.": "%2$@ میں سے %1$@ ادائیگیاں مطابقت رکھتی ہیں۔", + "Retry": "دوبارہ کوشش کریں", + "%@ blocks left to prove it.": "اسے ثابت کرنے کے لیے %@ بلاک باقی ہیں۔", + "Payments changed: check them again before signing.": "ادائیگیاں بدل گئیں: دستخط سے پہلے انہیں دوبارہ جانچیں۔", + "Signing…": "دستخط ہو رہے ہیں…", + "Sign": "دستخط کریں", + "Type the last four characters of the account fingerprint": "اکاؤنٹ کے فنگر پرنٹ کے آخری چار حروف لکھیں", + "Save as file": "فائل کے طور پر محفوظ کریں", + "Sign a multisig proposal": "ملٹی سگ تجویز پر دستخط کریں", + "this signer is already saved as %@": "یہ دستخط کنندہ پہلے سے %@ کے طور پر محفوظ ہے", + "a signer label is 1-64 characters": "دستخط کنندہ کا لیبل 1-64 حروف کا ہوتا ہے", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "پہلے ڈیوائس کا پاس کوڈ سیٹ کریں: دستخط کنندہ کی کلید صرف Face ID، Touch ID یا پاس کوڈ کے بعد پڑھی جاتی ہے۔", + "No such signer profile.": "ایسی کوئی دستخط کنندہ پروفائل نہیں ہے۔", + "This signer profile has no key on this device: remove it and add it again.": "اس دستخط کنندہ پروفائل کی اس ڈیوائس پر کوئی کی نہیں ہے: اسے ہٹا کر دوبارہ شامل کریں۔", + "No signer profiles yet": "ابھی کوئی دستخط کنندہ پروفائل نہیں", + "Add the policy file and your signer key to sign for a private multisig account.": "نجی ملٹی سگ اکاؤنٹ کے لیے دستخط کرنے کی غرض سے پالیسی فائل اور اپنی دستخط کنندہ کی شامل کریں۔", + "Remove this signer": "اس دستخط کنندہ کو ہٹائیں", + "Remove a multisig signer": "ملٹی سگ دستخط کنندہ ہٹائیں", + "Add a signer": "دستخط کنندہ شامل کریں", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "دستخط کنندہ کا پروفائل اس فون کی Keychain میں رکھا جاتا ہے۔ ہر دستخط پر دوبارہ Face ID، Touch ID یا آپ کا پاس کوڈ مانگا جاتا ہے۔", + "Policy file": "پالیسی فائل", + "Signer key file, or its 64-hex seed": "دستخط کنندہ کی فائل، یا اس کا 64-ہیکس سیڈ", + "Account fingerprint — compare it with every other signer before you save": "اکاؤنٹ کا فنگر پرنٹ — محفوظ کرنے سے پہلے اسے ہر دوسرے دستخط کنندہ سے ملائیں", + "Label": "لیبل", + "Check": "جانچیں", + "%@ of %@ · this key is signer %@": "%2$@ میں سے %1$@ · یہ کی دستخط کنندہ %3$@ ہے", + "Save signer": "دستخط کنندہ محفوظ کریں", + "That file could not be read.": "وہ فائل پڑھی نہ جا سکی۔", + "Remove %@?": "%@ کو ہٹائیں؟", + "You need the signer's key file to add this signer back.": "اس دستخط کنندہ کو دوبارہ شامل کرنے کے لیے آپ کو اس کی کلیدی فائل چاہیے ہوگی۔", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "یہ وصول کنندہ RAND اور ایک ٹوکن دونوں وصول کرتا ہے: ایک وصول کنندہ کو RAND اور ٹوکن کی ادائیگیوں کے لیے الگ الگ تجاویز درکار ہیں۔" }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/zh-Hans.json b/ios/l10n/zh-Hans.json index 25ff6eb..183f010 100644 --- a/ios/l10n/zh-Hans.json +++ b/ios/l10n/zh-Hans.json @@ -425,7 +425,99 @@ "unknown parameter %@": "未知参数 %@", "wallet is locked": "钱包已锁定", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "交易的哈希不是提案的绑定", + "the proposal's signing_message is not its binding's": "提案的 signing_message 与其绑定的不一致", + "the proposal's auth_commit is not this policy's account": "提案的 auth_commit 不是此策略的账户", + "the proposal's expected digest is not its bundle's": "提案的预期摘要与其捆绑包的不一致", + "the transaction carries no bundle": "交易不含捆绑包", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "交易带有一个动作;私密多签提案只是仅来自其捆绑包的付款", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "无法核对见证数据的金额:其 nk 或盐值不属于此提案", + "this key is not one of the policy's signers": "此密钥不属于该策略的签名人", + "a share is index:hex": "签名份额的格式为 index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "签名人密钥是 rand-node 的 keygen 文件或 64 位十六进制字符", + "a signer seed is 64 hex characters": "签名人种子为 64 位十六进制字符", + "expected_recipients must be a list of addresses": "expected_recipients 必须是地址列表", + "the key file's public_key is not the one its seed derives": "密钥文件中的 public_key 与其种子派生出的不一致", + "the key file's address is not the one its seed derives": "密钥文件中的地址与其种子派生出的地址不一致", + "no expected recipient given: the payee was NOT checked": "未给出预期收款人:收款人未经核对(NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "此提案属于另一个策略:它指向 %@,而此策略是 %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "输出 %@ 无法作为此账户的输出打开:拒绝对看不到的金额签名", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "见证数据的金额与交易不一致:输入 %@ 未对应到捆绑包的 nullifier", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "见证数据的金额与输出不一致:资产 %@ 输入 %@,输出 %@(含手续费和销毁)", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "提案在 %@ 过期,超出其捆绑包的有效区间 (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "交易属于链 %@,而提案指向链 %@", + "signer %@'s signature does not verify over this proposal": "签名人 %@ 的签名对此提案验证未通过", + "signer %@ is not in this policy (it has %@ signers)": "签名人 %@ 不在此策略中(该策略有 %@ 位签名人)", + "expected recipient %@: %@": "预期收款人 %@:%@", + "the policy does not parse: %@": "策略无法解析:%@", + "the proposal does not parse: %@": "提案无法解析:%@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案核对返回了不完整的结果,因此此提案不会签名。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "请输入每位收款人的地址:与所输入地址不匹配、或地址为空的输出无法签名。", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@ 没有节点应答,因此无法核对过期时间,此提案不会签名。待节点可访问后重试。", + "This proposal is for chain %@; this wallet is on chain %@.": "此提案属于链 %@;此钱包在链 %@ 上。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "此提案绑定的创世区块与此钱包的节点所提供的不同。", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "此提案在高度 %@ 过期,而链已到 %@:已来不及签名。请索取新的提案。", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "无法移除签名人密钥,因此钱包没有被忘记。请重试。", + "Manage multisig signers": "管理多签签名人", + "Multisig signers": "多签签名人", + "OK": "好", + "Enter the address of every payment.": "请输入每笔付款的地址。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "请输入每笔付款的金额:它可以防止拆分或虚增付款。", + "An amount is a number of RAND, such as 1.5.": "金额是 RAND 的数值,例如 1.5。", + "Sign a proposal": "签署提案", + "Proposal (proposal.json)": "提案(proposal.json)", + "Check a share": "核对签名份额", + "%@ of %@ · signer %@": "%@/%@ · 签名人 %@", + "Open file": "打开文件", + "Checking…": "正在核对…", + "Check proposal": "核对提案", + "What this proposal does, as checked against your policy": "此提案的作用(已对照你的策略核对)", + "Your share — send it to whoever proves the proposal": "你的签名份额 — 请发给负责证明提案的人", + "index:hex": "index:hex", + "Check share": "核对签名份额", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "签名人 %@ 的签名份额验证通过。已收集 %@/%@:签名人 %@。", + "That file is larger than a proposal, a policy or a key file can be.": "该文件大于提案、策略或密钥文件所允许的大小。", + "Add payment": "添加付款", + "Check payments": "核对付款", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "非 RAND 付款无法核对金额:这类付款请将金额留空。", + "Payee address": "收款人地址", + "Remove": "移除", + "Amount in RAND": "金额(RAND)", + "Intended payments": "拟进行的付款", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "请输入你打算进行的每一笔付款:地址和以 RAND 计的金额。向同一地址的两笔付款只输入一次,填合计金额。与其中任何一笔都不匹配的输出无法签名。", + "Fee: %@ RAND · expires at height %@": "手续费:%@ RAND · 在高度 %@ 过期", + "%@ of %@ payments matched.": "%2$@ 笔付款中有 %1$@ 笔匹配。", + "Retry": "重试", + "%@ blocks left to prove it.": "还差 %@ 个区块即可证明。", + "Payments changed: check them again before signing.": "付款已变更:签名前请重新核对。", + "Signing…": "正在签名…", + "Sign": "签名", + "Type the last four characters of the account fingerprint": "请输入账户指纹的后四个字符", + "Save as file": "另存为文件", + "Sign a multisig proposal": "签署多签提案", + "this signer is already saved as %@": "此签名人已保存为 %@", + "a signer label is 1-64 characters": "签名人标签为 1-64 个字符", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "请先设置设备密码:签名人密钥只有在通过 Face ID、Touch ID 或设备密码后才会被读取。", + "No such signer profile.": "没有这个签名人资料。", + "This signer profile has no key on this device: remove it and add it again.": "此签名人资料在此设备上没有密钥:请移除后重新添加。", + "No signer profiles yet": "尚无签名人资料", + "Add the policy file and your signer key to sign for a private multisig account.": "添加策略文件和你的签名人密钥,即可为私密多签账户签名。", + "Remove this signer": "移除此签名人", + "Remove a multisig signer": "移除多签签名人", + "Add a signer": "添加签名人", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "签名人配置保存在这部手机的钥匙串(Keychain)中。每次签名都会再次要求 Face ID、Touch ID 或设备密码。", + "Policy file": "策略文件", + "Signer key file, or its 64-hex seed": "签名人密钥文件,或其 64 位十六进制种子", + "Account fingerprint — compare it with every other signer before you save": "账户指纹 — 保存前请与其他每位签名人核对", + "Label": "标签", + "Check": "核对", + "%@ of %@ · this key is signer %@": "%@/%@ · 此密钥是签名人 %@", + "Save signer": "保存签名人", + "That file could not be read.": "无法读取该文件。", + "Remove %@?": "移除%@?", + "You need the signer's key file to add this signer back.": "要重新添加此签名人,需要他的密钥文件。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "此收款人同时收取 RAND 和代币:向同一收款人支付 RAND 和代币需要分开的提案。" }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/l10n/zh-Hant-HK.json b/ios/l10n/zh-Hant-HK.json index 675af33..80e9d26 100644 --- a/ios/l10n/zh-Hant-HK.json +++ b/ios/l10n/zh-Hant-HK.json @@ -425,7 +425,99 @@ "unknown parameter %@": "未知參數 %@", "wallet is locked": "錢包已鎖定", "−%@ %@": "−%@ %@", - "−%@ RAND": "−%@ RAND" + "−%@ RAND": "−%@ RAND", + "the transaction does not hash to the proposal's binding": "交易的雜湊不是提案的綁定", + "the proposal's signing_message is not its binding's": "提案的 signing_message 與其綁定的不一致", + "the proposal's auth_commit is not this policy's account": "提案的 auth_commit 不是此政策的帳戶", + "the proposal's expected digest is not its bundle's": "提案的預期摘要與其捆綁包的不一致", + "the transaction carries no bundle": "交易不含捆綁包", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "交易帶有一個動作;私密多重簽署提案只是僅來自其捆綁包的付款", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "無法核對見證資料的金額:其 nk 或鹽值不屬於此提案", + "this key is not one of the policy's signers": "此金鑰不屬於該政策的簽署人", + "a share is index:hex": "簽署份額的格式為 index:hex", + "a signer key is a rand-node keygen file or 64 hex characters": "簽署人金鑰是 rand-node 的 keygen 檔案或 64 位十六進制字元", + "a signer seed is 64 hex characters": "簽署人種子為 64 位十六進制字元", + "expected_recipients must be a list of addresses": "expected_recipients 必須是地址清單", + "the key file's public_key is not the one its seed derives": "金鑰檔案內的 public_key 與其種子衍生出的不一致", + "the key file's address is not the one its seed derives": "金鑰檔案內的地址與其種子衍生出的地址不一致", + "no expected recipient given: the payee was NOT checked": "未提供預期收款人:收款人未經核對(NOT checked)", + "this proposal is for another policy: it names %@, this policy is %@": "此提案屬於另一個政策:它指向 %@,而此政策是 %@", + "output %@ does not open as this account's: refusing to sign value it cannot see": "輸出 %@ 無法作為此帳戶的輸出開啟:拒絕簽署看不到的金額", + "the witness's value does not match the transaction: input %@ does not nullify to the bundle's nullifier": "見證資料的金額與交易不一致:輸入 %@ 未對應到捆綁包的 nullifier", + "the witness's value does not match the outputs: asset %@ has %@ in and %@ out with the fee and burns": "見證資料的金額與輸出不一致:資產 %@ 輸入 %@,輸出 %@(含手續費及銷毀)", + "the proposal expires at %@, outside its bundle's window (%@, %@]": "提案在 %@ 過期,超出其捆綁包的有效區間 (%@, %@]", + "the transaction is for chain %@, the proposal says chain %@": "交易屬於鏈 %@,而提案指向鏈 %@", + "signer %@'s signature does not verify over this proposal": "簽署人 %@ 的簽署對此提案驗證不通過", + "signer %@ is not in this policy (it has %@ signers)": "簽署人 %@ 不在此政策內(該政策有 %@ 位簽署人)", + "expected recipient %@: %@": "預期收款人 %@:%@", + "the policy does not parse: %@": "政策無法解析:%@", + "the proposal does not parse: %@": "提案無法解析:%@", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案核對傳回不完整的結果,因此此提案不會簽署。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "請輸入每位收款人的地址:與所輸入地址不相符、或地址為空的輸出無法簽署。", + "No node answered at %@, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "%@ 沒有節點回應,因此無法核對過期時間,此提案不會簽署。待節點可連線後再試。", + "This proposal is for chain %@; this wallet is on chain %@.": "此提案屬於鏈 %@;此錢包在鏈 %@ 上。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "此提案綁定的創世區塊與此錢包的節點所提供的不同。", + "This proposal expires at height %@ and the chain is at %@: too late to sign. Ask for a new proposal.": "此提案在高度 %@ 過期,而鏈已去到 %@:已來不及簽署。請索取新的提案。", + "The signer keys could not be removed, so the wallet was not forgotten. Try again.": "無法移除簽署人密鑰,因此錢包沒有被忘記。請再試一次。", + "Manage multisig signers": "管理多重簽署簽署人", + "Multisig signers": "多重簽署簽署人", + "OK": "好", + "Enter the address of every payment.": "請輸入每筆付款的地址。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "請輸入每筆付款的金額:它可防止拆分或虛增付款。", + "An amount is a number of RAND, such as 1.5.": "金額是 RAND 的數值,例如 1.5。", + "Sign a proposal": "簽署提案", + "Proposal (proposal.json)": "提案(proposal.json)", + "Check a share": "核對簽署份額", + "%@ of %@ · signer %@": "%@/%@ · 簽署人 %@", + "Open file": "開啟檔案", + "Checking…": "正在核對…", + "Check proposal": "核對提案", + "What this proposal does, as checked against your policy": "此提案的作用(已對照你的政策核對)", + "Your share — send it to whoever proves the proposal": "你的簽署份額 — 請交給負責證明提案的人", + "index:hex": "index:hex", + "Check share": "核對簽署份額", + "Signer %@'s share verifies. %@ of %@ in hand: signers %@.": "簽署人 %@ 的簽署份額驗證通過。已收集 %@/%@:簽署人 %@。", + "That file is larger than a proposal, a policy or a key file can be.": "該檔案大於提案、政策或金鑰檔案所容許的大小。", + "Add payment": "新增付款", + "Check payments": "核對付款", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "非 RAND 付款無法核對金額:該類付款請將金額留空。", + "Payee address": "收款人地址", + "Remove": "移除", + "Amount in RAND": "金額(RAND)", + "Intended payments": "擬進行的付款", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "請輸入你打算進行的每一筆付款:地址及以 RAND 計的金額。向同一地址的兩筆付款只輸入一次,填合計金額。與其中任何一筆都不相符的輸出無法簽署。", + "Fee: %@ RAND · expires at height %@": "手續費:%@ RAND · 於高度 %@ 過期", + "%@ of %@ payments matched.": "%2$@ 筆付款中有 %1$@ 筆相符。", + "Retry": "重試", + "%@ blocks left to prove it.": "尚欠 %@ 個區塊便可證明。", + "Payments changed: check them again before signing.": "付款已更改:簽署前請重新核對。", + "Signing…": "正在簽署…", + "Sign": "簽署", + "Type the last four characters of the account fingerprint": "請輸入帳戶指紋的最後四個字元", + "Save as file": "另存為檔案", + "Sign a multisig proposal": "簽署多重簽署提案", + "this signer is already saved as %@": "此簽署人已儲存為 %@", + "a signer label is 1-64 characters": "簽署人標籤為 1-64 個字元", + "Set a device passcode first: a signer key is read only after Face ID, Touch ID or the passcode.": "請先設定裝置密碼:簽署人密鑰只有在通過 Face ID、Touch ID 或密碼後才會被讀取。", + "No such signer profile.": "沒有這個簽署人資料。", + "This signer profile has no key on this device: remove it and add it again.": "此簽署人資料在此裝置上沒有金鑰:請移除後重新新增。", + "No signer profiles yet": "尚未有簽署人資料", + "Add the policy file and your signer key to sign for a private multisig account.": "新增政策檔案及你的簽署人金鑰,即可為私密多重簽署帳戶簽署。", + "Remove this signer": "移除此簽署人", + "Remove a multisig signer": "移除多重簽署簽署人", + "Add a signer": "新增簽署人", + "A signer profile is kept in this phone's Keychain. Every signature asks for Face ID, Touch ID or your passcode again.": "簽署人設定檔保存在這部手機的鑰匙圈(Keychain)中。每次簽署都會再次要求 Face ID、Touch ID 或密碼。", + "Policy file": "政策檔案", + "Signer key file, or its 64-hex seed": "簽署人金鑰檔案,或其 64 位十六進制種子", + "Account fingerprint — compare it with every other signer before you save": "帳戶指紋 — 儲存前請與其他每位簽署人核對", + "Label": "標籤", + "Check": "核對", + "%@ of %@ · this key is signer %@": "%@/%@ · 此金鑰是簽署人 %@", + "Save signer": "儲存簽署人", + "That file could not be read.": "無法讀取該檔案。", + "Remove %@?": "移除%@?", + "You need the signer's key file to add this signer back.": "要重新新增此簽署人,需要他的密鑰檔案。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "此收款人同時收取 RAND 及代幣:向同一收款人支付 RAND 及代幣,需要分開的提案。" }, "InfoPlist": { "CFBundleDisplayName": "Rand Wallet", diff --git a/ios/project.yml b/ios/project.yml index 8ec90d1..6d165e5 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -56,6 +56,10 @@ targets: # durian-core's own AMM vectors: the very file ui/test/amm.test.mjs reads (AmmTests). - path: ../ui/test/fixtures/amm-vectors.json buildPhase: resources + # The private-multisig vectors (fullnode 9b014927, randprotocol-msig's tests/fixtures), the + # files ui/test/msauth-fixtures.test.mjs pins (MsauthTests). + - path: ../ui/test/fixtures/msauth + buildPhase: resources dependencies: - target: RandWallet settings: diff --git a/scripts/e2e-msauth.sh b/scripts/e2e-msauth.sh new file mode 100755 index 0000000..c4c8edb --- /dev/null +++ b/scripts/e2e-msauth.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# Private-multisig signing end to end (spec 2026-10-11-msauth-signing-design.md §6.5): a 2-of-3 +# account on a one-validator Test-profile devnet (chain 21's shape: split authorisation + the +# multisig auth guest), the CLI prepares, signer 0 signs in the web wallet's engine +# (ui/engine/signers.js over the real wasm core), signer 2 in iOS through MsauthSigning and RandCore +# on the simulator (FakeSignerSecrets: a simulator has no passcode), the CLI proves (msauth tier 18 +# + bundle, on this machine) and submits, and the payee's scan finds the payout. +# +# RAND_NODE_BIN=core/target/e2e/release/rand-node RAND_BIN=core/target/e2e/release/rand scripts/e2e-msauth.sh +# +# Both binaries from the fullnode this repo vendors (core/vendor/fullnode): +# (cd core/vendor/fullnode && CARGO_TARGET_DIR=$PWD/../../target/e2e cargo build --release \ +# -p randprotocol-node -p randprotocol-client) # then restore its Cargo.lock (see e2e-prover.sh) +# and the cores: core/scripts/build-wasm.sh, core/scripts/build-ios.sh. +# +# The prove is guarded: it does not start beside another prove or a process over 4 GB, and it is +# killed if its memory passes MSAUTH_E2E_MAX_MEM_GB (default 40) — the tier-18 msauth prove is "at +# least 48 GB" at Production; this chain runs the Test FRI profile. On macOS the gauge is the +# physical footprint (`footprint`), not RSS: the compressor keeps RSS low while the footprint grows +# (2026-10-12 on a 48 GB Mac: RSS 22.5 GB, footprint 76 GB, killed by the OS). Every process +# started here is killed on exit. MSAUTH_E2E_SKIP_IOS=1 has signer 2 sign with the CLI instead of +# the simulator. +set -euo pipefail +cd "$(dirname "$0")/.." +ROOT=$(pwd) +: "${RAND_NODE_BIN:?set RAND_NODE_BIN}" "${RAND_BIN:?set RAND_BIN}" +DIR=${MSAUTH_E2E_DIR:-$(mktemp -d)} +mkdir -p "$DIR" +PORT=${MSAUTH_E2E_PORT:-18545} +RPC="http://127.0.0.1:$PORT" +AMOUNT=10 +MAX_KB=$(( ${MSAUTH_E2E_MAX_MEM_GB:-40} * 1024 * 1024 )) +SIM=${MSAUTH_E2E_SIMULATOR:-iPhone 18 Pro} # a simulator on the newest installed iOS runtime +export RAND_MSAUTH_VK="$ROOT/core/vendor/fullnode/crates/randprotocol-zkvm/keys/msauth-vk-test.bin" +export RAND_ALLOW_TEST_FRI_PROFILE=1 +R() { "$RAND_BIN" --rpc "$RPC" "$@"; } +rpc() { curl -sf -X POST -H 'content-type: application/json' -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":[]}" "$RPC"; } +step() { printf '\n== %s (%s)\n' "$*" "$(date +%H:%M:%S)"; } +echo "workdir $DIR" +echo "rand-node $("$RAND_NODE_BIN" --version) at $RAND_NODE_BIN" +echo "rand $("$RAND_BIN" --version) at $RAND_BIN" + +free_gib=$(df -g "$DIR" | awk 'NR==2 {print $4}') +[ "$free_gib" -ge 25 ] || { echo "only $free_gib GiB free (need 25)"; exit 1; } + +NODE_PID='' PROVE_PID='' +cleanup() { + [ -n "$PROVE_PID" ] && { pkill -P "$PROVE_PID" 2>/dev/null || true; kill "$PROVE_PID" 2>/dev/null || true; } + [ -n "$NODE_PID" ] && kill "$NODE_PID" 2>/dev/null || true + [ -n "$NODE_PID" ] && wait "$NODE_PID" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +step "devnet: chain 21 shape, Test FRI profile" +"$RAND_NODE_BIN" keygen --out "$DIR/node.key.json" >/dev/null +for i in 0 1 2; do "$RAND_NODE_BIN" keygen --out "$DIR/signer$i.key.json" >/dev/null; done +# The validator's payout wallet and the payee are two wallets: rewards never reach the payee. +R --key "$DIR/v.key.json" keygen >/dev/null +R --key "$DIR/p.key.json" keygen >/dev/null +addr() { R --key "$1" address 2>/dev/null | grep -Eo '^rand1[1-9A-HJ-NP-Za-km-z]+' | head -1; } +V=$(addr "$DIR/v.key.json") +P=$(addr "$DIR/p.key.json") +"$RAND_NODE_BIN" genesis --chain-id 21 --fri-profile test --faucet --validator "$DIR/node.key.json,1000,$V" \ + --gas-price 100 --byte-price 800 --bundle-gas-limit 20479 --bundle-guest v3 --auth-guest --auth-multisig-guest \ + --max-proof-bytes 4194304 --max-block-bytes 20971520 --binding-domain 1 --proof-window-blocks 1024 --out "$DIR/genesis.json" +"$RAND_NODE_BIN" init --datadir "$DIR/data" --genesis "$DIR/genesis.json" +"$RAND_NODE_BIN" run --datadir "$DIR/data" --key "$DIR/node.key.json" --validator --rpc "127.0.0.1:$PORT" \ + --listen /ip4/127.0.0.1/tcp/0 --no-mdns --block-interval-ms 3000 --view-timeout-ms 6000 --min-free-disk-mb 0 > "$DIR/node.log" 2>&1 & +NODE_PID=$! +for _ in $(seq 120); do rpc rand_status | grep -q '"msauth_key":"loaded"' && break; sleep 1; done +rpc rand_status | grep -q '"msauth_key":"loaded"' || { echo "the node did not load the msauth key"; tail -40 "$DIR/node.log"; exit 1; } + +step "policy: 2 of 3; fund the account" +pk() { python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["public_key"])' "$1"; } +R multisig-private new-policy --signer "$(pk "$DIR/signer0.key.json")" --signer "$(pk "$DIR/signer1.key.json")" \ + --signer "$(pk "$DIR/signer2.key.json")" --threshold 2 --out "$DIR/policy.json" +ACCT=$(R multisig-private address --policy "$DIR/policy.json" | sed -n 's/^address: //p') +R faucet "$ACCT" --amount 100 +R multisig-private balance --policy "$DIR/policy.json" + +step "prepare: pay the payee $AMOUNT RAND" +R multisig-private prepare --policy "$DIR/policy.json" --to "$P" --amount "$AMOUNT" --out "$DIR/proposal.json" +# What the payee told each signer out of band, and what each signer enters: the address and amount. +echo "$P:$AMOUNT" > "$DIR/payee.txt" +R multisig-private check --proposal "$DIR/proposal.json" --policy "$DIR/policy.json" --expect-to "$P:$AMOUNT" + +step "signer 0: the web wallet's engine" +MSAUTH_E2E_DIR="$DIR" MSAUTH_E2E_RPC="$RPC" MSAUTH_E2E_SIGNER=0 node --test web/wallet/test/msauth.devnet.e2e.test.mjs 2>&1 | tee "$DIR/web.log" +grep -q '^0:' "$DIR/share0.txt" || { echo "the web wallet wrote no share"; exit 1; } + +if [ "${MSAUTH_E2E_SKIP_IOS:-0}" = 1 ]; then + step "signer 2: the CLI (MSAUTH_E2E_SKIP_IOS=1)" + R multisig-private sign --proposal "$DIR/proposal.json" --policy "$DIR/policy.json" --key "$DIR/signer2.key.json" \ + --expect-to "$P:$AMOUNT" > "$DIR/share2.txt" +else + step "signer 2: iOS on the simulator ($SIM)" + ( cd ios && TEST_RUNNER_RAND_MSAUTH_PROPOSAL="$(cat "$DIR/proposal.json")" TEST_RUNNER_RAND_MSAUTH_POLICY="$(cat "$DIR/policy.json")" \ + TEST_RUNNER_RAND_MSAUTH_KEY="$(cat "$DIR/signer2.key.json")" TEST_RUNNER_RAND_MSAUTH_RPC="$RPC" \ + TEST_RUNNER_RAND_MSAUTH_PAYEE="$P:$AMOUNT" TEST_RUNNER_RAND_MSAUTH_CHAIN_ID=21 \ + xcodebuild -project RandWallet.xcodeproj -scheme RandWallet -destination "platform=iOS Simulator,name=$SIM" \ + -derivedDataPath "$DIR/ios-dd" test -only-testing:RandWalletTests/LiveMsauthTests ) > "$DIR/ios.log" 2>&1 || { tail -40 "$DIR/ios.log"; exit 1; } + grep -E 'Test (Case|Suite).*(passed|failed|skipped)|Executed' "$DIR/ios.log" | tail -4 + if grep -q 'LiveMsauthTests.*skipped' "$DIR/ios.log"; then echo "the iOS test was skipped"; exit 1; fi + sed -n 's/.*MSAUTH-SHARE //p' "$DIR/ios.log" | head -1 > "$DIR/share2.txt" + # The simulator's memory back before the prove. + xcrun simctl shutdown all 2>/dev/null || true +fi +grep -q '^2:' "$DIR/share2.txt" || { echo "signer 2 produced no share"; exit 1; } + +step "prove: msauth tier 18 + bundle, on this machine" +busy=$(ps -Ao rss=,pid=,command= | awk '$1 > 4194304 || ($3 ~ /(^|\/)(rand|rand-node|rand-prover)$/ && $0 ~ / prove/)' || true) +[ -z "$busy" ] || { echo "not proving beside:"; echo "$busy"; exit 1; } +/usr/bin/time -l "$RAND_BIN" --rpc "$RPC" --cpu multisig-private prove --proposal "$DIR/proposal.json" --policy "$DIR/policy.json" \ + --signature "$(tr -d '\n' < "$DIR/share0.txt")" --signature "$(tr -d '\n' < "$DIR/share2.txt")" --out "$DIR/proposal.tx" \ + > "$DIR/prove.out" 2> "$DIR/prove.err" & +PROVE_PID=$! +# KiB: the physical footprint (compressed pages included) where `footprint` exists, else RSS. +mem_kb() { + local fp + if command -v footprint >/dev/null; then + fp=$(footprint -p "$1" 2>/dev/null | sed -n 's/.*Footprint: \([0-9.]*\) \([KMG]\)B.*/\1 \2/p' | head -1) + [ -n "$fp" ] && { echo "$fp" | awk '{ m = ($2 == "G") ? 1048576 : ($2 == "M") ? 1024 : 1; printf "%d\n", $1 * m }'; return; } + fi + ps -o rss= -p "$1" 2>/dev/null | tr -d ' ' +} +started=$(date +%s); peak=0; killed=0 +while kill -0 "$PROVE_PID" 2>/dev/null; do + child=$(pgrep -P "$PROVE_PID" | head -1 || true) + if [ -n "$child" ]; then + mem=$(mem_kb "$child") + if [ -n "$mem" ]; then + [ "$mem" -gt "$peak" ] && peak=$mem + if [ "$mem" -gt "$MAX_KB" ]; then kill -9 "$child" 2>/dev/null || true; killed=1; fi + fi + fi + sleep 2 +done +set +e; wait "$PROVE_PID"; prove_rc=$?; set -e +PROVE_PID='' +echo "prove: $(( $(date +%s) - started )) s wall, sampled peak footprint $(( peak / 1024 )) MiB, exit $prove_rc" +grep -E 'real|maximum resident set size|peak memory footprint' "$DIR/prove.err" || true +cat "$DIR/prove.out" +[ "$killed" = 0 ] || { echo "the prove passed $(( MAX_KB / 1024 / 1024 )) GB and was killed"; exit 1; } +[ "$prove_rc" = 0 ] || { tail -20 "$DIR/prove.err"; exit 1; } + +step "submit; the payee scans" +R multisig-private submit --tx "$DIR/proposal.tx" +R --key "$DIR/p.key.json" balance | tee "$DIR/p.balance" +grep -Eq "^balance: $AMOUNT(\.0+)? RAND" "$DIR/p.balance" || { echo "the payee does not hold $AMOUNT RAND"; exit 1; } +R multisig-private balance --policy "$DIR/policy.json" +echo "msauth e2e: OK" diff --git a/scripts/e2e-node.sh b/scripts/e2e-node.sh index a7e6120..6935218 100755 --- a/scripts/e2e-node.sh +++ b/scripts/e2e-node.sh @@ -10,14 +10,14 @@ # RAND_NODE_BIN=… scripts/e2e-node.sh # all but the spend round trip # RAND_NODE_BIN=… RAND_PROVER_BIN=… scripts/e2e-node.sh # and the spend round trip # -# RAND_NODE_BIN a `rand-node`: a fullnode release asset (CI downloads v0.7.1) or a build. +# RAND_NODE_BIN a `rand-node`: a fullnode release asset (CI downloads v0.8.0) or a build. # Required — without it the suite would only report itself skipped. # RAND_PROVER_BIN a `rand-prover` from the same release; without it the spend case is skipped. # # Nothing is built here except the wasm core (extension/shared/core/rand_wallet_bg.wasm), when # missing — as chrome/pack.sh does. The node binaries are never built by this script: take a -# release (`gh release download v0.7.1 -R randprotocol/fullnode`) or see scripts/e2e-prover.sh for -# building them from the vendored fullnode. The suite must hold on v0.7.1, so it uses no node +# release (`gh release download v0.8.0 -R randprotocol/fullnode`) or see scripts/e2e-prover.sh for +# building them from the vendored fullnode. The suite must hold on v0.8.0, so it uses no node # feature newer than that. set -euo pipefail cd "$(dirname "$0")/.." diff --git a/scripts/i18n/native_check.py b/scripts/i18n/native_check.py index 46cbd26..d142547 100644 --- a/scripts/i18n/native_check.py +++ b/scripts/i18n/native_check.py @@ -60,6 +60,19 @@ def ios_keys(name): d = json.load(open(CAT[name])) return {k: v for k, v in d['strings'].items() if v.get('shouldTranslate', True) is not False} +def ios_ph_ok(key, s): + # Same placeholders in the same order; or, where a language reorders them, every placeholder + # positional (%2$@ ...) so that argument n has the type key's n-th placeholder has. + want = [m.group(0) for m in I_PH.finditer(key)] + got = [m.group(0) for m in I_PH.finditer(s)] + if got == want: return True + if '%%' in key or not got or not all(re.match(r'%\d+\$', g) for g in got): return False + kinds = {} + for g in got: + n, kind = re.match(r'%(\d+)\$(.*)', g).groups() + if kinds.setdefault(int(n), kind) != kind: return False + return sorted(kinds) == list(range(1, len(want) + 1)) and [kinds[i + 1] for i in range(len(want))] == [w[1:] for w in want] and len(got) == len(want) + def check_ios(code): p = os.path.join(ROOT, 'ios', 'l10n', f'{code}.json') t = json.load(open(p)) @@ -72,7 +85,7 @@ def check_ios(code): vals = v.values() if isinstance(v, dict) else [v] for s in vals: if not isinstance(s, str) or not s.strip(): errs.append(f'empty {k!r}'); continue - if [m.group(0) for m in I_PH.finditer(s)] != [m.group(0) for m in I_PH.finditer(k)] and name == 'Localizable': + if not ios_ph_ok(k, s) and name == 'Localizable': if not (isinstance(v, dict)): errs.append(f'placeholders {k!r} -> {s!r}') return errs diff --git a/ui/app.js b/ui/app.js index 0487cef..77547d4 100644 --- a/ui/app.js +++ b/ui/app.js @@ -290,6 +290,7 @@ function ensureBuiltinScreensLoaded() { import('./screens/detail.js'), import('./screens/receive.js'), import('./screens/contacts.js'), + import('./screens/signers.js'), import('./screens/faucet.js'), import('./screens/send.js'), import('./screens/swap.js'), diff --git a/ui/backend.js b/ui/backend.js index 011e51c..d40763f 100644 --- a/ui/backend.js +++ b/ui/backend.js @@ -294,6 +294,8 @@ * browser's side panel (Chrome `sidePanel`, Firefox `sidebarAction`). Must be called straight * from the user's click — both browsers open a panel only inside a gesture — and closes the * popup on success. Home offers it only in `mode: 'popup'` and only where it exists. + * - `platform.saveText?({name, text})` → Promise. Saves a small non-secret text (a multisig + * share) as a file: a download on web and extension, the Downloads folder on desktop (the path). * - `platform.paste?()` → string. Reads the clipboard, for the send screen's Paste affordance * (a shielded address is pasted, never typed). Optional because reading the clipboard needs a * permission some shells will not have: where it is missing, no Paste button is offered at all @@ -342,6 +344,11 @@ * exactly as `send.send`'s do, and **it refuses exactly what `estimate` refuses, before a * proof starts** — the two run one shared list, because a gate on one and not the other is * a proof spent on a transaction the chain was always going to refuse. + * - `signers?` — private multisig signer profiles (spec 2026-10-11-msauth-signing-design.md): + * `list()`, `preview({policy, key})`, `add({label, policy, key, password})`, `remove(id, password)`, + * `check(id, proposal, expectedRecipients)` → `{summary, signable, refusal?, head?, blocksLeft?}`, + * `sign(id, proposal, expectedRecipients, password)` → `{share, index}`, `verifyShare(id, proposal, share)` → `{index}`. + * Screens offer Settings → Multisig signers only where it exists. * - `prover?` — a whole OPTIONAL GROUP, delegated proving (spec 2026-09-28, Phases 1 and 2): a * paired prover makes the BUNDLE proof for a device that cannot. Since split authorisation * (bundle guest v3, every chain since 17) the job carries the wallet's **viewing key** and a diff --git a/ui/engine/backend-shared.js b/ui/engine/backend-shared.js index 0201455..4991df9 100644 --- a/ui/engine/backend-shared.js +++ b/ui/engine/backend-shared.js @@ -92,6 +92,7 @@ import { import { runPhased } from './execute.js'; import { normalizeInvokeRequest, invokeEffects, invokeError } from './invoke.js'; import { translateCoreError } from './core-errors.js'; +import { makeSigners } from './signers.js'; import { t } from '../i18n.js'; /** @@ -167,7 +168,7 @@ const FALLBACK = Object.freeze({ 'https://rpc.randprotocol.org', ]), explorerUrl: 'https://randscan.org', - chainId: 20, + chainId: 21, decimals: 9, autoLockMin: 15, theme: 'system', @@ -728,6 +729,11 @@ export function makeSharedBackend({ async function loadNotes() { return engine.loadStore(); } + const signers = makeSigners({ + c, storage, requireUnlocked, openVault, passwordAttempt, requireVerifiedChain, getSettings, currentRpcUrl, + setSession: (record) => storage.session.set(K.unlocked, record), + }); + // ------------------------------------------------------------------------- the unlock session - // `storage.session` is memory only. Nothing else holds the plaintext between calls. async function unlockedSession() { @@ -924,36 +930,48 @@ export function makeSharedBackend({ * count is touched, so a user whose storage got corrupted is not also locked out by a backoff * that grows every time they try. */ - function openVault(password) { + /** + * One password attempt through the shared throttle: queued, delayed by the failure count, counted + * before the KDF runs, cleared on success. `open()` resolves the plaintext or throws; a record + * error (`isVaultRecordError`) is rethrown and not counted. Used by the unlock and by every + * signer signature (signers.js). + */ + function passwordAttempt(open) { return enqueueAttempt(async () => { - const vault = await storage.get(K.vault); - if (!vault) throw new Error(t('no wallet on this device')); - checkVault(vault); // VaultVersionError / VaultDamagedError — not an attempt const rec = await failureRecord(); await sleep(unlockDelayMs(rec.count)); // Persisted before the KDF runs, not after it resolves. await bumpFailures(rec); - let key; + let value; try { - key = await decryptSecret(password, vault); + value = await open(); } catch (err) { - if (isVaultRecordError(err)) throw err; // cannot happen after checkVault, but do not count it + if (isVaultRecordError(err)) throw err; // a damaged record is not a wrong password: not counted return null; } await clearFailures(); - return key; + return value; }); } + async function openVault(password) { + const vault = await storage.get(K.vault); + if (!vault) throw new Error(t('no wallet on this device')); + checkVault(vault); // VaultVersionError / VaultDamagedError — not an attempt + return passwordAttempt(() => decryptSecret(password, vault)); + } + /** * Records a freshly created/imported/unlocked wallet. The vault is written first. `pairing` is * the paired prover's `{token, kemEk, url, fingerprint}`, decrypted with the same password * (ruling R2), when there is one. */ - async function startSession(info, pairing) { + async function startSession(info, pairing, signerState = { profiles: [], unreadable: false }) { const record = { spend_key: info.spend_key, viewing_key: info.viewing_key }; const p = pairingOf(pairing); if (p) record.prover = p; + record.signers = signerState.profiles; + if (signerState.unreadable) record.signersUnreadable = true; await storage.session.set(K.unlocked, record); // A wallet wiped and created again in the same page had no multi-tab coordination at all // until a reload, because `wipe()` closed the channel for good. @@ -1005,7 +1023,7 @@ export function makeSharedBackend({ if (!key) throw new Error('wrong password'); const info = await c.walletInfo(key); await storage.set(K.wallet, { ...((await storage.get(K.wallet)) || {}), address: info.address, pk: info.pk }); - await startSession(info, await openProverPairing(password)); + await startSession(info, await openProverPairing(password), await signers.openOnUnlock(password)); return { address: info.address, pk: info.pk }; }, @@ -2732,7 +2750,7 @@ export function makeSharedBackend({ closeChannel(); } - const backend = { wallet, sync, assets, send, faucet, rpc, settings, platform, address, contacts, prover, dispose }; + const backend = { wallet, sync, assets, send, faucet, rpc, settings, platform, address, contacts, prover, signers: signers.group, dispose }; // `bridge` is OPTIONAL in the contract and is not in BACKEND_SHAPE: a shell that supplied no // `executeWithdraw` simply does not have the group, and every screen feature-detects it // (`ctx.backend.bridge?.canWithdraw`). Both real shells do supply one — the wasm shell's always diff --git a/ui/engine/core-errors.js b/ui/engine/core-errors.js index c3b6b88..0574d1d 100644 --- a/ui/engine/core-errors.js +++ b/ui/engine/core-errors.js @@ -70,6 +70,22 @@ const EXACT = new Map([ ['cell keys must be strictly ascending', () => t('cell keys must be strictly ascending')], ['the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)', () => t('the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)')], + // ---- private multisig signing (`msauth_*`, fullnode randprotocol-msig) ---- + ['the transaction does not hash to the proposal\'s binding', () => t('the transaction does not hash to the proposal\'s binding')], + ['the proposal\'s signing_message is not its binding\'s', () => t('the proposal\'s signing_message is not its binding\'s')], + ['the proposal\'s auth_commit is not this policy\'s account', () => t('the proposal\'s auth_commit is not this policy\'s account')], + ['the proposal\'s expected digest is not its bundle\'s', () => t('the proposal\'s expected digest is not its bundle\'s')], + ['the transaction carries no bundle', () => t('the transaction carries no bundle')], + ['the transaction carries an action; a private multisig proposal is a payment from its bundle alone', () => t('the transaction carries an action; a private multisig proposal is a payment from its bundle alone')], + ['the witness\'s value cannot be checked: its nk or salt is not this proposal\'s', () => t('the witness\'s value cannot be checked: its nk or salt is not this proposal\'s')], + ['this key is not one of the policy\'s signers', () => t('this key is not one of the policy\'s signers')], + ['a share is index:hex', () => t('a share is index:hex')], + ['a signer key is a rand-node keygen file or 64 hex characters', () => t('a signer key is a rand-node keygen file or 64 hex characters')], + ['a signer seed is 64 hex characters', () => t('a signer seed is 64 hex characters')], + ['expected_recipients must be a list of addresses', () => t('expected_recipients must be a list of addresses')], + ['the key file\'s public_key is not the one its seed derives', () => t('the key file\'s public_key is not the one its seed derives')], + ['the key file\'s address is not the one its seed derives', () => t('the key file\'s address is not the one its seed derives')], + ['no expected recipient given: the payee was NOT checked', () => t('no expected recipient given: the payee was NOT checked')], ]); /** @@ -137,6 +153,24 @@ const PATTERNED = [ + 'this wallet carries {ours} — refusing to prove a bundle it cannot authorise', { ours: m[1] })], [/^this chain's auth guest is ([0-9a-f]+); this wallet carries ([0-9a-f]+) — refusing to prove a v3 bundle it cannot authorise; update the wallet$/, (m) => t('this chain\'s auth guest is {theirs}; this wallet carries {ours} — refusing to prove a v3 bundle it cannot authorise; update the wallet', { theirs: m[1], ours: m[2] })], + // ---- private multisig signing ---- + [/^this proposal is for another policy: it names ([0-9a-f]+), this policy is ([0-9a-f]+)$/, + (m) => t('this proposal is for another policy: it names {theirs}, this policy is {ours}', { theirs: m[1], ours: m[2] })], + [/^output (\d+) does not open as this account's: refusing to sign value it cannot see$/, + (m) => t('output {slot} does not open as this account\'s: refusing to sign value it cannot see', { slot: m[1] })], + [/^the witness's value does not match the transaction: input (\d+) does not nullify to the bundle's nullifier$/, + (m) => t('the witness\'s value does not match the transaction: input {input} does not nullify to the bundle\'s nullifier', { input: m[1] })], + [/^the witness's value does not match the outputs: asset (\d+) has (\d+) in and (\d+) out with the fee and burns$/, + (m) => t('the witness\'s value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns', { asset: m[1], held: m[2], spent: m[3] })], + [/^the proposal expires at (\d+), outside its bundle's window \((\d+), (\d+)\]$/, + (m) => t('the proposal expires at {expires}, outside its bundle\'s window ({from}, {to}]', { expires: m[1], from: m[2], to: m[3] })], + [/^the transaction is for chain (\d+), the proposal says chain (\d+)$/, + (m) => t('the transaction is for chain {tx}, the proposal says chain {proposal}', { tx: m[1], proposal: m[2] })], + [/^signer (\d+)'s signature does not verify over this proposal$/, (m) => t('signer {index}\'s signature does not verify over this proposal', { index: m[1] })], + [/^signer (\d+) is not in this policy \(it has (\d+) signers\)$/, (m) => t('signer {index} is not in this policy (it has {count} signers)', { index: m[1], count: m[2] })], + [/^expected recipient (\S+): ([\s\S]+)$/, (m, inner) => t('expected recipient {address}: {reason}', { address: m[1], reason: inner(m[2]) })], + [/^the policy does not parse: ([\s\S]+)$/, (m) => t('the policy does not parse: {reason}', { reason: m[1] })], + [/^the proposal does not parse: ([\s\S]+)$/, (m) => t('the proposal does not parse: {reason}', { reason: m[1] })], // ---- proving on this device ---- [/^proving failed: ([\s\S]+)$/, (m) => t('proving failed: {reason}', { reason: m[1] })], [/^proving the spend authorisation failed: ([\s\S]+)$/, (m) => t('proving the spend authorisation failed: {reason}', { reason: m[1] })], diff --git a/ui/engine/crypto.js b/ui/engine/crypto.js index 150a795..c37bc8c 100644 --- a/ui/engine/crypto.js +++ b/ui/engine/crypto.js @@ -105,15 +105,38 @@ async function deriveKey(password, salt, iterations, usage) { ); } -/** Returns a vault object safe to persist. */ -export async function encryptSecret(password, plaintext) { +/** + * Returns a vault object safe to persist, over raw bytes (a signer seed). Same format as + * `encryptSecret`: a string record is just these bytes UTF-8 encoded. + */ +export async function encryptBytes(password, bytes) { const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await deriveKey(password, salt, ITERATIONS, ['encrypt']); - const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, enc.encode(plaintext))); + const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, bytes)); return { v: VAULT_VERSION, kdf: 'pbkdf2-sha256', iter: ITERATIONS, salt: b64(salt), iv: b64(iv), ct: b64(ct) }; } +/** Returns a vault object safe to persist. */ +export async function encryptSecret(password, plaintext) { + return encryptBytes(password, enc.encode(plaintext)); +} + +/** + * Opens a byte record into a fresh `Uint8Array` the caller must `fill(0)` when done: a typed array + * is the one buffer JavaScript can overwrite (a string cannot be wiped). That is all this offers; + * copies the engine makes elsewhere are not tracked. Same refusals as `decryptSecret`. + */ +export async function decryptBytes(password, vault) { + const iterations = checkVault(vault); + const key = await deriveKey(password, unb64(vault.salt), iterations, ['decrypt']); + try { + return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: unb64(vault.iv) }, key, unb64(vault.ct))); + } catch { + throw new Error('wrong password'); + } +} + /** * Opens a vault. Throws `VaultVersionError`/`VaultDamagedError` for a record that could never be * opened by any password (neither of which is a failed attempt), and plain `wrong password` when @@ -124,13 +147,11 @@ export async function encryptSecret(password, plaintext) { * the storage it sits in is, and `{"iter": 1}` would otherwise turn the KDF off. */ export async function decryptSecret(password, vault) { - const iterations = checkVault(vault); - const key = await deriveKey(password, unb64(vault.salt), iterations, ['decrypt']); + const bytes = await decryptBytes(password, vault); try { - const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: unb64(vault.iv) }, key, unb64(vault.ct)); - return dec.decode(pt); - } catch { - throw new Error('wrong password'); + return dec.decode(bytes); + } finally { + bytes.fill(0); } } diff --git a/ui/engine/signers.js b/ui/engine/signers.js new file mode 100644 index 0000000..fbd9836 --- /dev/null +++ b/ui/engine/signers.js @@ -0,0 +1,259 @@ +// Private multisig signer profiles (spec docs/superpowers/specs/2026-10-11-msauth-signing-design.md +// §3, §4) for the shared engine: web, extension, desktop. +// +// A profile is {label, policy, seed}; both halves are secret (the seed signs, the policy's `msk` +// sees every note of the account). Storage holds two vault records: `signerPolicies`, ONE +// `encryptSecret` record of every profile's {id, label, policy, index, policyHash}, opened at unlock +// into the session beside the spend key; and `signerSeeds`, one `encryptBytes` record per seed, +// opened only by `sign()` with the password typed for that signature — never at unlock, never kept. +import { encryptSecret, decryptSecret, encryptBytes, decryptBytes } from './crypto.js'; +import { t } from '../i18n.js'; + +export const SIGNER_POLICIES_KEY = 'signerPolicies'; +export const SIGNER_SEEDS_KEY = 'signerSeeds'; +/** Blocks a proposal needs before its window closes: the CLI's `EXPIRY_MARGIN` (fullnode d5b2e652). */ +export const EXPIRY_MARGIN = 16; + +export class SignerRefusal extends Error { + constructor(code, message, detail = {}) { + super(message); + this.name = 'SignerRefusal'; + this.code = code; + this.definite = true; + Object.assign(this, detail); + } +} + +/** A pasted or opened file: a leading byte-order mark dropped, whitespace trimmed. */ +export function cleanText(s) { + return String(s ?? '').replace(/^\uFEFF/, '').trim(); +} + +export const hexOf = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); +export function bytesOf(hex) { + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i += 1) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); + return out; +} +const randomId = () => hexOf(crypto.getRandomValues(new Uint8Array(8))); + +function validProfiles(v) { + if (!Array.isArray(v)) return []; + return v.filter((p) => p && typeof p.id === 'string' && typeof p.label === 'string' && typeof p.policy === 'string' + && Number.isInteger(p.index) && typeof p.policyHash === 'string'); +} + +export const isZero = (a) => (typeof a === 'string' ? /^0+$/.test(a.trim()) : a === 0); + +/** A paying output whose asset is a whole number other than 0. Anything else is RAND (fails closed). */ +export const isNonRand = (o) => o != null && o.asset != null && o.asset !== '' && Number.isInteger(Number(o.asset)) && Number(o.asset) !== 0; +/** Of the paying outputs, the owner fingerprints that receive a non-RAND asset and those that receive RAND. */ +export function payeeAssets(paying) { + const fp = (o) => (typeof o.owner_fingerprint === 'string' && o.owner_fingerprint !== '' ? o.owner_fingerprint : null); + const tokenOwners = new Set(paying.filter(isNonRand).map(fp).filter(Boolean)); + const randOwners = new Set(paying.filter((o) => !isNonRand(o)).map(fp).filter(Boolean)); + return { tokenOwners, randOwners }; +} + +const amountless = (entry) => { const i = String(entry).indexOf(':'); return i < 0 || String(entry).slice(i + 1).trim() === ''; }; + +/** Whether a checked proposal may be signed now, by the CLI's rule, on this wallet's chain. */ +export function verdict({ summary, head, chainId, genesis, url, recipients }) { + const malformed = () => ({ signable: false, refusal: { code: 'malformed-summary', message: t('The proposal check returned an incomplete answer, so this proposal is not signed.') } }); + if (summary === null || typeof summary !== 'object') return malformed(); + // Spec §4 step 3: never sign on an unchecked payee (a coordinator could pay its own key). + const unchecked = () => ({ signable: false, refusal: { code: 'unchecked-recipient', message: t('Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.') } }); + if (summary.recipients_checked !== true) return unchecked(); + // A field the rule reads that is missing or malformed fails closed: NaN compares false. + const outs = summary.outputs; + const amountOk = (a) => (typeof a === 'string' ? /^\s*\d+\s*$/.test(a) : Number.isSafeInteger(a) && a >= 0); + const outputOk = (o) => o !== null && typeof o === 'object' && !Array.isArray(o) && typeof o.to_account === 'boolean' && amountOk(o.amount); + // The review shows `lines` verbatim: missing, or anything but an array of strings, is malformed. + if (!Array.isArray(summary.lines) || !summary.lines.every((l) => typeof l === 'string')) return malformed(); + if (!Array.isArray(outs) || !outs.every(outputOk)) return malformed(); + // No intended payment named at all while something pays out: unchecked, whatever the core said. + const paying = outs.filter((o) => o.to_account !== true && !isZero(o.amount)); + if (Array.isArray(recipients) && recipients.length === 0 && paying.length > 0) return unchecked(); + // An amount pin is required on every entry, except an entry whose own owner fingerprint owns a + // non-RAND paying output and NO RAND paying output: the core refuses a pin on a non-RAND output, + // so that entry is the address alone. The exception is per row, so one small non-RAND output + // cannot unpin the RAND rows, nor the RAND paid to its own payee (1 unit of a token beside an + // inflated RAND output to the same payee is refused). + if (Array.isArray(recipients) && recipients.some(amountless)) { + const exp = summary.expected; + if (!Array.isArray(exp)) return unchecked(); + const { tokenOwners, randOwners } = payeeAssets(paying); + const allowed = (entry) => exp.some((x) => x !== null && typeof x === 'object' && x.entry === entry + && typeof x.fingerprint === 'string' && x.fingerprint !== '' && tokenOwners.has(x.fingerprint) + && !randOwners.has(x.fingerprint)); + if (!recipients.filter(amountless).every(allowed)) return unchecked(); + } + if (outs.some((o) => o.to_account !== true && !isZero(o.amount) && !(typeof o.matches === 'string' && o.matches.trim() !== ''))) return unchecked(); + const sound = Number.isSafeInteger(summary.expires_at) && summary.expires_at >= 0 + && Number.isSafeInteger(summary.chain_id) + && Number.isInteger(summary.binding_domain) && summary.binding_domain >= 0 + && typeof summary.genesis === 'string' && /^[0-9a-fA-F]+$/.test(summary.genesis); + if (!sound) return malformed(); + if (head == null) { + return { signable: false, refusal: { code: 'no-node', node: url, message: t('No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.', { url: url || '—' }) } }; + } + if (Number(summary.chain_id) !== Number(chainId)) { + return { signable: false, refusal: { code: 'wrong-chain', message: t('This proposal is for chain {theirs}; this wallet is on chain {ours}.', { theirs: String(summary.chain_id), ours: String(chainId) }) } }; + } + if (Number(summary.binding_domain) >= 1 && String(summary.genesis).toLowerCase() !== String(genesis || '').toLowerCase()) { + return { signable: false, refusal: { code: 'wrong-genesis', message: t('This proposal is bound to another genesis than the one this wallet\'s node serves.') } }; + } + if (head + EXPIRY_MARGIN >= Number(summary.expires_at)) { + return { signable: false, refusal: { code: 'expired', message: t('This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.', { expires: String(summary.expires_at), head: String(head) }) } }; + } + return { signable: true, head, blocksLeft: Number(summary.expires_at) - head }; +} + +// Intended payments, `addr:amount` each, passed in full and unchanged but for cleanText: an entry +// that is blank or lacks its address or its amount is never dropped, it is refused. +const payees = (list) => (Array.isArray(list) ? list : []).map(cleanText); +// An empty address is always refused; a missing amount only once the summary shows no non-RAND output. +const blankPayee = (list) => payees(list).some((a) => a === '' || a.startsWith(':')); +const blankRefusal = () => new SignerRefusal('unchecked-recipient', t('Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.')); + +export function makeSigners(deps) { + const { c, storage, requireUnlocked, setSession, openVault, passwordAttempt, requireVerifiedChain, getSettings, currentRpcUrl } = deps; + + async function profiles() { + const u = await requireUnlocked(); + if (u.signersUnreadable) throw new SignerRefusal('signers-unreadable', t('the saved signer profiles could not be opened; nothing was changed. Restore this device\'s storage or wipe the wallet and restore it from its key')); + return validProfiles(u.signers); + } + async function profileOf(id) { + const p = (await profiles()).find((x) => x.id === id); + if (!p) throw new Error(t('No such signer profile.')); + return p; + } + async function requirePassword(password) { + if ((await openVault(password)) === null) throw new Error('wrong password'); + } + async function writeProfiles(password, list) { + await storage.set(SIGNER_POLICIES_KEY, await encryptSecret(password, JSON.stringify(list))); + await setSession({ ...(await requireUnlocked()), signers: list }); + } + async function row(p) { + const info = await c.msauthPolicyInfo(p.policy); + return { id: p.id, label: p.label, index: p.index, fingerprint: info.fingerprint, threshold: info.threshold, signers: info.signers, address: info.address }; + } + async function list() { + return Promise.all((await profiles()).map(row)); + } + + /** The head from a node that passed this wallet's chain check; any failure is "no node". */ + async function judge(summary, recipients) { + const settings = await getSettings(); + let url = ''; + try { url = await currentRpcUrl(); } catch { /* no endpoint configured */ } + try { + const { client, url: u, identity } = await requireVerifiedChain(); + const h = await client.head(); + const height = Number(h && h.height); + if (!Number.isSafeInteger(height) || height < 0) throw new Error('no height'); + return verdict({ summary, head: height, chainId: settings.chainId, genesis: identity && identity.genesis, url: u, recipients }); + } catch { + return verdict({ summary, head: null, url, recipients }); + } + } + + const group = { + list, + async preview({ policy, key }) { + const pol = cleanText(policy); + const info = await c.msauthPolicyInfo(pol); + const signer = await c.msauthSignerImport(cleanText(key), pol); + return { fingerprint: info.fingerprint, threshold: info.threshold, signers: info.signers, address: info.address, policyHash: info.policy_hash, index: signer.index, publicKey: signer.public_key }; + }, + async add({ label, policy, key, password }) { + const name = String(label ?? '').trim(); + if (!name || [...name].length > 64) throw new Error(t('a signer label is 1-64 characters')); + const pol = cleanText(policy); + const info = await c.msauthPolicyInfo(pol); + const signer = await c.msauthSignerImport(cleanText(key), pol); + const current = await profiles(); + const same = current.find((p) => p.policyHash === info.policy_hash && p.index === signer.index); + if (same) throw new Error(t('this signer is already saved as {label}', { label: same.label })); + await requirePassword(password); + const id = randomId(); + const seed = bytesOf(signer.seed); + try { + const seeds = (await storage.get(SIGNER_SEEDS_KEY)) || {}; + seeds[id] = await encryptBytes(password, seed); + await storage.set(SIGNER_SEEDS_KEY, seeds); + } finally { + seed.fill(0); + } + await writeProfiles(password, [...current, { id, label: name, policy: pol, index: signer.index, policyHash: info.policy_hash }]); + return list(); + }, + async remove(id, password) { + await profileOf(id); + await requirePassword(password); + const seeds = (await storage.get(SIGNER_SEEDS_KEY)) || {}; + delete seeds[id]; + await storage.set(SIGNER_SEEDS_KEY, seeds); + await writeProfiles(password, (await profiles()).filter((p) => p.id !== id)); + return list(); + }, + // The proposal is a parameter, never stored: it carries the account's witness (§3). + async check(id, proposal, expectedRecipients = []) { + const p = await profileOf(id); + if (blankPayee(expectedRecipients)) { const r = blankRefusal(); return { summary: null, signable: false, refusal: { code: r.code, message: r.message } }; } + const summary = await c.msauthCheck(cleanText(proposal), p.policy, payees(expectedRecipients)); + return { summary, ...(await judge(summary, payees(expectedRecipients))) }; + }, + async sign(id, proposal, expectedRecipients, password) { + const p = await profileOf(id); + const text = cleanText(proposal); + if (blankPayee(expectedRecipients)) throw blankRefusal(); + const summary = await c.msauthCheck(text, p.policy, payees(expectedRecipients)); + const v = await judge(summary, payees(expectedRecipients)); + if (!v.signable) throw new SignerRefusal(v.refusal.code, v.refusal.message, { node: v.refusal.node }); + const rec = ((await storage.get(SIGNER_SEEDS_KEY)) || {})[id]; + if (!rec) throw new Error(t('This signer profile has no key on this device: remove it and add it again.')); + const seed = await passwordAttempt(() => decryptBytes(password, rec)); + if (!seed) throw new Error('wrong password'); + try { + // The backoff above can last up to 30 s: re-read the head and re-run the rule before signing. + const again = await judge(summary, payees(expectedRecipients)); + if (!again.signable) throw new SignerRefusal(again.refusal.code, again.refusal.message, { node: again.refusal.node }); + // The hex copy below cannot be wiped (a JS string); it is unreachable after this call, and + // wallet-core holds its own copy in Zeroizing. The bytes are wiped here. + // The same list the check used: the core re-runs the payee check and refuses on its own. + const share = await c.msauthSign(text, p.policy, hexOf(seed), payees(expectedRecipients)); + return { share, index: Number(String(share).split(':')[0]) }; + } finally { + seed.fill(0); + } + }, + async verifyShare(id, proposal, share) { + const p = await profileOf(id); + const s = cleanText(share); + await c.msauthVerifyShare(cleanText(proposal), p.policy, s); + return { index: Number(s.split(':')[0].trim()) }; + }, + }; + + /** + * The profiles, opened with the password that just opened the vault: `{profiles, unreadable}`. + * A record that exists but does not open (damaged, or from a newer build) is `unreadable: true` + * and is never overwritten — unlock must not fail on it, but add/remove/list refuse. + */ + async function openOnUnlock(password) { + const rec = await storage.get(SIGNER_POLICIES_KEY); + if (!rec) return { profiles: [], unreadable: false }; + try { + const list = JSON.parse(await decryptSecret(password, rec)); + if (!Array.isArray(list)) throw new Error('not a list'); + return { profiles: validProfiles(list), unreadable: false }; + } catch { + return { profiles: [], unreadable: true }; + } + } + + return { group, openOnUnlock, profileOf, deps }; +} diff --git a/ui/engine/wallet.js b/ui/engine/wallet.js index c44541d..6b4448f 100644 --- a/ui/engine/wallet.js +++ b/ui/engine/wallet.js @@ -384,6 +384,13 @@ export function coreApi(core) { finishProof: (pending, reply_hex) => call('finish_proof', { pending, reply_hex }), parseProverLink: (link) => call('parse_prover_link', { link }), proverFingerprint: (kem_ek) => call('prover_fingerprint', { kem_ek }), + // Private multisig signing (wallet-core msauth.rs): check, sign, verify a share, import a + // signer key, describe a policy. `seed` is 64 hex; never log a call's params. + msauthCheck: (proposal, policy, expected_recipients = []) => call('msauth_check', { proposal, policy, expected_recipients }), + msauthSign: (proposal, policy, seed, expected_recipients = []) => call('msauth_sign', { proposal, policy, seed, expected_recipients }), + msauthVerifyShare: (proposal, policy, share) => call('msauth_verify_share', { proposal, policy, share }), + msauthSignerImport: (input, policy) => call('msauth_signer_import', policy ? { input, policy } : { input }), + msauthPolicyInfo: (policy) => call('msauth_policy_info', { policy }), // A bridge burn. `plan_burn` selects both groups' notes (the token to burn and the RAND to pay // with) and costs nothing; `prove_burn` proves the ONE bundle they share, which since chain 14 // is a single proof of about two minutes rather than two of about three and a half. diff --git a/ui/locales/ar.js b/ui/locales/ar.js index 9cf850b..698a9f6 100644 --- a/ui/locales/ar.js +++ b/ui/locales/ar.js @@ -8,6 +8,7 @@ // Plural objects carry all six Arabic categories (zero one two few many other). export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "بقي {blocks} كتلة لإثباته.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { zero: "{count} نظراء", one: "{count} نظير", two: "{count} نظيران", few: "{count} نظراء", many: "{count} نظيرًا", other: "{count} نظير" }, "{count} peers": { zero: "{count} نظراء", one: "{count} نظير", two: "{count} نظيران", few: "{count} نظراء", many: "{count} نظيرًا", other: "{count} نظير" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} متوقف", "{label} is on": "{label} مفعّل", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "لم يعد {label} يفتح هذه المحفظة (تغيّرت كلمة مرورها). افتحها بكلمة المرور، ثم فعّل {label} من جديد في الإعدادات.", + "{matched} of {total} payments matched.": "تطابقت {matched} من أصل {total} من المدفوعات.", "{member} answered with another key than the one this wallet pins": "أجاب {member} بمفتاح غير المفتاح الذي تثبّته هذه المحفظة", "{member} did not answer": "لم يُجب {member}", "{member} does not take this wallet's jobs": "لا يقبل {member} مهام هذه المحفظة", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "لا يمكن سحب {symbol}", "{symbol} has {decimals} decimal places — that is more.": "لـ {symbol} عدد {decimals} من المنازل العشرية — وهذا المبلغ يتجاوزها.", "{symbol} is released as the coin you pick, on that coin's own chain.": "يُصرف {symbol} بالعملة التي تختارها، على سلسلة تلك العملة نفسها.", + "{threshold} of {count} · signer {index}": "{threshold} من {count} · الموقّع {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} من {count} · هذا المفتاح هو الموقّع {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "لـ {token} على السلسلة {chain} عدد {decimals} من المنازل العشرية: يجب أن يكون المبلغ ورسوم المُرحِّل من مضاعفات {unit}", "{units} units of asset {asset}": "{units} وحدة من الأصل {asset}", "{url} answered first — chain {chain} · height {height}.": "أجاب {url} أولًا — السلسلة {chain} · الارتفاع {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "يجب أن يبدأ عنوان المُثبِت بـ https://.", "A prover is making the proof": "مُثبِت يُعِدّ الإثبات", "A prover sees your history": "المُثبِت يرى سجلك", + "a share is index:hex": "حصة التوقيع بصيغة index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "محفظة محمية لشبكة Rand. تبقى الأرصدة والسجل خاصة — كل إرسال يُثبَت على هذا الجهاز.", + "a signer key is a rand-node keygen file or 64 hex characters": "مفتاح الموقّع هو ملف keygen من rand-node أو 64 حرفًا سداسيًا عشريًا", + "a signer label is 1-64 characters": "تسمية الموقّع من 1-64 حرفًا", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "يُحفظ ملف الموقّع في الخزنة المشفّرة لهذه المحفظة. يطلب كل توقيع كلمة المرور مرة أخرى.", + "a signer seed is 64 hex characters": "بذرة الموقّع هي 64 حرفًا سداسيًا عشريًا", "A site": "موقع", "a spend key is 64 hex characters, or a wallet.key.json": "مفتاح الإنفاق 64 حرفًا ست عشريًا، أو ملف wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "يُجرَّب تجزئة المعاملة أولًا، وإن لم يُعثر على شيء تُجرَّب التجزئة نفسها ككتلة. والأرقام وحدها تبحث عن كتلة بارتفاعها.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "قد يُرفض السحب إلى سلسلة لا يدرجها. لا يُنفق شيء حتى يُثبَت.", "About": "حول", "about {left} left": "يتبقى نحو {left}", + "Account fingerprint — compare it with every other signer before you save": "بصمة الحساب — قارنها مع كل موقّع آخر قبل أن تحفظ", "Activity": "النشاط", "Add a contact": "إضافة جهة اتصال", + "Add a signer": "إضافة موقّع", + "Add payment": "إضافة دفعة", + "Add the policy file and your signer key to sign for a private multisig account.": "أضف ملف السياسة ومفتاح الموقّع الخاص بك للتوقيع عن حساب متعدد التوقيع خاص.", "Address": "العنوان", "Address copied": "نُسخ العنوان", "Address or payment link": "عنوان أو رابط دفع", @@ -115,14 +128,18 @@ export default { "almost done": "أوشك على الانتهاء", "also tried {endpoints}": "جُرِّبت أيضًا {endpoints}", "Amount": "المبلغ", + "Amount in RAND": "المبلغ بعملة RAND", "amount must be greater than zero": "يجب أن يكون المبلغ أكبر من صفر", "amount overflow": "تجاوز المبلغ الحد", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "لا يمكن التحقق من المبالغ في المدفوعات بغير RAND: اترك المبلغ فارغًا لها.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "يتلقى هذا المستفيد RAND ورمزًا معًا: مدفوعات RAND والرموز إلى مستفيد واحد تحتاج إلى مقترحات منفصلة.", "amounts overflow": "تجاوزت المبالغ الحد", "an action is {n} characters, more than {max}": { zero: "الإجراء من {n} أحرف، أي أكثر من {max}", one: "الإجراء من {n} حرف، أي أكثر من {max}", two: "الإجراء من {n} حرفين، أي أكثر من {max}", few: "الإجراء من {n} أحرف، أي أكثر من {max}", many: "الإجراء من {n} حرفًا، أي أكثر من {max}", other: "الإجراء من {n} حرف، أي أكثر من {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "عنوان (rand1…)، أو رابط دفع (randpay:…)، أو اسم جهة اتصال محفوظة.", "An address here is hexadecimal — 0-9 and a-f only.": "العنوان هنا ست عشري — 0-9 وa-f فقط.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "العنوان على هذه السلسلة 20 بايت — 40 حرفًا ست عشريًا. وهذا طوله {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "العنوان على هذه السلسلة 32 بايت — 64 حرفًا ست عشريًا. وهذا طوله {length}.", + "An amount is a number of RAND, such as 1.5.": "المبلغ هو عدد من RAND، مثل 1.5.", "an unknown chain": "سلسلة غير معروفة", "another tab changed this wallet while it was syncing; try again": "غيّر تبويب آخر هذه المحفظة أثناء المزامنة، حاول مرة أخرى", "Another tab is syncing": "تبويب آخر يُجري المزامنة", @@ -189,11 +206,16 @@ export default { "Chains known": "السلاسل المعروفة", "Change back to you": "الباقي يعود إليك", "Change it": "غيّره", + "Check": "تحقق", + "Check a share": "التحقق من حصة توقيع", "Check Activity": "راجع النشاط", "Check Activity first": "راجع النشاط أولًا", "Check before saving": "تحقّق قبل الحفظ", "Check fingerprint": "تحقّق من البصمة", + "Check payments": "التحقق من المدفوعات", + "Check proposal": "التحقق من الاقتراح", "Check settings": "راجع الإعدادات", + "Check share": "التحقق من حصة التوقيع", "Checking it against the chain before anything is proved.": "جارٍ التحقق منه مقابل السلسلة قبل إثبات أي شيء.", "Checking the pool and your notes…": "جارٍ التحقق من المجمّع ومن سنداتك…", "Checking the swap…": "جارٍ التحقق من المبادلة…", @@ -220,6 +242,7 @@ export default { "Continue": "متابعة", "Copied": "نُسخ", "Copied as JSON": "نُسخ بصيغة JSON", + "Copy": "نسخ", "Copy address": "نسخ العنوان", "Copy as JSON": "نسخ بصيغة JSON", "Copy link": "نسخ الرابط", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "أدخل مبلغًا للإرسال.", "Enter an amount.": "أدخل مبلغًا.", "Enter at least 10 characters": "أدخل 10 أحرف على الأقل", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "أدخل كل دفعة تنوي إجراءها: العنوان والمبلغ بعملة RAND. الدفعة إلى العنوان نفسه مرتين تُدخل مرة واحدة بالمجموع. لا يمكن توقيع مخرج لا يطابق أيًّا منها.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "أدخل عنوان كل مستلم: لا يمكن توقيع مخرج لا يطابق العنوان الذي أدخلته أو عنوانه فارغ.", + "Enter the address of every payment.": "أدخل عنوان كل دفعة.", "Enter the address this should be released to.": "أدخل العنوان الذي يُصرف إليه هذا المبلغ.", "Enter the address you are sending to.": "أدخل العنوان الذي ترسل إليه.", "Enter the amount as a number, for example 1.25.": "أدخل المبلغ رقمًا، مثل 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "أدخل مبلغ كل دفعة: فهو ما يمنع التجزئة أو تضخيم الدفعة.", "Enter the first 4 characters of your recovery key.": "أدخل أول 4 أحرف من مفتاح الاستعادة.", "Enter the relayer fee as a number, for example 1.25.": "أدخل رسوم المُرحِّل رقمًا، مثل 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "أدخل كلمة مرور هذه المحفظة — الإقران مختوم بها.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "أدخل مفتاح الاستعادة.", "Epoch": "الحقبة", "Exactly those four letters, in capitals.": "هذه الأحرف الأربعة بالضبط، بأحرف كبيرة.", + "expected recipient {address}: {reason}": "المستلم المتوقع {address}: {reason}", + "expected_recipients must be a list of addresses": "يجب أن يكون expected_recipients قائمة عناوين", "Explore": "استكشاف", "Export spend key": "تصدير مفتاح الإنفاق", "Exporting the spend key is behind your password.": "تصدير مفتاح الإنفاق محميّ بكلمة المرور.", @@ -288,6 +317,7 @@ export default { "Fee": "الرسوم", "fee must be at least {fee} RAND (the bridge burn floor)": "يجب ألا تقل الرسوم عن {fee} RAND (الحد الأدنى لحرق الجسر)", "fee must be at least {fee} RAND (the bundle floor)": "يجب ألا تقل الرسوم عن {fee} RAND (الحد الأدنى للحزمة)", + "Fee: {fee} RAND · expires at height {height}": "الرسوم: {fee} RAND · تنتهي عند الارتفاع {height}", "Filter activity by asset": "تصفية النشاط حسب الأصل", "Fingerprint": "البصمة", "fingerprint {fp}": "البصمة {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "أفهم أن أي شخص لديه هذا المفتاح يستطيع إنفاق أموالي", "Import wallet": "استيراد محفظة", "Incorrect password.": "كلمة المرور غير صحيحة.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "المدخلات تحمل {held}، لكن المبلغ + الرسوم = {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "المدخلات تحمل {held}، لكن الرسوم + إيداع RAND = {need}", "insufficient balance: have {have}, need {need}": "رصيد غير كافٍ: المتاح {have}، والمطلوب {need}", + "Intended payments": "المدفوعات المقصودة", "is on chain {chain}, not {expected}": "على السلسلة {chain}، لا {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "لا يستطيع رؤية رصيدك أو نشاطك، ولا يستطيع نقل أي شيء.", "It cannot spend.": "لا يمكنه الإنفاق.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "ملف المفتاح لا يحتوي على spend_key", "key file must be version 2": "يجب أن يكون ملف المفتاح بالإصدار 2", "Key hidden — enter your password to view it again.": "المفتاح مخفيّ — أدخل كلمة المرور لعرضه مرة أخرى.", + "Label": "التسمية", "Language": "اللغة", "Leave empty to let the payer choose.": "اتركه فارغًا ليختار الدافع.", "Light": "فاتح", @@ -360,11 +393,15 @@ export default { "machines": "أجهزة", "malformed parameter {parameter}": "معامل مشوّه {parameter}", "Manage contacts": "إدارة جهات الاتصال", + "Manage multisig signers": "إدارة موقّعي التوقيع المتعدد", "Max": "الحد الأقصى", "Memo": "المذكرة", "memo \"{memo}\"": "المذكرة «{memo}»", "memo is {n} bytes, at most {max}": { zero: "المذكرة {n} بايتات، والحد الأقصى {max}", one: "المذكرة {n} بايت، والحد الأقصى {max}", two: "المذكرة {n} بايتان، والحد الأقصى {max}", few: "المذكرة {n} بايتات، والحد الأقصى {max}", many: "المذكرة {n} بايتًا، والحد الأقصى {max}", other: "المذكرة {n} بايت، والحد الأقصى {max}" }, "More is on the way": "المزيد في الطريق", + "Multisig share": "حصة التوقيع المتعدد", + "Multisig signers": "موقّعو التوقيع المتعدد", + "Multisig signing is not available in this app.": "التوقيع المتعدد غير متاح في هذا التطبيق.", "My own prover · {name}": "مُثبِتي الخاص · {name}", "Name": "الاسم", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "أسماء للعناوين التي تدفع لها كثيرًا، محفوظة على هذا الجهاز فقط. أرسل إلى جهة اتصال باسمها، ويُتحقق من بصمتها عند حفظها.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "لا توجد جهات اتصال بعد. احفظ واحدة من الإعدادات، أو الصق عنوانًا.", "no endpoint could answer {method}": "لم تستطع أي نقطة نهاية الإجابة على {method}", "no endpoint could answer {method}; tried {endpoints}": "لم تستطع أي نقطة نهاية الإجابة على {method}، جُرِّبت {endpoints}", + "no expected recipient given: the payee was NOT checked": "لم يُحدَّد مستلم متوقع: لم يُتحقق من المستلم (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "لم تردّ أي عقدة على {url}، لذا لا يمكن التحقق من انتهاء الصلاحية ولن يُوقَّع هذا الاقتراح. أعد المحاولة عندما تصبح العقدة متاحة.", "No proof is pending.": "لا يوجد إثبات معلّق.", "No prover": "لا يوجد مُثبِت", "No prover is paired.": "لا يوجد مُثبِت مقترن.", "No RAND to send yet": "لا يوجد RAND للإرسال بعد", "No registry assets": "لا توجد أصول في السجل", "no reply": "لا رد", + "No signer profiles yet": "لا توجد ملفات موقّعين بعد", + "No such signer profile.": "لا يوجد ملف موقّع كهذا.", "No transaction or block matches that.": "لا توجد معاملة أو كتلة مطابقة.", "no wallet on this device": "لا توجد محفظة على هذا الجهاز", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "لا يذكر أي من روابط مُثبِتات RandProtocol المدمجة المفتاح الذي تثبّته هذه المحفظة له، لذا لن تُستخدم.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "يعمل إثبات واحد على هذا الجهاز — نحو دقيقتين. يمكنك تصفح شاشات أخرى، وإغلاق المحفظة يوقفه.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "المقفل في تلك العملة على السلسلة {chain} هو {locked} فقط، اختر غطاءً آخر أو مبلغًا أصغر", "Open durian.market": "فتح durian.market", + "Open file": "فتح ملف", "Open in explorer": "فتح في المستكشف", "Open in randscan": "فتح في randscan", "Open in side panel": "فتح في اللوحة الجانبية", + "Open in the side panel to choose files": "افتح في اللوحة الجانبية لاختيار الملفات", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "افتح Rand Wallet بـ {label} بدلًا من كتابة كلمة المرور. تظل كلمة المرور تعمل، وتبقى وسيلة استعادة الوصول.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "افتح المحفظة بـ {label} بدلًا من كتابة كلمة المرور. تظل كلمة المرور تعمل، وتبقى وسيلة استعادة الوصول.", "Open this window from a site that asked to connect.": "افتح هذه النافذة من موقع طلب الاتصال.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { zero: "أو استخدم مُثبِتات RandProtocol — {n} أجهزة يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", one: "أو استخدم مُثبِتات RandProtocol — {n} جهاز يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", two: "أو استخدم مُثبِتات RandProtocol — {n} جهازان يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", few: "أو استخدم مُثبِتات RandProtocol — {n} أجهزة يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", many: "أو استخدم مُثبِتات RandProtocol — {n} جهازًا يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", other: "أو استخدم مُثبِتات RandProtocol — {n} جهاز يشغّلها المدقّقون، لكلٍّ منها مفتاحه." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "أو استخدم مُثبِتات RandProtocol — أجهزة يشغّلها المدقّقون، لكلٍّ منها مفتاحه.", "Outcome unknown": "النتيجة غير معروفة", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "المخرج {slot} لا يُفتح على أنه يخص هذا الحساب: رفض توقيع قيمة لا يمكن رؤيتها", "own={value}, expected 1 or absent": "own={value}، والمتوقع 1 أو غيابه", "Paid": "مدفوع", "Pair your own prover": "اقرن مُثبِتك الخاص", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "الصق مفتاح الاستعادة", "path is not {n} levels": { zero: "المسار ليس {n} مستويات", one: "المسار ليس {n} مستوى", two: "المسار ليس {n} مستويين", few: "المسار ليس {n} مستويات", many: "المسار ليس {n} مستوًى", other: "المسار ليس {n} مستوى" }, "Pay me on Rand": "ادفع لي على Rand", + "Payee address": "عنوان المستلم", "Payment link": "رابط الدفع", "Payment link · fingerprint {fingerprint}": "رابط الدفع · البصمة {fingerprint}", "Payment link copied": "نُسخ رابط الدفع", "Payment links cannot be read in this app; paste the address instead.": "لا يمكن قراءة روابط الدفع في هذا التطبيق، الصق العنوان بدلًا منها.", "Payment links cannot be read in this app.": "لا يمكن قراءة روابط الدفع في هذا التطبيق.", + "Payments changed: check them again before signing.": "تغيّرت المدفوعات: تحقق منها مجددًا قبل التوقيع.", "Peers": "النظراء", "Pending": "قيد الانتظار", "Permission to reach that host was not granted, so the node was left as it was.": "لم يُمنح إذن الوصول إلى ذلك المضيف، لذا بقيت العقدة كما كانت.", "Permission to reach that host was not granted.": "لم يُمنح إذن الوصول إلى ذلك المضيف.", "Permission to reach that prover was not granted, so nothing was saved.": "لم يُمنح إذن الوصول إلى ذلك المُثبِت، لذا لم يُحفظ شيء.", "Point the camera at a Rand QR code.": "وجّه الكاميرا نحو رمز QR خاص بـ Rand.", + "Policy file": "ملف السياسة", "Pool fee": "رسوم المجمّع", "Price impact": "تأثير السعر", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "مُسعَّر وفق مجمّعات durian.market على هذه السلسلة (رسوم المجمّع {pct}%). المبادلة معاملة تُثبتها هذه المحفظة وترسلها بنفسها.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "تُعَدّ الإثباتات على هذا الجهاز فقط. وحيث لا يستطيع إعداد إثبات، ينتظر الإرسال حتى تقرن مُثبِتًا أو تعود إلى مُثبِت RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "الإثباتات التي لا يستطيع هذا الجهاز إعدادها تذهب إلى {name}. بصمته {fingerprint} — تحقّق من أن المُثبِت يعرض البصمة نفسها.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "الإثباتات التي لا يستطيع هذا الجهاز إعدادها تذهب إلى أحدها، وكل مُثبِت يُثبت إرسالًا يرى مفتاح العرض لتلك المحفظة.", + "Proposal (proposal.json)": "الاقتراح (proposal.json)", "Prove and send": "أثبِت وأرسل", "Prove for my other devices": "الإثبات لأجهزتي الأخرى", "Prover": "المُثبِت", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "رسوم المُرحِّل (اختيارية)", "Remove": "إزالة", "Remove {name}": "إزالة {name}", + "Remove this signer": "إزالة هذا الموقّع", "Request RAND": "طلب RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "اطلب RAND من الصنبور لتجربة الإرسال والاستلام والإثبات على هذه الشبكة.", "Rescan": "إعادة المسح", @@ -574,8 +623,12 @@ export default { "Running on": "يعمل على", "Save": "حفظ", "Save an address you pay often, and send to it by name.": "احفظ عنوانًا تدفع له كثيرًا، وأرسل إليه باسمه.", + "Save as file": "حفظ كملف", + "Save signer": "حفظ الموقّع", "Save your recovery key": "احفظ مفتاح الاستعادة", "Saved": "حُفظ", + "Saved to {path}": "حُفظ في {path}", + "Saved.": "تم الحفظ.", "Scan": "مسح", "Scan a payment link": "مسح رابط دفع", "Scan a QR code": "مسح رمز QR", @@ -596,6 +649,7 @@ export default { "Sent.": "أُرسل.", "Set on this device when this wallet was created.": "عُيّنت على هذا الجهاز عند إنشاء هذه المحفظة.", "Settings": "الإعدادات", + "Share": "حصة التوقيع", "Share payment link": "مشاركة رابط الدفع", "shielded address decodes to {n} bytes, expected {expected}": { zero: "يُفكّ العنوان المحمي إلى {n} بايتات، والمتوقع {expected}", one: "يُفكّ العنوان المحمي إلى {n} بايت، والمتوقع {expected}", two: "يُفكّ العنوان المحمي إلى {n} بايتان، والمتوقع {expected}", few: "يُفكّ العنوان المحمي إلى {n} بايتات، والمتوقع {expected}", many: "يُفكّ العنوان المحمي إلى {n} بايتًا، والمتوقع {expected}", other: "يُفكّ العنوان المحمي إلى {n} بايت، والمتوقع {expected}" }, "shielded address is not base58": "العنوان المحمي ليس بترميز base58", @@ -607,6 +661,12 @@ export default { "Show the full address": "عرض العنوان الكامل", "Show viewing key": "عرض مفتاح العرض", "Show viewing key for 10 seconds": "عرض مفتاح العرض لمدة 10 ثوانٍ", + "Sign": "توقيع", + "Sign a proposal": "توقيع اقتراح", + "signer {index} is not in this policy (it has {count} signers)": "الموقّع {index} ليس في هذه السياسة (فيها {count} موقّعين)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "حصة توقيع الموقّع {index} صحيحة. لديك {have} من {threshold}: الموقّعون {list}.", + "signer {index}'s signature does not verify over this proposal": "توقيع الموقّع {index} غير صالح لهذا الاقتراح", + "Signer key file, or its 64-hex seed": "ملف مفتاح الموقّع، أو بذرته من 64 حرفًا سداسيًا عشريًا", "So the address above is one you have actually read.": "حتى يكون العنوان أعلاه عنوانًا قرأته فعلًا.", "Something went wrong": "حدث خطأ ما", "Something went wrong.": "حدث خطأ ما.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "تعذّر نسخ ذلك.", "That could not be saved.": "تعذّر حفظ ذلك.", "That does not match — reveal the key again and check.": "غير متطابق — اكشف المفتاح مرة أخرى وتحقّق.", + "That file is larger than a proposal, a policy or a key file can be.": "هذا الملف أكبر مما يمكن أن يكون عليه اقتراح أو سياسة أو ملف مفتاح.", "That is more than your balance of {balance} {symbol}.": "هذا أكثر من رصيدك البالغ {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "هذا ليس 64 حرفًا ست عشريًا (تجزئة) ولا ارتفاع كتلة (أرقام فقط).", "That is not a pairing link.": "هذا ليس رابط إقران.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "تعذّر حساب الرسوم.", "the genesis hash is not hex of a plausible length": "تجزئة التكوين ليست نصًا ست عشريًا بطول معقول", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "يكون التدفق الوارد `none` فقط حين لا تحرق الحزمة أي رمز (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "عنوان ملف المفتاح ليس العنوان الذي تشتقه بذرته", + "the key file's public_key is not the one its seed derives": "قيمة public_key في ملف المفتاح ليست التي تشتقها بذرته", "The key you exported when this wallet was created.": "المفتاح الذي صدّرته عند إنشاء هذه المحفظة.", "The language could not be saved.": "تعذّر حفظ اللغة.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "يطلب الرابط {linked} {symbol}، وأنت كتبت {typed} {symbol}.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "كلمة المرور التي تفتح هذه المحفظة على هذا الجهاز.", "The payer decides": "يقرر الدافع", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "مُسح الإثبات المعلّق (قُفلت المحفظة، أو أُلغي من مكان آخر)، لذا لم يُرسل شيء. أرسل مرة أخرى.", + "the policy does not parse: {reason}": "تعذّر تحليل السياسة: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "تغيّر المجمّع منذ أن قرأته هذه الصفحة. لم يُرسل شيء، ويمكن للموقع تقديم عرض سعر جديد.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "تغيّر المجمّع أثناء الإثبات. لم يُرسل شيء، ويمكن للموقع تقديم عرض سعر جديد.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "لا يملك المجمّع ما يكفي لدفع هذا. لم يُرسل شيء، ويمكن للموقع تقديم عرض سعر جديد.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "لا يجتاز الإثبات التحقق: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "نشر الإثبات خلاصة لم تبنها هذه المحفظة ({published} بدلًا من {expected})، لذا لن يُستخدم", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "يعمل الإثبات على هذا الجهاز. يمكنك تصفح شاشات أخرى — ويستمر — لكن إغلاق المحفظة يوقفه.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "أعاد التحقق من الاقتراح ردًّا ناقصًا، لذا لن يُوقَّع هذا الاقتراح.", + "the proposal does not parse: {reason}": "تعذّر تحليل الاقتراح: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "ينتهي الاقتراح عند {expires}، خارج نافذة حزمته ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "قيمة auth_commit في الاقتراح ليست حساب هذه السياسة", + "the proposal's expected digest is not its bundle's": "الملخّص (digest) المتوقع للاقتراح ليس ملخّص حزمته", + "the proposal's signing_message is not its binding's": "قيمة signing_message في الاقتراح ليست قيمة ارتباطه", "the prover": "المُثبِت", "the prover accepted the job but named no job id": "قبِل المُثبِت المهمة لكنه لم يذكر معرّفًا لها", "The prover answered with an unknown state ({state}).": "أجاب المُثبِت بحالة غير معروفة ({state}).", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "لم تتم الموافقة على الطلب في Rand Wallet.", "The rescan could not be started.": "تعذّر بدء إعادة المسح.", "The rescan did not finish": "لم تكتمل إعادة المسح", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "تعذّر فتح ملفات الموقّعين المحفوظة؛ لم يتغيّر شيء. استعد تخزين هذا الجهاز أو امسح المحفظة واستعدها من مفتاحها", "The scan was cancelled.": "أُلغي المسح.", "The side panel could not be opened.": "تعذّر فتح اللوحة الجانبية.", "The site sent an invoke this wallet cannot read: {reason}.": "أرسل الموقع استدعاءً لا تستطيع هذه المحفظة قراءته: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "لم تتم المبادلة.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "تستمر المبادلة. إغلاق المحفظة قبل ظهور «أُرسل» يوقفها، ولا يضيع شيء.", "The theme could not be saved.": "تعذّر حفظ السمة.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "تحمل المعاملة إجراءً؛ والاقتراح الخاص متعدد التوقيع هو دفعة من حزمته وحدها", + "the transaction carries no bundle": "لا تحمل المعاملة أي حزمة", + "the transaction does not hash to the proposal's binding": "تجزئة المعاملة ليست ارتباط الاقتراح", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "المعاملة للسلسلة {tx}، بينما يذكر الاقتراح السلسلة {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "يكشف مفتاح المعاملة هذه الدفعة وحدها — مبلغها ومستلمها — لمن تعطيه إياه. لا يمكنه إنفاق أي شيء.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "ربما تكون المعاملة على السلسلة بالفعل. راجع النشاط قبل السحب مرة أخرى — السحب مرتين يعني الحرق مرتين.", "The transfer could not be proved.": "تعذّر إثبات التحويل.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "أُبلغت المحفظة: {message}", "The withdrawal could not be proved.": "تعذّر إثبات السحب.", "The withdrawal was submitted, but the node did not return a usable hash.": "أُرسل السحب، لكن العقدة لم تُعِد تجزئة صالحة للاستخدام.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "لا يمكن التحقق من قيمة الشاهد (witness): قيمة nk أو الملح فيه ليست لهذا الاقتراح", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "قيمة الشاهد (witness) لا تطابق المخرجات: للأصل {asset} مدخل {held} ومخرج {spent} مع الرسوم والحرق", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "قيمة الشاهد (witness) لا تطابق المعاملة: المدخل {input} لا يُبطَل إلى nullifier الحزمة", "Theme": "السمة", "There is no pool for that pair.": "لا يوجد مجمّع لهذا الزوج.", "There is no program {program}… on this chain. Nothing was sent.": "لا يوجد البرنامج {program}… على هذه السلسلة. لم يُرسل شيء.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "هذا الجهاز، وحيث لا يستطيع · مُثبِتات {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "يُنسي هذا السندات والسجل المقروءة من السلسلة القديمة ويقرأ هذه العقدة من البداية. لن تُمسّ مفاتيحك ولا كلمة مرورك.", "This key can spend your funds": "هذا المفتاح يستطيع إنفاق أموالك", + "this key is not one of the policy's signers": "هذا المفتاح ليس من موقّعي السياسة", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "هذا المفتاح هو محفظتك. أي شخص يملكه يستطيع إنفاق أموالك، وبدونه يعني فقدان الجهاز فقدان الأموال. يمكنك عرضه مرة أخرى في الإعدادات بكلمة المرور.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "هذا يغادر المجمّع المحمي. يصبح عنوان الوجهة والمبلغ علنيين على السلسلة الأخرى.", "This link is too long for a QR code — copy it instead.": "هذا الرابط أطول من أن يُعرض في رمز QR — انسخه بدلًا من ذلك.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "لم يُعثر على هذا السند.", "This passkey does not support the PRF extension.": "مفتاح المرور هذا لا يدعم امتداد PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "أُرسل هذا الإثبات بالفعل من نافذة أخرى. راجع النشاط قبل الإرسال مرة أخرى.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "ينتهي هذا الاقتراح عند الارتفاع {expires} والسلسلة عند {head}: فات أوان التوقيع. اطلب اقتراحًا جديدًا.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "هذا الاقتراح مرتبط بنشأة (genesis) مختلفة عن التي تخدمها عقدة هذه المحفظة.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "هذا الاقتراح لسياسة أخرى: يذكر {theirs}، وهذه السياسة هي {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "هذا الاقتراح للسلسلة {theirs}؛ وهذه المحفظة على السلسلة {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "يتقاضى هذا المُثبِت {fee} RAND لكل إثبات، وهذا الإصدار من المحفظة لا يدفع رسوم المُثبِت: اقرن مُثبِتًا لا يتقاضى شيئًا، أو أثبِت على هذا الجهاز", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "يتقاضى هذا المُثبِت رسومًا لا يدفعها هذا الإصدار من المحفظة. اقرن مُثبِتًا لا يتقاضى شيئًا، أو أرسل من تطبيق سطح المكتب.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "لا يقبل هذا المُثبِت هذا النوع من المهام ({reason}). اقرن مُثبِتًا آخر في الإعدادات، أو أرسل من تطبيق سطح المكتب.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "هذا يزيل المحفظة من هذا الجهاز. ستحتاج إلى مفتاح الاستعادة لاستعادتها.", "This screen could not be shown.": "تعذّر عرض هذه الشاشة.", "This screen ships in a later task. Your wallet is unlocked and ready.": "ستتوفر هذه الشاشة في مهمة لاحقة. محفظتك مفتوحة وجاهزة.", + "this signer is already saved as {label}": "هذا الموقّع محفوظ مسبقًا باسم {label}", + "This signer profile has no key on this device: remove it and add it again.": "ملف الموقّع هذا لا مفتاح له على هذا الجهاز: أزله وأضفه من جديد.", "This tab is showing what it has so far, and will refresh when that finishes.": "يعرض هذا التبويب ما لديه حتى الآن، وسيتحدّث عند انتهاء ذلك.", "This takes a few minutes; keep this window open.": "يستغرق هذا بضع دقائق، أبقِ هذه النافذة مفتوحة.", "This transaction was not found.": "لم يُعثر على هذه المعاملة.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "اكتب {word} للتأكيد", "Type it once more.": "اكتبها مرة أخرى.", "Type the last four characters of that address": "اكتب آخر أربعة أحرف من ذلك العنوان", + "Type the last four characters of the account fingerprint": "اكتب آخر أربعة أحرف من بصمة الحساب", "unavailable in this app": "غير متاح في هذا التطبيق", "unknown": "غير معروف", "unknown parameter {name}": "معامل غير معروف {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "ضعيفة", "Welcome back": "مرحبًا بعودتك", "What the pool pays you arrives as a note your wallet finds on its next sync.": "ما يدفعه لك المجمّع يصل في صورة سند تجده محفظتك عند مزامنتها التالية.", + "What this proposal does, as checked against your policy": "ما يفعله هذا الاقتراح، مُتحقَّقًا منه مقابل سياستك", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "حيث لا يستطيع هذا الجهاز إعداد إثبات، اقرن مُثبِتًا. يبقى مفتاح الإنفاق هنا في كل الأحوال، والمُثبِت الذي تشغّله بنفسك — تطبيق سطح المكتب، أو rand-prover على جهازك — يُبقي سجلك لك وحدك أيضًا.", "Where to?": "إلى أين؟", "Which address?": "أي عنوان؟", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { zero: "لم ينتهِ مُثبِتك بعد {n} دقائق من الإثبات. استأنف لاحقًا، أو ألغِ.", one: "لم ينتهِ مُثبِتك بعد {n} دقيقة من الإثبات. استأنف لاحقًا، أو ألغِ.", two: "لم ينتهِ مُثبِتك بعد {n} دقيقتين من الإثبات. استأنف لاحقًا، أو ألغِ.", few: "لم ينتهِ مُثبِتك بعد {n} دقائق من الإثبات. استأنف لاحقًا، أو ألغِ.", many: "لم ينتهِ مُثبِتك بعد {n} دقيقة من الإثبات. استأنف لاحقًا، أو ألغِ.", other: "لم ينتهِ مُثبِتك بعد {n} دقيقة من الإثبات. استأنف لاحقًا، أو ألغِ." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { zero: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقائق في طابوره. استأنف لاحقًا، أو ألغِ.", one: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقيقة في طابوره. استأنف لاحقًا، أو ألغِ.", two: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقيقتين في طابوره. استأنف لاحقًا، أو ألغِ.", few: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقائق في طابوره. استأنف لاحقًا، أو ألغِ.", many: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقيقة في طابوره. استأنف لاحقًا، أو ألغِ.", other: "لم يبدأ مُثبِتك هذا الإثبات بعد {n} دقيقة في طابوره. استأنف لاحقًا، أو ألغِ." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "تعذّر فتح إقران مُثبِتك. اقفل المحفظة وافتحها، أو اقرن المُثبِت مرة أخرى في الإعدادات.", + "Your share — send it to whoever proves the proposal": "حصة توقيعك — أرسلها إلى من يُثبت الاقتراح", "Your wallet picks up what it pays you on its next sync.": "تلتقط محفظتك ما يُدفع لك عند مزامنتها التالية.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "قُرئ سجل محفظتك من {chain} — لم يتغيّر شيء، ولن ترسل هذه المحفظة أموالًا أو تطلبها حتى تتوافقا. بدّل العقدة في الإعدادات، أو أعد مسح هذه المحفظة للسلسلة الجديدة.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "هذا هو عنوان مُثبِتات {name}. لا تحتاج إلى اقتران: تستخدمها هذه المحفظة افتراضيًا.", diff --git a/ui/locales/de.js b/ui/locales/de.js index 48b0583..ffbd900 100644 --- a/ui/locales/de.js +++ b/ui/locales/de.js @@ -8,6 +8,7 @@ // (Kopplung), transfer/send — Überweisung/senden, rescan — neu scannen, burn — verbrennen (Burn). export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Noch {blocks} Blöcke, um es zu beweisen.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} Peer", other: "{count} Peers" }, "{count} peers": { one: "{count} Peer", other: "{count} Peers" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} ist aus", "{label} is on": "{label} ist an", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} öffnet diese Wallet nicht mehr (ihr Passwort wurde geändert). Entsperre sie mit dem Passwort und schalte {label} dann in den Einstellungen wieder ein.", + "{matched} of {total} payments matched.": "{matched} von {total} Zahlungen stimmen überein.", "{member} answered with another key than the one this wallet pins": "{member} hat mit einem anderen Schlüssel geantwortet als dem, den diese Wallet hinterlegt hat", "{member} did not answer": "{member} hat nicht geantwortet", "{member} does not take this wallet's jobs": "{member} nimmt keine Aufträge dieser Wallet an", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} kann nicht abgehoben werden", "{symbol} has {decimals} decimal places — that is more.": "{symbol} hat {decimals} Nachkommastellen – das sind mehr.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} wird als die Coin ausgezahlt, die du wählst, auf deren eigener Chain.", + "{threshold} of {count} · signer {index}": "{threshold} von {count} · Unterzeichner {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} von {count} · dieser Schlüssel ist Unterzeichner {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} auf Chain {chain} hat {decimals} Nachkommastellen: Betrag und Relayer-Gebühr müssen Vielfache von {unit} sein", "{units} units of asset {asset}": "{units} Einheiten von Asset {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} hat zuerst geantwortet – Chain {chain} · Höhe {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "Eine Prover-Adresse muss mit https:// beginnen.", "A prover is making the proof": "Ein Prover erstellt den Beweis", "A prover sees your history": "Ein Prover sieht deinen Verlauf", + "a share is index:hex": "ein Signaturanteil ist index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Eine abgeschirmte Wallet für das Rand-Netzwerk. Guthaben und Verlauf bleiben privat – jede Überweisung wird auf diesem Gerät bewiesen.", + "a signer key is a rand-node keygen file or 64 hex characters": "ein Unterzeichner-Schlüssel ist eine rand-node-Keygen-Datei oder 64 Hex-Zeichen", + "a signer label is 1-64 characters": "eine Unterzeichner-Bezeichnung hat 1-64 Zeichen", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Ein Unterzeichnerprofil wird im verschlüsselten Tresor dieser Wallet aufbewahrt. Jede Signatur fragt erneut nach deinem Passwort.", + "a signer seed is 64 hex characters": "ein Unterzeichner-Seed besteht aus 64 Hex-Zeichen", "A site": "Eine Website", "a spend key is 64 hex characters, or a wallet.key.json": "ein Ausgabeschlüssel ist 64 Hex-Zeichen lang oder eine wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Zuerst wird ein Transaktions-Hash gesucht; wird nichts gefunden, wird derselbe Hash als Block gesucht. Nur Ziffern suchen einen Block nach Höhe.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Eine Abhebung auf eine nicht aufgeführte Chain kann abgelehnt werden. Nichts wird ausgegeben, bevor der Beweis erstellt ist.", "About": "Über", "about {left} left": "noch etwa {left}", + "Account fingerprint — compare it with every other signer before you save": "Konto-Fingerabdruck — vergleiche ihn mit jedem anderen Unterzeichner, bevor du speicherst", "Activity": "Aktivität", "Add a contact": "Kontakt hinzufügen", + "Add a signer": "Unterzeichner hinzufügen", + "Add payment": "Zahlung hinzufügen", + "Add the policy file and your signer key to sign for a private multisig account.": "Füge die Richtliniendatei und deinen Unterzeichner-Schlüssel hinzu, um für ein privates Multisig-Konto zu signieren.", "Address": "Adresse", "Address copied": "Adresse kopiert", "Address or payment link": "Adresse oder Zahlungslink", @@ -115,14 +128,18 @@ export default { "almost done": "fast fertig", "also tried {endpoints}": "auch versucht: {endpoints}", "Amount": "Betrag", + "Amount in RAND": "Betrag in RAND", "amount must be greater than zero": "der Betrag muss größer als null sein", "amount overflow": "Betragsüberlauf", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Beträge können bei Zahlungen, die nicht in RAND erfolgen, nicht geprüft werden: Lass den Betrag dort leer.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Dieser Empfänger erhält sowohl RAND als auch einen Token: RAND- und Token-Zahlungen an einen Empfänger brauchen getrennte Vorschläge.", "amounts overflow": "Überlauf der Beträge", "an action is {n} characters, more than {max}": { one: "eine Aktion hat {n} Zeichen, mehr als {max}", other: "eine Aktion hat {n} Zeichen, mehr als {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Eine Adresse (rand1…), ein Zahlungslink (randpay:…) oder der Name eines gespeicherten Kontakts.", "An address here is hexadecimal — 0-9 and a-f only.": "Eine Adresse ist hier hexadezimal – nur 0–9 und a–f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Eine Adresse auf dieser Chain hat 20 Bytes – 40 Hex-Zeichen. Das sind {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Eine Adresse auf dieser Chain hat 32 Bytes – 64 Hex-Zeichen. Das sind {length}.", + "An amount is a number of RAND, such as 1.5.": "Ein Betrag ist eine Anzahl RAND, zum Beispiel 1.5.", "an unknown chain": "eine unbekannte Chain", "another tab changed this wallet while it was syncing; try again": "ein anderer Tab hat diese Wallet während der Synchronisierung geändert; versuche es erneut", "Another tab is syncing": "Ein anderer Tab synchronisiert", @@ -189,11 +206,16 @@ export default { "Chains known": "Bekannte Chains", "Change back to you": "Wechselgeld an dich", "Change it": "Ändern", + "Check": "Prüfen", + "Check a share": "Einen Signaturanteil prüfen", "Check Activity": "Aktivität prüfen", "Check Activity first": "Zuerst Aktivität prüfen", "Check before saving": "Vor dem Speichern prüfen", "Check fingerprint": "Fingerabdruck prüfen", + "Check payments": "Zahlungen prüfen", + "Check proposal": "Vorschlag prüfen", "Check settings": "Einstellungen prüfen", + "Check share": "Signaturanteil prüfen", "Checking it against the chain before anything is proved.": "Wird mit der Chain abgeglichen, bevor etwas bewiesen wird.", "Checking the pool and your notes…": "Prüfe den Pool und deine Noten …", "Checking the swap…": "Prüfe den Tausch …", @@ -220,6 +242,7 @@ export default { "Continue": "Weiter", "Copied": "Kopiert", "Copied as JSON": "Als JSON kopiert", + "Copy": "Kopieren", "Copy address": "Adresse kopieren", "Copy as JSON": "Als JSON kopieren", "Copy link": "Link kopieren", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "Gib einen Betrag zum Senden ein.", "Enter an amount.": "Gib einen Betrag ein.", "Enter at least 10 characters": "Mindestens 10 Zeichen eingeben", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Gib jede Zahlung ein, die du leisten willst: die Adresse und den Betrag in RAND. Eine Zahlung, die zweimal an dieselbe Adresse geht, wird einmal mit der Summe eingetragen. Eine Ausgabe, die zu keiner davon passt, kann nicht signiert werden.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Gib die Adresse jedes Zahlungsempfängers ein: Eine Ausgabe, die nicht zur eingegebenen Adresse passt oder deren Adresse leer ist, kann nicht signiert werden.", + "Enter the address of every payment.": "Gib die Adresse jeder Zahlung ein.", "Enter the address this should be released to.": "Gib die Adresse ein, an die ausgezahlt werden soll.", "Enter the address you are sending to.": "Gib die Adresse ein, an die du sendest.", "Enter the amount as a number, for example 1.25.": "Gib den Betrag als Zahl ein, zum Beispiel 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Gib den Betrag jeder Zahlung ein: Er verhindert eine Aufteilung oder eine überhöhte Zahlung.", "Enter the first 4 characters of your recovery key.": "Gib die ersten 4 Zeichen deines Wiederherstellungsschlüssels ein.", "Enter the relayer fee as a number, for example 1.25.": "Gib die Relayer-Gebühr als Zahl ein, zum Beispiel 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Gib das Passwort dieser Wallet ein – die Kopplung ist damit versiegelt.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "Gib deinen Wiederherstellungsschlüssel ein.", "Epoch": "Epoche", "Exactly those four letters, in capitals.": "Genau diese vier Buchstaben, in Großbuchstaben.", + "expected recipient {address}: {reason}": "erwarteter Empfänger {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients muss eine Liste von Adressen sein", "Explore": "Erkunden", "Export spend key": "Ausgabeschlüssel exportieren", "Exporting the spend key is behind your password.": "Der Export des Ausgabeschlüssels ist durch dein Passwort geschützt.", @@ -288,6 +317,7 @@ export default { "Fee": "Gebühr", "fee must be at least {fee} RAND (the bridge burn floor)": "die Gebühr muss mindestens {fee} RAND betragen (Burn-Untergrenze der Bridge)", "fee must be at least {fee} RAND (the bundle floor)": "die Gebühr muss mindestens {fee} RAND betragen (Bundle-Untergrenze)", + "Fee: {fee} RAND · expires at height {height}": "Gebühr: {fee} RAND · läuft bei Höhe {height} ab", "Filter activity by asset": "Aktivität nach Asset filtern", "Fingerprint": "Fingerabdruck", "fingerprint {fp}": "Fingerabdruck {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "Ich verstehe, dass jeder mit diesem Schlüssel mein Guthaben ausgeben kann", "Import wallet": "Wallet importieren", "Incorrect password.": "Falsches Passwort.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "die Eingaben halten {held}, aber Betrag + Gebühr sind {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "die Eingaben halten {held}, aber Gebühr + RAND-Einzahlung sind {need}", "insufficient balance: have {have}, need {need}": "unzureichendes Guthaben: vorhanden {have}, benötigt {need}", + "Intended payments": "Vorgesehene Zahlungen", "is on chain {chain}, not {expected}": "ist auf Chain {chain}, nicht {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Sie kann weder dein Guthaben noch deine Aktivität sehen und nichts bewegen.", "It cannot spend.": "Er kann nichts ausgeben.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "die Schlüsseldatei hat keinen spend_key", "key file must be version 2": "die Schlüsseldatei muss Version 2 sein", "Key hidden — enter your password to view it again.": "Schlüssel ausgeblendet – gib dein Passwort ein, um ihn wieder anzuzeigen.", + "Label": "Bezeichnung", "Language": "Sprache", "Leave empty to let the payer choose.": "Leer lassen, damit der Zahler wählt.", "Light": "Hell", @@ -360,11 +393,15 @@ export default { "machines": "Rechner", "malformed parameter {parameter}": "fehlerhafter Parameter {parameter}", "Manage contacts": "Kontakte verwalten", + "Manage multisig signers": "Multisig-Unterzeichner verwalten", "Max": "Max.", "Memo": "Memo", "memo \"{memo}\"": "Memo „{memo}“", "memo is {n} bytes, at most {max}": { one: "das Memo hat {n} Byte, höchstens {max}", other: "das Memo hat {n} Bytes, höchstens {max}" }, "More is on the way": "Es kommt noch mehr", + "Multisig share": "Multisig-Signaturanteil", + "Multisig signers": "Multisig-Unterzeichner", + "Multisig signing is not available in this app.": "Multisig-Signieren ist in dieser App nicht verfügbar.", "My own prover · {name}": "Mein eigener Prover · {name}", "Name": "Name", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Namen für Adressen, an die du oft zahlst, nur auf diesem Gerät gespeichert. Sende per Name an einen Kontakt; sein Fingerabdruck wird beim Speichern geprüft.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Noch keine Kontakte. Speichere einen in den Einstellungen oder füge eine Adresse ein.", "no endpoint could answer {method}": "kein Endpunkt konnte {method} beantworten", "no endpoint could answer {method}; tried {endpoints}": "kein Endpunkt konnte {method} beantworten; versucht: {endpoints}", + "no expected recipient given: the payee was NOT checked": "kein erwarteter Empfänger angegeben: der Zahlungsempfänger wurde NICHT geprüft", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Unter {url} hat kein Knoten geantwortet, daher lässt sich der Ablauf nicht prüfen und dieser Vorschlag wird nicht signiert. Versuche es erneut, wenn der Knoten erreichbar ist.", "No proof is pending.": "Kein Beweis ausstehend.", "No prover": "Kein Prover", "No prover is paired.": "Kein Prover gekoppelt.", "No RAND to send yet": "Noch kein RAND zum Senden", "No registry assets": "Keine Registry-Assets", "no reply": "keine Antwort", + "No signer profiles yet": "Noch keine Unterzeichnerprofile", + "No such signer profile.": "Dieses Unterzeichnerprofil gibt es nicht.", "No transaction or block matches that.": "Keine Transaktion und kein Block passt dazu.", "no wallet on this device": "keine Wallet auf diesem Gerät", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Keiner der integrierten RandProtocol-Prover-Links nennt den Schlüssel, den diese Wallet dafür hinterlegt hat; sie werden nicht verwendet.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Ein Beweis läuft auf diesem Gerät – etwa zwei Minuten. Du kannst andere Bildschirme ansehen; das Schließen der Wallet stoppt ihn.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "in dieser Coin auf Chain {chain} sind nur {locked} hinterlegt; wähle eine andere Deckung oder einen kleineren Betrag", "Open durian.market": "durian.market öffnen", + "Open file": "Datei öffnen", "Open in explorer": "Im Explorer öffnen", "Open in randscan": "In randscan öffnen", "Open in side panel": "In Seitenleiste öffnen", + "Open in the side panel to choose files": "Im Seitenbereich öffnen, um Dateien auszuwählen", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Öffne Rand Wallet mit {label}, statt dein Passwort einzugeben. Dein Passwort funktioniert weiterhin und bleibt der Weg, den Zugang wiederherzustellen.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Öffne die Wallet mit {label}, statt dein Passwort einzugeben. Dein Passwort funktioniert weiterhin und bleibt der Weg, den Zugang wiederherzustellen.", "Open this window from a site that asked to connect.": "Öffne dieses Fenster von einer Website aus, die eine Verbindung angefragt hat.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Oder nutze die RandProtocol-Prover – {n} Rechner, betrieben von den Validatoren, mit eigenem Schlüssel.", other: "Oder nutze die RandProtocol-Prover – {n} Rechner, betrieben von den Validatoren, jeder mit eigenem Schlüssel." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Oder nutze die RandProtocol-Prover – Rechner, betrieben von den Validatoren, jeder mit eigenem Schlüssel.", "Outcome unknown": "Ergebnis unbekannt", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "Ausgabe {slot} lässt sich nicht als die dieses Kontos öffnen: Signatur eines Werts, den sie nicht sehen kann, wird verweigert", "own={value}, expected 1 or absent": "own={value}, erwartet 1 oder nicht vorhanden", "Paid": "Bezahlt", "Pair your own prover": "Eigenen Prover koppeln", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "Wiederherstellungsschlüssel einfügen", "path is not {n} levels": { one: "der Pfad hat nicht {n} Ebene", other: "der Pfad hat nicht {n} Ebenen" }, "Pay me on Rand": "Bezahl mich auf Rand", + "Payee address": "Adresse des Zahlungsempfängers", "Payment link": "Zahlungslink", "Payment link · fingerprint {fingerprint}": "Zahlungslink · Fingerabdruck {fingerprint}", "Payment link copied": "Zahlungslink kopiert", "Payment links cannot be read in this app; paste the address instead.": "Zahlungslinks können in dieser App nicht gelesen werden; füge stattdessen die Adresse ein.", "Payment links cannot be read in this app.": "Zahlungslinks können in dieser App nicht gelesen werden.", + "Payments changed: check them again before signing.": "Zahlungen geändert: Prüfe sie vor dem Signieren erneut.", "Peers": "Peers", "Pending": "Ausstehend", "Permission to reach that host was not granted, so the node was left as it was.": "Die Berechtigung, diesen Host zu erreichen, wurde nicht erteilt, daher bleibt der Knoten unverändert.", "Permission to reach that host was not granted.": "Die Berechtigung, diesen Host zu erreichen, wurde nicht erteilt.", "Permission to reach that prover was not granted, so nothing was saved.": "Die Berechtigung, diesen Prover zu erreichen, wurde nicht erteilt, daher wurde nichts gespeichert.", "Point the camera at a Rand QR code.": "Richte die Kamera auf einen Rand-QR-Code.", + "Policy file": "Richtliniendatei", "Pool fee": "Pool-Gebühr", "Price impact": "Preiseinfluss", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Bepreist von den durian.market-Pools auf dieser Chain ({pct} % Pool-Gebühr). Der Tausch ist eine Transaktion, die diese Wallet selbst beweist und sendet.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Beweise werden nur auf diesem Gerät erstellt. Wo es keinen erstellen kann, wartet das Senden, bis du einen Prover koppelst oder wieder den RandProtocol-Prover nutzt.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Beweise, die dieses Gerät nicht erstellen kann, gehen an {name}. Sein Fingerabdruck ist {fingerprint} – prüfe, dass der Prover denselben anzeigt.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Beweise, die dieses Gerät nicht erstellen kann, gehen an einen von ihnen; jeder, der eine Überweisung beweist, sieht den Ansichtsschlüssel dieser Wallet.", + "Proposal (proposal.json)": "Vorschlag (proposal.json)", "Prove and send": "Beweisen und senden", "Prove for my other devices": "Für meine anderen Geräte beweisen", "Prover": "Prover", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "Relayer-Gebühr (optional)", "Remove": "Entfernen", "Remove {name}": "{name} entfernen", + "Remove this signer": "Diesen Unterzeichner entfernen", "Request RAND": "RAND anfordern", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Fordere RAND vom Faucet an, um Senden, Empfangen und Beweisen in diesem Netzwerk auszuprobieren.", "Rescan": "Neu scannen", @@ -574,8 +623,12 @@ export default { "Running on": "Läuft auf", "Save": "Speichern", "Save an address you pay often, and send to it by name.": "Speichere eine Adresse, an die du oft zahlst, und sende per Name an sie.", + "Save as file": "Als Datei speichern", + "Save signer": "Unterzeichner speichern", "Save your recovery key": "Sichere deinen Wiederherstellungsschlüssel", "Saved": "Gespeichert", + "Saved to {path}": "Gespeichert unter {path}", + "Saved.": "Gespeichert.", "Scan": "Scannen", "Scan a payment link": "Zahlungslink scannen", "Scan a QR code": "QR-Code scannen", @@ -596,6 +649,7 @@ export default { "Sent.": "Gesendet.", "Set on this device when this wallet was created.": "Auf diesem Gerät festgelegt, als diese Wallet erstellt wurde.", "Settings": "Einstellungen", + "Share": "Signaturanteil", "Share payment link": "Zahlungslink teilen", "shielded address decodes to {n} bytes, expected {expected}": { one: "abgeschirmte Adresse ergibt {n} Byte, erwartet {expected}", other: "abgeschirmte Adresse ergibt {n} Bytes, erwartet {expected}" }, "shielded address is not base58": "abgeschirmte Adresse ist kein Base58", @@ -607,6 +661,12 @@ export default { "Show the full address": "Vollständige Adresse anzeigen", "Show viewing key": "Ansichtsschlüssel anzeigen", "Show viewing key for 10 seconds": "Ansichtsschlüssel 10 Sekunden anzeigen", + "Sign": "Signieren", + "Sign a proposal": "Einen Vorschlag signieren", + "signer {index} is not in this policy (it has {count} signers)": "Unterzeichner {index} gehört nicht zu dieser Richtlinie (sie hat {count} Unterzeichner)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Der Signaturanteil von Unterzeichner {index} ist gültig. {have} von {threshold} liegen vor: Unterzeichner {list}.", + "signer {index}'s signature does not verify over this proposal": "die Signatur von Unterzeichner {index} ist für diesen Vorschlag nicht gültig", + "Signer key file, or its 64-hex seed": "Unterzeichner-Schlüsseldatei oder ihr Seed aus 64 Hex-Zeichen", "So the address above is one you have actually read.": "Damit die Adresse oben eine ist, die du wirklich gelesen hast.", "Something went wrong": "Etwas ist schiefgelaufen", "Something went wrong.": "Etwas ist schiefgelaufen.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "Das konnte nicht kopiert werden.", "That could not be saved.": "Das konnte nicht gespeichert werden.", "That does not match — reveal the key again and check.": "Das stimmt nicht überein – zeige den Schlüssel erneut an und prüfe.", + "That file is larger than a proposal, a policy or a key file can be.": "Diese Datei ist größer, als ein Vorschlag, eine Richtlinie oder eine Schlüsseldatei sein kann.", "That is more than your balance of {balance} {symbol}.": "Das ist mehr als dein Guthaben von {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Das sind weder 64 Hex-Zeichen (ein Hash) noch eine Blockhöhe (nur Ziffern).", "That is not a pairing link.": "Das ist kein Kopplungslink.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "Die Gebühr konnte nicht ermittelt werden.", "the genesis hash is not hex of a plausible length": "der Genesis-Hash ist kein Hex-Wert plausibler Länge", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "der Zufluss ist genau dann `none`, wenn das Bundle keinen Token verbrennt (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "die Adresse der Schlüsseldatei ist nicht die, die ihr Seed ableitet", + "the key file's public_key is not the one its seed derives": "der public_key der Schlüsseldatei ist nicht der, den ihr Seed ableitet", "The key you exported when this wallet was created.": "Der Schlüssel, den du beim Erstellen dieser Wallet exportiert hast.", "The language could not be saved.": "Die Sprache konnte nicht gespeichert werden.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Der Link fordert {linked} {symbol}; du hast {typed} {symbol} eingegeben.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "Das Passwort, das diese Wallet auf diesem Gerät entsperrt.", "The payer decides": "Der Zahler entscheidet", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "Der ausstehende Beweis wurde verworfen (die Wallet wurde gesperrt oder er wurde anderswo abgebrochen), daher wurde nichts gesendet. Sende erneut.", + "the policy does not parse: {reason}": "die Richtlinie lässt sich nicht parsen: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Der Pool hat sich geändert, seit diese Seite ihn gelesen hat. Es wurde nichts gesendet; die Website kann ein neues Angebot machen.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Der Pool hat sich geändert, während dies bewiesen wurde. Es wurde nichts gesendet; die Website kann ein neues Angebot machen.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Der Pool hält nicht genug, um das auszuzahlen. Es wurde nichts gesendet; die Website kann ein neues Angebot machen.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "der Beweis ist nicht gültig: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "der Beweis hat einen Digest veröffentlicht, den diese Wallet nicht erstellt hat ({published} statt {expected}); er wird nicht verwendet", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "Der Beweis läuft auf diesem Gerät. Du kannst andere Bildschirme ansehen – er läuft weiter –, aber das Schließen der Wallet stoppt ihn.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Die Vorschlagsprüfung hat eine unvollständige Antwort geliefert, daher wird dieser Vorschlag nicht signiert.", + "the proposal does not parse: {reason}": "der Vorschlag lässt sich nicht parsen: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "der Vorschlag läuft bei {expires} ab, außerhalb des Fensters seines Bundles ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "das auth_commit des Vorschlags ist nicht das Konto dieser Richtlinie", + "the proposal's expected digest is not its bundle's": "der erwartete Digest des Vorschlags ist nicht der seines Bundles", + "the proposal's signing_message is not its binding's": "die signing_message des Vorschlags ist nicht die seiner Bindung", "the prover": "der Prover", "the prover accepted the job but named no job id": "der Prover hat den Auftrag angenommen, aber keine Auftrags-ID genannt", "The prover answered with an unknown state ({state}).": "Der Prover hat mit einem unbekannten Status geantwortet ({state}).", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "Die Anfrage wurde in Rand Wallet nicht genehmigt.", "The rescan could not be started.": "Der Neuscan konnte nicht gestartet werden.", "The rescan did not finish": "Der Neuscan wurde nicht abgeschlossen", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "die gespeicherten Unterzeichnerprofile konnten nicht geöffnet werden; es wurde nichts geändert. Stelle den Speicher dieses Geräts wieder her oder lösche die Wallet und stelle sie aus ihrem Schlüssel wieder her", "The scan was cancelled.": "Der Scan wurde abgebrochen.", "The side panel could not be opened.": "Die Seitenleiste konnte nicht geöffnet werden.", "The site sent an invoke this wallet cannot read: {reason}.": "Die Website hat einen Aufruf gesendet, den diese Wallet nicht lesen kann: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "Der Tausch ist nicht durchgegangen.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Der Tausch läuft weiter. Wird die Wallet geschlossen, bevor „gesendet“ angezeigt wird, stoppt er, und nichts geht verloren.", "The theme could not be saved.": "Das Design konnte nicht gespeichert werden.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "die Transaktion enthält eine Aktion; ein privater Multisig-Vorschlag ist allein eine Zahlung aus seinem Bundle", + "the transaction carries no bundle": "die Transaktion enthält kein Bundle", + "the transaction does not hash to the proposal's binding": "der Hash der Transaktion ist nicht die Bindung des Vorschlags", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "die Transaktion gilt für Chain {tx}, der Vorschlag nennt Chain {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "Der Transaktionsschlüssel legt diese eine Zahlung offen – ihren Betrag und ihren Empfänger – für jeden, dem du ihn gibst. Er kann nichts ausgeben.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Die Transaktion ist möglicherweise schon auf der Chain. Prüfe die Aktivität, bevor du erneut abhebst – zweimal abheben heißt zweimal verbrennen.", "The transfer could not be proved.": "Die Überweisung konnte nicht bewiesen werden.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "Die Wallet hat folgende Meldung erhalten: {message}", "The withdrawal could not be proved.": "Die Abhebung konnte nicht bewiesen werden.", "The withdrawal was submitted, but the node did not return a usable hash.": "Die Abhebung wurde übermittelt, aber der Knoten hat keinen verwendbaren Hash zurückgegeben.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "der Wert des Witness lässt sich nicht prüfen: sein nk oder Salt gehört nicht zu diesem Vorschlag", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "der Wert des Witness passt nicht zu den Ausgaben: Asset {asset} hat {held} Eingang und {spent} Ausgang mit Gebühr und Burns", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "der Wert des Witness passt nicht zur Transaktion: Eingang {input} ergibt nicht den Nullifier des Bundles", "Theme": "Design", "There is no pool for that pair.": "Für dieses Paar gibt es keinen Pool.", "There is no program {program}… on this chain. Nothing was sent.": "Auf dieser Chain gibt es kein Programm {program}… Es wurde nichts gesendet.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "Dieses Gerät, oder wo es nicht kann · {name}-Prover", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Dadurch werden die von der alten Chain gelesenen Noten und der Verlauf vergessen und dieser Knoten wird von Anfang an gelesen. Deine Schlüssel und dein Passwort bleiben unberührt.", "This key can spend your funds": "Dieser Schlüssel kann dein Guthaben ausgeben", + "this key is not one of the policy's signers": "dieser Schlüssel gehört nicht zu den Unterzeichnern der Richtlinie", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Dieser Schlüssel ist deine Wallet. Wer ihn hat, kann dein Guthaben ausgeben, und ohne ihn bedeutet ein verlorenes Gerät verlorenes Guthaben. Du kannst ihn in den Einstellungen mit deinem Passwort erneut ansehen.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Damit verlässt du den abgeschirmten Pool. Zieladresse und Betrag werden auf der anderen Chain öffentlich.", "This link is too long for a QR code — copy it instead.": "Dieser Link ist zu lang für einen QR-Code – kopiere ihn stattdessen.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "Diese Note wurde nicht gefunden.", "This passkey does not support the PRF extension.": "Dieser Passkey unterstützt die PRF-Erweiterung nicht.", "This proof was already submitted from another window. Check Activity before sending again.": "Dieser Beweis wurde bereits aus einem anderen Fenster übermittelt. Prüfe die Aktivität, bevor du erneut sendest.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Dieser Vorschlag läuft bei Höhe {expires} ab und die Chain steht bei {head}: zu spät zum Signieren. Fordere einen neuen Vorschlag an.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Dieser Vorschlag ist an ein anderes Genesis gebunden als das, das der Knoten dieser Wallet bedient.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "dieser Vorschlag gehört zu einer anderen Richtlinie: er nennt {theirs}, diese Richtlinie ist {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Dieser Vorschlag gilt für Chain {theirs}; diese Wallet ist auf Chain {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "dieser Prover verlangt {fee} RAND pro Beweis, und diese Version der Wallet zahlt keine Prover-Gebühr: Kopple einen Prover, der nichts verlangt, oder beweise auf diesem Gerät", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Dieser Prover verlangt eine Gebühr, die diese Version der Wallet nicht zahlt. Kopple einen Prover, der nichts verlangt, oder sende aus der Desktop-App.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Dieser Prover nimmt diese Art von Auftrag nicht an ({reason}). Kopple einen anderen Prover in den Einstellungen oder sende aus der Desktop-App.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Dadurch wird die Wallet von diesem Gerät entfernt. Zum Wiederherstellen brauchst du deinen Wiederherstellungsschlüssel.", "This screen could not be shown.": "Dieser Bildschirm konnte nicht angezeigt werden.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Dieser Bildschirm kommt in einem späteren Schritt. Deine Wallet ist entsperrt und bereit.", + "this signer is already saved as {label}": "dieser Unterzeichner ist bereits als {label} gespeichert", + "This signer profile has no key on this device: remove it and add it again.": "Dieses Unterzeichnerprofil hat auf diesem Gerät keinen Schlüssel: Entferne es und füge es erneut hinzu.", "This tab is showing what it has so far, and will refresh when that finishes.": "Dieser Tab zeigt, was er bisher hat, und aktualisiert sich, wenn das fertig ist.", "This takes a few minutes; keep this window open.": "Das dauert ein paar Minuten; lass dieses Fenster offen.", "This transaction was not found.": "Diese Transaktion wurde nicht gefunden.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "Gib {word} zur Bestätigung ein", "Type it once more.": "Gib es noch einmal ein.", "Type the last four characters of that address": "Gib die letzten vier Zeichen dieser Adresse ein", + "Type the last four characters of the account fingerprint": "Gib die letzten vier Zeichen des Konto-Fingerabdrucks ein", "unavailable in this app": "in dieser App nicht verfügbar", "unknown": "unbekannt", "unknown parameter {name}": "unbekannter Parameter {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "Schwach", "Welcome back": "Willkommen zurück", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Was der Pool dir zahlt, kommt als Note an, die deine Wallet bei der nächsten Synchronisierung findet.", + "What this proposal does, as checked against your policy": "Was dieser Vorschlag tut, geprüft gegen deine Richtlinie", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Wo dieses Gerät keinen Beweis erstellen kann, kopple einen Prover. Dein Ausgabeschlüssel bleibt in jedem Fall hier; ein Prover, den du selbst betreibst – die Desktop-App oder rand-prover auf deinem eigenen Rechner –, behält außerdem deinen Verlauf für dich.", "Where to?": "Wohin?", "Which address?": "Welche Adresse?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Dein Prover ist nach {n} Minute Beweisen nicht fertig. Setze später fort oder brich ab.", other: "Dein Prover ist nach {n} Minuten Beweisen nicht fertig. Setze später fort oder brich ab." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Dein Prover hat diesen Beweis nach {n} Minute in der Warteschlange nicht begonnen. Setze später fort oder brich ab.", other: "Dein Prover hat diesen Beweis nach {n} Minuten in der Warteschlange nicht begonnen. Setze später fort oder brich ab." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Die Kopplung deines Provers konnte nicht geöffnet werden. Sperre und entsperre die Wallet oder kopple den Prover in den Einstellungen erneut.", + "Your share — send it to whoever proves the proposal": "Dein Signaturanteil — schicke ihn an die Person, die den Vorschlag beweist", "Your wallet picks up what it pays you on its next sync.": "Deine Wallet findet bei der nächsten Synchronisierung, was dir ausgezahlt wird.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "Der Verlauf deiner Wallet wurde von {chain} gelesen – es wurde nichts geändert, und diese Wallet sendet oder fordert kein Guthaben an, bis beide übereinstimmen. Wechsle den Knoten in den Einstellungen oder scanne diese Wallet für die neue Chain neu.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Das ist die Adresse der {name}-Prover. Sie brauchen keine Kopplung: Diese Wallet verwendet sie standardmäßig.", diff --git a/ui/locales/en.js b/ui/locales/en.js index 9fa7246..f34e262 100644 --- a/ui/locales/en.js +++ b/ui/locales/en.js @@ -4,9 +4,10 @@ // value, or an object of CLDR plural categories ({one, few, many, other}) for a key with an // {n} hole whose translation inflects by number. `{name}` holes and the words in the glossary // at the top of each dictionary are kept as they are. -// 1007 strings. +// 1095 strings. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "{blocks} blocks left to prove it.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} peer", "{count} peers": "{count} peers", @@ -35,6 +36,7 @@ export default { "{label} is off": "{label} is off", "{label} is on": "{label} is on", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.", + "{matched} of {total} payments matched.": "{matched} of {total} payments matched.", "{member} answered with another key than the one this wallet pins": "{member} answered with another key than the one this wallet pins", "{member} did not answer": "{member} did not answer", "{member} does not take this wallet's jobs": "{member} does not take this wallet's jobs", @@ -57,6 +59,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} cannot be withdrawn", "{symbol} has {decimals} decimal places — that is more.": "{symbol} has {decimals} decimal places — that is more.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} is released as the coin you pick, on that coin's own chain.", + "{threshold} of {count} · signer {index}": "{threshold} of {count} · signer {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} of {count} · this key is signer {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}", "{units} units of asset {asset}": "{units} units of asset {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} answered first — chain {chain} · height {height}.", @@ -90,7 +94,12 @@ export default { "A prover is making the proof": "A prover is making the proof", "A prover sees your history": "A prover sees your history", "A prover will make the proof": "A prover will make the proof", + "a share is index:hex": "a share is index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.", + "a signer key is a rand-node keygen file or 64 hex characters": "a signer key is a rand-node keygen file or 64 hex characters", + "a signer label is 1-64 characters": "a signer label is 1-64 characters", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.", + "a signer seed is 64 hex characters": "a signer seed is 64 hex characters", "A site": "A site", "a spend key is 64 hex characters, or a wallet.key.json": "a spend key is 64 hex characters, or a wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.", @@ -107,8 +116,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.", "About": "About", "about {left} left": "about {left} left", + "Account fingerprint — compare it with every other signer before you save": "Account fingerprint — compare it with every other signer before you save", "Activity": "Activity", "Add a contact": "Add a contact", + "Add a signer": "Add a signer", + "Add payment": "Add payment", + "Add the policy file and your signer key to sign for a private multisig account.": "Add the policy file and your signer key to sign for a private multisig account.", "Address": "Address", "Address copied": "Address copied", "Address or payment link": "Address or payment link", @@ -117,14 +130,17 @@ export default { "almost done": "almost done", "also tried {endpoints}": "also tried {endpoints}", "Amount": "Amount", + "Amount in RAND": "Amount in RAND", "amount must be greater than zero": "amount must be greater than zero", "amount overflow": "amount overflow", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.", "amounts overflow": "amounts overflow", "an action is {n} characters, more than {max}": "an action is {n} characters, more than {max}", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.", "An address here is hexadecimal — 0-9 and a-f only.": "An address here is hexadecimal — 0-9 and a-f only.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "An address on this chain is 20 bytes — 40 hex characters. That is {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "An address on this chain is 32 bytes — 64 hex characters. That is {length}.", + "An amount is a number of RAND, such as 1.5.": "An amount is a number of RAND, such as 1.5.", "an unknown chain": "an unknown chain", "another tab changed this wallet while it was syncing; try again": "another tab changed this wallet while it was syncing; try again", "Another tab is syncing": "Another tab is syncing", @@ -191,11 +207,16 @@ export default { "Chains known": "Chains known", "Change back to you": "Change back to you", "Change it": "Change it", + "Check": "Check", + "Check a share": "Check a share", "Check Activity": "Check Activity", "Check Activity first": "Check Activity first", "Check before saving": "Check before saving", "Check fingerprint": "Check fingerprint", + "Check payments": "Check payments", + "Check proposal": "Check proposal", "Check settings": "Check settings", + "Check share": "Check share", "Checking it against the chain before anything is proved.": "Checking it against the chain before anything is proved.", "Checking the pool and your notes…": "Checking the pool and your notes…", "Checking the swap…": "Checking the swap…", @@ -222,6 +243,7 @@ export default { "Continue": "Continue", "Copied": "Copied", "Copied as JSON": "Copied as JSON", + "Copy": "Copy", "Copy address": "Copy address", "Copy as JSON": "Copy as JSON", "Copy link": "Copy link", @@ -270,9 +292,13 @@ export default { "Enter an amount to send.": "Enter an amount to send.", "Enter an amount.": "Enter an amount.", "Enter at least 10 characters": "Enter at least 10 characters", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.", + "Enter the address of every payment.": "Enter the address of every payment.", "Enter the address this should be released to.": "Enter the address this should be released to.", "Enter the address you are sending to.": "Enter the address you are sending to.", "Enter the amount as a number, for example 1.25.": "Enter the amount as a number, for example 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Enter the amount of every payment: it is what stops a split or an inflated payment.", "Enter the first 4 characters of your recovery key.": "Enter the first 4 characters of your recovery key.", "Enter the relayer fee as a number, for example 1.25.": "Enter the relayer fee as a number, for example 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Enter this wallet's password — the pairing is sealed under it.", @@ -282,6 +308,8 @@ export default { "Enter your recovery key.": "Enter your recovery key.", "Epoch": "Epoch", "Exactly those four letters, in capitals.": "Exactly those four letters, in capitals.", + "expected recipient {address}: {reason}": "expected recipient {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients must be a list of addresses", "Explore": "Explore", "Export spend key": "Export spend key", "Exporting the spend key is behind your password.": "Exporting the spend key is behind your password.", @@ -290,6 +318,7 @@ export default { "Fee": "Fee", "fee must be at least {fee} RAND (the bridge burn floor)": "fee must be at least {fee} RAND (the bridge burn floor)", "fee must be at least {fee} RAND (the bundle floor)": "fee must be at least {fee} RAND (the bundle floor)", + "Fee: {fee} RAND · expires at height {height}": "Fee: {fee} RAND · expires at height {height}", "Filter activity by asset": "Filter activity by asset", "Fingerprint": "Fingerprint", "fingerprint {fp}": "fingerprint {fp}", @@ -327,9 +356,11 @@ export default { "I understand anyone with this key can spend my funds": "I understand anyone with this key can spend my funds", "Import wallet": "Import wallet", "Incorrect password.": "Incorrect password.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "inputs hold {held}, but amount + fee is {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "inputs hold {held}, but fee + RAND deposit is {need}", "insufficient balance: have {have}, need {need}": "insufficient balance: have {have}, need {need}", + "Intended payments": "Intended payments", "is on chain {chain}, not {expected}": "is on chain {chain}, not {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "It cannot see your balance or your activity, and it cannot move anything.", "It cannot spend.": "It cannot spend.", @@ -352,6 +383,7 @@ export default { "key file has no spend_key": "key file has no spend_key", "key file must be version 2": "key file must be version 2", "Key hidden — enter your password to view it again.": "Key hidden — enter your password to view it again.", + "Label": "Label", "Language": "Language", "Leave empty to let the payer choose.": "Leave empty to let the payer choose.", "Light": "Light", @@ -362,11 +394,15 @@ export default { "machines": "machines", "malformed parameter {parameter}": "malformed parameter {parameter}", "Manage contacts": "Manage contacts", + "Manage multisig signers": "Manage multisig signers", "Max": "Max", "Memo": "Memo", "memo \"{memo}\"": "memo \"{memo}\"", "memo is {n} bytes, at most {max}": "memo is {n} bytes, at most {max}", "More is on the way": "More is on the way", + "Multisig share": "Multisig share", + "Multisig signers": "Multisig signers", + "Multisig signing is not available in this app.": "Multisig signing is not available in this app.", "My own prover · {name}": "My own prover · {name}", "Name": "Name", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.", @@ -390,12 +426,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "No contacts yet. Save one from Settings, or paste an address.", "no endpoint could answer {method}": "no endpoint could answer {method}", "no endpoint could answer {method}; tried {endpoints}": "no endpoint could answer {method}; tried {endpoints}", + "no expected recipient given: the payee was NOT checked": "no expected recipient given: the payee was NOT checked", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.", "No proof is pending.": "No proof is pending.", "No prover": "No prover", "No prover is paired.": "No prover is paired.", "No RAND to send yet": "No RAND to send yet", "No registry assets": "No registry assets", "no reply": "no reply", + "No signer profiles yet": "No signer profiles yet", + "No such signer profile.": "No such signer profile.", "No transaction or block matches that.": "No transaction or block matches that.", "no wallet on this device": "no wallet on this device", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.", @@ -442,9 +482,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount", "Open durian.market": "Open durian.market", + "Open file": "Open file", "Open in explorer": "Open in explorer", "Open in randscan": "Open in randscan", "Open in side panel": "Open in side panel", + "Open in the side panel to choose files": "Open in the side panel to choose files", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.", "Open this window from a site that asked to connect.": "Open this window from a site that asked to connect.", @@ -454,6 +496,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Or use the RandProtocol provers — machines run by the validators, each with its own key.", "Outcome unknown": "Outcome unknown", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "output {slot} does not open as this account's: refusing to sign value it cannot see", "own={value}, expected 1 or absent": "own={value}, expected 1 or absent", "Paid": "Paid", "Pair your own prover": "Pair your own prover", @@ -474,17 +517,20 @@ export default { "Paste your recovery key": "Paste your recovery key", "path is not {n} levels": "path is not {n} levels", "Pay me on Rand": "Pay me on Rand", + "Payee address": "Payee address", "Payment link": "Payment link", "Payment link · fingerprint {fingerprint}": "Payment link · fingerprint {fingerprint}", "Payment link copied": "Payment link copied", "Payment links cannot be read in this app; paste the address instead.": "Payment links cannot be read in this app; paste the address instead.", "Payment links cannot be read in this app.": "Payment links cannot be read in this app.", + "Payments changed: check them again before signing.": "Payments changed: check them again before signing.", "Peers": "Peers", "Pending": "Pending", "Permission to reach that host was not granted, so the node was left as it was.": "Permission to reach that host was not granted, so the node was left as it was.", "Permission to reach that host was not granted.": "Permission to reach that host was not granted.", "Permission to reach that prover was not granted, so nothing was saved.": "Permission to reach that prover was not granted, so nothing was saved.", "Point the camera at a Rand QR code.": "Point the camera at a Rand QR code.", + "Policy file": "Policy file", "Pool fee": "Pool fee", "Price impact": "Price impact", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.", @@ -494,6 +540,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.", + "Proposal (proposal.json)": "Proposal (proposal.json)", "Prove and send": "Prove and send", "Prove for my other devices": "Prove for my other devices", "Prover": "Prover", @@ -542,6 +589,7 @@ export default { "Relayer fee (optional)": "Relayer fee (optional)", "Remove": "Remove", "Remove {name}": "Remove {name}", + "Remove this signer": "Remove this signer", "Request RAND": "Request RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Request RAND from the faucet to try sending, receiving and proving on this network.", "Rescan": "Rescan", @@ -576,8 +624,12 @@ export default { "Running on": "Running on", "Save": "Save", "Save an address you pay often, and send to it by name.": "Save an address you pay often, and send to it by name.", + "Save as file": "Save as file", + "Save signer": "Save signer", "Save your recovery key": "Save your recovery key", "Saved": "Saved", + "Saved to {path}": "Saved to {path}", + "Saved.": "Saved.", "Scan": "Scan", "Scan a payment link": "Scan a payment link", "Scan a QR code": "Scan a QR code", @@ -598,6 +650,7 @@ export default { "Sent.": "Sent.", "Set on this device when this wallet was created.": "Set on this device when this wallet was created.", "Settings": "Settings", + "Share": "Share", "Share payment link": "Share payment link", "shielded address decodes to {n} bytes, expected {expected}": "shielded address decodes to {n} bytes, expected {expected}", "shielded address is not base58": "shielded address is not base58", @@ -609,6 +662,12 @@ export default { "Show the full address": "Show the full address", "Show viewing key": "Show viewing key", "Show viewing key for 10 seconds": "Show viewing key for 10 seconds", + "Sign": "Sign", + "Sign a proposal": "Sign a proposal", + "signer {index} is not in this policy (it has {count} signers)": "signer {index} is not in this policy (it has {count} signers)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.", + "signer {index}'s signature does not verify over this proposal": "signer {index}'s signature does not verify over this proposal", + "Signer key file, or its 64-hex seed": "Signer key file, or its 64-hex seed", "So the address above is one you have actually read.": "So the address above is one you have actually read.", "Something went wrong": "Something went wrong", "Something went wrong.": "Something went wrong.", @@ -641,6 +700,7 @@ export default { "That could not be copied.": "That could not be copied.", "That could not be saved.": "That could not be saved.", "That does not match — reveal the key again and check.": "That does not match — reveal the key again and check.", + "That file is larger than a proposal, a policy or a key file can be.": "That file is larger than a proposal, a policy or a key file can be.", "That is more than your balance of {balance} {symbol}.": "That is more than your balance of {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "That is neither 64 hex characters (a hash) nor a block height (digits only).", "That is not a pairing link.": "That is not a pairing link.", @@ -677,6 +737,8 @@ export default { "The fee could not be worked out.": "The fee could not be worked out.", "the genesis hash is not hex of a plausible length": "the genesis hash is not hex of a plausible length", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "the key file's address is not the one its seed derives", + "the key file's public_key is not the one its seed derives": "the key file's public_key is not the one its seed derives", "The key you exported when this wallet was created.": "The key you exported when this wallet was created.", "The language could not be saved.": "The language could not be saved.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "The link asks for {linked} {symbol}; you typed {typed} {symbol}.", @@ -717,6 +779,7 @@ export default { "The password that unlocks this wallet on this device.": "The password that unlocks this wallet on this device.", "The payer decides": "The payer decides", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.", + "the policy does not parse: {reason}": "the policy does not parse: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "The pool changed since this page read it. Nothing was sent; the site can quote again.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "The pool changed while this was being proved. Nothing was sent; the site can quote again.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.", @@ -727,6 +790,12 @@ export default { "the proof does not verify: {reason}": "the proof does not verify: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "the proof published a digest this wallet did not build ({published} for {expected}); not using it", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "The proposal check returned an incomplete answer, so this proposal is not signed.", + "the proposal does not parse: {reason}": "the proposal does not parse: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "the proposal's auth_commit is not this policy's account", + "the proposal's expected digest is not its bundle's": "the proposal's expected digest is not its bundle's", + "the proposal's signing_message is not its binding's": "the proposal's signing_message is not its binding's", "the prover": "the prover", "the prover accepted the job but named no job id": "the prover accepted the job but named no job id", "The prover answered with an unknown state ({state}).": "The prover answered with an unknown state ({state}).", @@ -788,6 +857,7 @@ export default { "The request was not approved in Rand Wallet.": "The request was not approved in Rand Wallet.", "The rescan could not be started.": "The rescan could not be started.", "The rescan did not finish": "The rescan did not finish", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key", "The scan was cancelled.": "The scan was cancelled.", "The side panel could not be opened.": "The side panel could not be opened.", "The site sent an invoke this wallet cannot read: {reason}.": "The site sent an invoke this wallet cannot read: {reason}.", @@ -798,6 +868,10 @@ export default { "The swap did not go through.": "The swap did not go through.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.", "The theme could not be saved.": "The theme could not be saved.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "the transaction carries an action; a private multisig proposal is a payment from its bundle alone", + "the transaction carries no bundle": "the transaction carries no bundle", + "the transaction does not hash to the proposal's binding": "the transaction does not hash to the proposal's binding", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "the transaction is for chain {tx}, the proposal says chain {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.", "The transfer could not be proved.": "The transfer could not be proved.", @@ -815,6 +889,9 @@ export default { "The wallet was told: {message}": "The wallet was told: {message}", "The withdrawal could not be proved.": "The withdrawal could not be proved.", "The withdrawal was submitted, but the node did not return a usable hash.": "The withdrawal was submitted, but the node did not return a usable hash.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "the witness's value cannot be checked: its nk or salt is not this proposal's", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier", "Theme": "Theme", "There is no pool for that pair.": "There is no pool for that pair.", "There is no program {program}… on this chain. Nothing was sent.": "There is no program {program}… on this chain. Nothing was sent.", @@ -857,6 +934,7 @@ export default { "This device, or where it cannot · {name} provers": "This device, or where it cannot · {name} provers", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.", "This key can spend your funds": "This key can spend your funds", + "this key is not one of the policy's signers": "this key is not one of the policy's signers", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "This leaves the shielded pool. The destination address and amount become public on the other chain.", "This link is too long for a QR code — copy it instead.": "This link is too long for a QR code — copy it instead.", @@ -871,7 +949,12 @@ export default { "This node’s token registry does not list this asset, so its name and decimal places are this wallet’s guess rather than the chain’s. It cannot be sent or withdrawn until a node lists it.": "This node’s token registry does not list this asset, so its name and decimal places are this wallet’s guess rather than the chain’s. It cannot be sent or withdrawn until a node lists it.", "This note was not found.": "This note was not found.", "This passkey does not support the PRF extension.": "This passkey does not support the PRF extension.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.", "This proof was already submitted from another window. Check Activity before sending again.": "This proof was already submitted from another window. Check Activity before sending again.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "This proposal is bound to another genesis than the one this wallet's node serves.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "this proposal is for another policy: it names {theirs}, this policy is {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "This proposal is for chain {theirs}; this wallet is on chain {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.", @@ -883,6 +966,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "This removes the wallet from this device. You will need your recovery key to restore it.", "This screen could not be shown.": "This screen could not be shown.", "This screen ships in a later task. Your wallet is unlocked and ready.": "This screen ships in a later task. Your wallet is unlocked and ready.", + "this signer is already saved as {label}": "this signer is already saved as {label}", + "This signer profile has no key on this device: remove it and add it again.": "This signer profile has no key on this device: remove it and add it again.", "This tab is showing what it has so far, and will refresh when that finishes.": "This tab is showing what it has so far, and will refresh when that finishes.", "This takes a few minutes; keep this window open.": "This takes a few minutes; keep this window open.", "This transaction was not found.": "This transaction was not found.", @@ -929,6 +1014,7 @@ export default { "Type {word} to confirm": "Type {word} to confirm", "Type it once more.": "Type it once more.", "Type the last four characters of that address": "Type the last four characters of that address", + "Type the last four characters of the account fingerprint": "Type the last four characters of the account fingerprint", "unavailable in this app": "unavailable in this app", "unknown": "unknown", "unknown parameter {name}": "unknown parameter {name}", @@ -965,6 +1051,7 @@ export default { "Weak": "Weak", "Welcome back": "Welcome back", "What the pool pays you arrives as a note your wallet finds on its next sync.": "What the pool pays you arrives as a note your wallet finds on its next sync.", + "What this proposal does, as checked against your policy": "What this proposal does, as checked against your policy", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.", "Where to?": "Where to?", "Which address?": "Which address?", @@ -1011,6 +1098,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.", + "Your share — send it to whoever proves the proposal": "Your share — send it to whoever proves the proposal", "Your wallet picks up what it pays you on its next sync.": "Your wallet picks up what it pays you on its next sync.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.", }; diff --git a/ui/locales/es.js b/ui/locales/es.js index fc9e265..7c9d1e7 100644 --- a/ui/locales/es.js +++ b/ui/locales/es.js @@ -8,6 +8,7 @@ // llave de acceso, pair — vincular. Plural categories (Intl.PluralRules('es')): one, many, other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Quedan {blocks} bloques para probarlo.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} par", many: "{count} pares", other: "{count} pares" }, "{count} peers": { one: "{count} par", many: "{count} pares", other: "{count} pares" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} está desactivado", "{label} is on": "{label} está activado", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} ya no abre esta billetera (su contraseña cambió). Desbloquéala con la contraseña y luego vuelve a activar {label} en Ajustes.", + "{matched} of {total} payments matched.": "{matched} de {total} pagos coinciden.", "{member} answered with another key than the one this wallet pins": "{member} respondió con una clave distinta de la que esta billetera tiene fijada", "{member} did not answer": "{member} no respondió", "{member} does not take this wallet's jobs": "{member} no acepta trabajos de esta billetera", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} no se puede retirar", "{symbol} has {decimals} decimal places — that is more.": "{symbol} tiene {decimals} decimales; este monto tiene más.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} se entrega como la moneda que elijas, en la propia cadena de esa moneda.", + "{threshold} of {count} · signer {index}": "{threshold} de {count} · firmante {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} de {count} · esta clave es el firmante {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} en la cadena {chain} tiene {decimals} decimales: el monto y la comisión del relayer deben ser múltiplos de {unit}", "{units} units of asset {asset}": "{units} unidades del activo {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} respondió primero: cadena {chain} · altura {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "La dirección de un probador debe ser https://.", "A prover is making the proof": "Un probador está generando la prueba", "A prover sees your history": "Un probador ve tu historial", + "a share is index:hex": "una parte de firma es index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Una billetera blindada para la red Rand. Los saldos y el historial se mantienen privados: cada envío se prueba en este dispositivo.", + "a signer key is a rand-node keygen file or 64 hex characters": "una clave de firmante es un archivo keygen de rand-node o 64 caracteres hexadecimales", + "a signer label is 1-64 characters": "la etiqueta de un firmante tiene 1-64 caracteres", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Un perfil de firmante se guarda en la bóveda cifrada de esta billetera. Cada firma vuelve a pedir tu contraseña.", + "a signer seed is 64 hex characters": "una semilla de firmante son 64 caracteres hexadecimales", "A site": "Un sitio", "a spend key is 64 hex characters, or a wallet.key.json": "una clave de gasto son 64 caracteres hexadecimales, o un wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Primero se busca como hash de transacción; si no se encuentra nada, el mismo hash se busca como bloque. Si son solo dígitos, se busca un bloque por altura.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Un retiro a una que no figura en la lista puede ser rechazado. No se gasta nada hasta que se pruebe.", "About": "Acerca de", "about {left} left": "quedan unos {left}", + "Account fingerprint — compare it with every other signer before you save": "Huella de la cuenta — compárala con la de cada otro firmante antes de guardar", "Activity": "Actividad", "Add a contact": "Agregar un contacto", + "Add a signer": "Añadir un firmante", + "Add payment": "Añadir pago", + "Add the policy file and your signer key to sign for a private multisig account.": "Añade el archivo de política y tu clave de firmante para firmar por una cuenta multifirma privada.", "Address": "Dirección", "Address copied": "Dirección copiada", "Address or payment link": "Dirección o enlace de pago", @@ -115,14 +128,18 @@ export default { "almost done": "casi listo", "also tried {endpoints}": "también se probó {endpoints}", "Amount": "Monto", + "Amount in RAND": "Monto en RAND", "amount must be greater than zero": "el monto debe ser mayor que cero", "amount overflow": "desbordamiento del monto", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Los montos no se pueden comprobar en pagos que no son en RAND: deja el monto vacío en esos.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Este beneficiario recibe RAND y también un token: los pagos en RAND y en tokens a un mismo beneficiario necesitan propuestas separadas.", "amounts overflow": "desbordamiento de los montos", "an action is {n} characters, more than {max}": { one: "una acción tiene {n} carácter, más de {max}", many: "una acción tiene {n} caracteres, más de {max}", other: "una acción tiene {n} caracteres, más de {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Una dirección (rand1…), un enlace de pago (randpay:…) o el nombre de un contacto guardado.", "An address here is hexadecimal — 0-9 and a-f only.": "Aquí una dirección es hexadecimal: solo 0-9 y a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Una dirección en esta cadena tiene 20 bytes: 40 caracteres hexadecimales. Esta tiene {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Una dirección en esta cadena tiene 32 bytes: 64 caracteres hexadecimales. Esta tiene {length}.", + "An amount is a number of RAND, such as 1.5.": "Un monto es una cantidad de RAND, como 1.5.", "an unknown chain": "una cadena desconocida", "another tab changed this wallet while it was syncing; try again": "otra pestaña cambió esta billetera mientras se sincronizaba; inténtalo de nuevo", "Another tab is syncing": "Otra pestaña está sincronizando", @@ -189,11 +206,16 @@ export default { "Chains known": "Cadenas conocidas", "Change back to you": "Cambio de vuelta para ti", "Change it": "Cambiarlo", + "Check": "Comprobar", + "Check a share": "Comprobar una parte de firma", "Check Activity": "Revisa Actividad", "Check Activity first": "Revisa Actividad primero", "Check before saving": "Verificar antes de guardar", "Check fingerprint": "Verificar huella", + "Check payments": "Comprobar pagos", + "Check proposal": "Comprobar propuesta", "Check settings": "Revisar ajustes", + "Check share": "Comprobar parte de firma", "Checking it against the chain before anything is proved.": "Verificándolo contra la cadena antes de probar nada.", "Checking the pool and your notes…": "Revisando el pool y tus notas…", "Checking the swap…": "Revisando el intercambio…", @@ -220,6 +242,7 @@ export default { "Continue": "Continuar", "Copied": "Copiado", "Copied as JSON": "Copiado como JSON", + "Copy": "Copiar", "Copy address": "Copiar dirección", "Copy as JSON": "Copiar como JSON", "Copy link": "Copiar enlace", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "Ingresa un monto para enviar.", "Enter an amount.": "Ingresa un monto.", "Enter at least 10 characters": "Ingresa al menos 10 caracteres", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Introduce cada pago que piensas hacer: la dirección y el monto en RAND. Un pago a la misma dirección dos veces se introduce una sola vez, con el total. Una salida que no coincide con ninguno no se puede firmar.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Introduce la dirección de cada beneficiario: una salida que no coincide con la dirección introducida, o cuya dirección está vacía, no se puede firmar.", + "Enter the address of every payment.": "Introduce la dirección de cada pago.", "Enter the address this should be released to.": "Ingresa la dirección a la que se debe liberar.", "Enter the address you are sending to.": "Ingresa la dirección a la que envías.", "Enter the amount as a number, for example 1.25.": "Ingresa el monto como un número, por ejemplo 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Introduce el monto de cada pago: es lo que impide una división o un pago inflado.", "Enter the first 4 characters of your recovery key.": "Ingresa los primeros 4 caracteres de tu clave de recuperación.", "Enter the relayer fee as a number, for example 1.25.": "Ingresa la comisión del relayer como un número, por ejemplo 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Ingresa la contraseña de esta billetera: la vinculación queda sellada con ella.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "Ingresa tu clave de recuperación.", "Epoch": "Época", "Exactly those four letters, in capitals.": "Exactamente esas cuatro letras, en mayúsculas.", + "expected recipient {address}: {reason}": "destinatario esperado {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients debe ser una lista de direcciones", "Explore": "Explorar", "Export spend key": "Exportar clave de gasto", "Exporting the spend key is behind your password.": "Exportar la clave de gasto requiere tu contraseña.", @@ -288,6 +317,7 @@ export default { "Fee": "Comisión", "fee must be at least {fee} RAND (the bridge burn floor)": "la comisión debe ser de al menos {fee} RAND (el mínimo de quema del puente)", "fee must be at least {fee} RAND (the bundle floor)": "la comisión debe ser de al menos {fee} RAND (el mínimo del paquete)", + "Fee: {fee} RAND · expires at height {height}": "Comisión: {fee} RAND · caduca en la altura {height}", "Filter activity by asset": "Filtrar actividad por activo", "Fingerprint": "Huella", "fingerprint {fp}": "huella {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "Entiendo que cualquiera con esta clave puede gastar mis fondos", "Import wallet": "Importar billetera", "Incorrect password.": "Contraseña incorrecta.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "las entradas suman {held}, pero monto + comisión es {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "las entradas suman {held}, pero comisión + depósito de RAND es {need}", "insufficient balance: have {have}, need {need}": "saldo insuficiente: tienes {have}, necesitas {need}", + "Intended payments": "Pagos previstos", "is on chain {chain}, not {expected}": "está en la cadena {chain}, no en {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "No puede ver tu saldo ni tu actividad, y no puede mover nada.", "It cannot spend.": "No puede gastar.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "el archivo de clave no tiene spend_key", "key file must be version 2": "el archivo de clave debe ser versión 2", "Key hidden — enter your password to view it again.": "Clave oculta: ingresa tu contraseña para verla de nuevo.", + "Label": "Etiqueta", "Language": "Idioma", "Leave empty to let the payer choose.": "Déjalo vacío para que elija quien paga.", "Light": "Claro", @@ -360,11 +393,15 @@ export default { "machines": "máquinas", "malformed parameter {parameter}": "parámetro mal formado {parameter}", "Manage contacts": "Administrar contactos", + "Manage multisig signers": "Gestionar firmantes multifirma", "Max": "Máx.", "Memo": "Memo", "memo \"{memo}\"": "memo \"{memo}\"", "memo is {n} bytes, at most {max}": { one: "el memo tiene {n} byte, como máximo {max}", many: "el memo tiene {n} bytes, como máximo {max}", other: "el memo tiene {n} bytes, como máximo {max}" }, "More is on the way": "Viene más en camino", + "Multisig share": "Parte de firma multifirma", + "Multisig signers": "Firmantes multifirma", + "Multisig signing is not available in this app.": "La firma multifirma no está disponible en esta app.", "My own prover · {name}": "Mi propio probador · {name}", "Name": "Nombre", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nombres para las direcciones a las que pagas a menudo, guardados solo en este dispositivo. Envía a un contacto por su nombre; su huella se verifica cuando lo guardas.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Todavía no hay contactos. Guarda uno desde Ajustes o pega una dirección.", "no endpoint could answer {method}": "ningún endpoint pudo responder {method}", "no endpoint could answer {method}; tried {endpoints}": "ningún endpoint pudo responder {method}; se probó {endpoints}", + "no expected recipient given: the payee was NOT checked": "no se indicó ningún destinatario esperado: el beneficiario NO se comprobó", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Ningún nodo respondió en {url}, así que no se puede comprobar la caducidad y esta propuesta no se firma. Reintenta cuando el nodo esté accesible.", "No proof is pending.": "No hay ninguna prueba pendiente.", "No prover": "Sin probador", "No prover is paired.": "No hay ningún probador vinculado.", "No RAND to send yet": "Todavía no hay RAND para enviar", "No registry assets": "No hay activos en el registro", "no reply": "sin respuesta", + "No signer profiles yet": "Aún no hay perfiles de firmante", + "No such signer profile.": "No existe ese perfil de firmante.", "No transaction or block matches that.": "Ninguna transacción ni bloque coincide con eso.", "no wallet on this device": "no hay billetera en este dispositivo", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Ninguno de los enlaces de probador de RandProtocol incluidos indica la clave que esta billetera tiene fijada para él; no se usan.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Una prueba se ejecuta en este dispositivo: unos dos minutos. Puedes ver otras pantallas; cerrar la billetera la detiene.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "solo hay {locked} bloqueado en esa moneda en la cadena {chain}; elige otro respaldo o un monto menor", "Open durian.market": "Abrir durian.market", + "Open file": "Abrir archivo", "Open in explorer": "Abrir en el explorador", "Open in randscan": "Abrir en randscan", "Open in side panel": "Abrir en el panel lateral", + "Open in the side panel to choose files": "Abre en el panel lateral para elegir archivos", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Abre Rand Wallet con {label} en lugar de escribir tu contraseña. Tu contraseña sigue funcionando y sigue siendo la forma de recuperar el acceso.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Abre la billetera con {label} en lugar de escribir tu contraseña. Tu contraseña sigue funcionando y sigue siendo la forma de recuperar el acceso.", "Open this window from a site that asked to connect.": "Abre esta ventana desde un sitio que haya pedido conectarse.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "O usa los probadores de RandProtocol: {n} máquina operada por los validadores, con su propia clave.", many: "O usa los probadores de RandProtocol: {n} máquinas operadas por los validadores, cada una con su propia clave.", other: "O usa los probadores de RandProtocol: {n} máquinas operadas por los validadores, cada una con su propia clave." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "O usa los probadores de RandProtocol: máquinas operadas por los validadores, cada una con su propia clave.", "Outcome unknown": "Resultado desconocido", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "la salida {slot} no se abre como de esta cuenta: se rechaza firmar un valor que no puede ver", "own={value}, expected 1 or absent": "own={value}, se esperaba 1 o ausente", "Paid": "Pagado", "Pair your own prover": "Vincula tu propio probador", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "Pega tu clave de recuperación", "path is not {n} levels": { one: "la ruta no tiene {n} nivel", many: "la ruta no tiene {n} niveles", other: "la ruta no tiene {n} niveles" }, "Pay me on Rand": "Págame en Rand", + "Payee address": "Dirección del beneficiario", "Payment link": "Enlace de pago", "Payment link · fingerprint {fingerprint}": "Enlace de pago · huella {fingerprint}", "Payment link copied": "Enlace de pago copiado", "Payment links cannot be read in this app; paste the address instead.": "Esta app no puede leer enlaces de pago; pega la dirección en su lugar.", "Payment links cannot be read in this app.": "Esta app no puede leer enlaces de pago.", + "Payments changed: check them again before signing.": "Los pagos cambiaron: compruébalos de nuevo antes de firmar.", "Peers": "Pares", "Pending": "Pendiente", "Permission to reach that host was not granted, so the node was left as it was.": "No se concedió permiso para conectar con ese host, así que el nodo quedó como estaba.", "Permission to reach that host was not granted.": "No se concedió permiso para conectar con ese host.", "Permission to reach that prover was not granted, so nothing was saved.": "No se concedió permiso para conectar con ese probador, así que no se guardó nada.", "Point the camera at a Rand QR code.": "Apunta la cámara a un código QR de Rand.", + "Policy file": "Archivo de política", "Pool fee": "Comisión del pool", "Price impact": "Impacto en el precio", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Precio según los pools de durian.market en esta cadena ({pct}% de comisión del pool). El intercambio es una transacción que esta billetera prueba y envía por sí misma.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Las pruebas se generan solo en este dispositivo. Cuando no pueda generar una, el envío espera hasta que vincules un probador o vuelvas a usar el probador de RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Las pruebas que este dispositivo no puede generar van a {name}. Su huella es {fingerprint}: verifica que el probador muestre la misma.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Las pruebas que este dispositivo no puede generar van a uno de ellos; cada uno que prueba un envío ve la clave de visualización de esa billetera.", + "Proposal (proposal.json)": "Propuesta (proposal.json)", "Prove and send": "Probar y enviar", "Prove for my other devices": "Probar para mis otros dispositivos", "Prover": "Probador", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "Comisión del relayer (opcional)", "Remove": "Eliminar", "Remove {name}": "Eliminar {name}", + "Remove this signer": "Quitar este firmante", "Request RAND": "Solicitar RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Solicita RAND al faucet para probar envíos, recepciones y pruebas en esta red.", "Rescan": "Reescanear", @@ -574,8 +623,12 @@ export default { "Running on": "Ejecutándose en", "Save": "Guardar", "Save an address you pay often, and send to it by name.": "Guarda una dirección a la que pagas a menudo y envíale por su nombre.", + "Save as file": "Guardar como archivo", + "Save signer": "Guardar firmante", "Save your recovery key": "Guarda tu clave de recuperación", "Saved": "Guardado", + "Saved to {path}": "Guardado en {path}", + "Saved.": "Guardado.", "Scan": "Escanear", "Scan a payment link": "Escanear un enlace de pago", "Scan a QR code": "Escanear un código QR", @@ -596,6 +649,7 @@ export default { "Sent.": "Enviado.", "Set on this device when this wallet was created.": "Se configuró en este dispositivo cuando se creó esta billetera.", "Settings": "Ajustes", + "Share": "Parte de firma", "Share payment link": "Compartir enlace de pago", "shielded address decodes to {n} bytes, expected {expected}": { one: "la dirección blindada se decodifica en {n} byte, se esperaban {expected}", many: "la dirección blindada se decodifica en {n} bytes, se esperaban {expected}", other: "la dirección blindada se decodifica en {n} bytes, se esperaban {expected}" }, "shielded address is not base58": "la dirección blindada no es base58", @@ -607,6 +661,12 @@ export default { "Show the full address": "Mostrar la dirección completa", "Show viewing key": "Mostrar clave de visualización", "Show viewing key for 10 seconds": "Mostrar la clave de visualización durante 10 segundos", + "Sign": "Firmar", + "Sign a proposal": "Firmar una propuesta", + "signer {index} is not in this policy (it has {count} signers)": "el firmante {index} no está en esta política (tiene {count} firmantes)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "La parte de firma del firmante {index} es válida. Tienes {have} de {threshold}: firmantes {list}.", + "signer {index}'s signature does not verify over this proposal": "la firma del firmante {index} no es válida para esta propuesta", + "Signer key file, or its 64-hex seed": "Archivo de clave de firmante, o su semilla de 64 hex", "So the address above is one you have actually read.": "Así, la dirección de arriba es una que de verdad leíste.", "Something went wrong": "Algo salió mal", "Something went wrong.": "Algo salió mal.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "No se pudo copiar.", "That could not be saved.": "No se pudo guardar.", "That does not match — reveal the key again and check.": "No coincide: vuelve a mostrar la clave y revísala.", + "That file is larger than a proposal, a policy or a key file can be.": "Ese archivo es mayor de lo que puede ser una propuesta, una política o un archivo de clave.", "That is more than your balance of {balance} {symbol}.": "Eso es más que tu saldo de {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Eso no son 64 caracteres hexadecimales (un hash) ni una altura de bloque (solo dígitos).", "That is not a pairing link.": "Eso no es un enlace de vinculación.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "No se pudo calcular la comisión.", "the genesis hash is not hex of a plausible length": "el hash génesis no es hexadecimal de una longitud plausible", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "el inflow es `none` exactamente cuando el paquete no quema ningún token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "la dirección del archivo de clave no es la que deriva su semilla", + "the key file's public_key is not the one its seed derives": "el public_key del archivo de clave no es el que deriva su semilla", "The key you exported when this wallet was created.": "La clave que exportaste cuando se creó esta billetera.", "The language could not be saved.": "No se pudo guardar el idioma.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "El enlace pide {linked} {symbol}; escribiste {typed} {symbol}.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "La contraseña que desbloquea esta billetera en este dispositivo.", "The payer decides": "Lo decide quien paga", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "La prueba pendiente se descartó (la billetera se bloqueó o se canceló en otro lugar), así que no se envió nada. Vuelve a enviar.", + "the policy does not parse: {reason}": "la política no se puede interpretar: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "El pool cambió desde que esta página lo leyó. No se envió nada; el sitio puede volver a cotizar.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "El pool cambió mientras se generaba la prueba. No se envió nada; el sitio puede volver a cotizar.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "El pool no tiene suficiente para pagar esto. No se envió nada; el sitio puede volver a cotizar.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "la prueba no se verifica: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "la prueba publicó un digest que esta billetera no construyó ({published} en lugar de {expected}); no se usa", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "La prueba se ejecuta en este dispositivo. Puedes ver otras pantallas (sigue avanzando), pero cerrar la billetera la detiene.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La comprobación de la propuesta devolvió una respuesta incompleta, así que esta propuesta no se firma.", + "the proposal does not parse: {reason}": "la propuesta no se puede interpretar: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "la propuesta caduca en {expires}, fuera de la ventana de su bundle ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "el auth_commit de la propuesta no es la cuenta de esta política", + "the proposal's expected digest is not its bundle's": "el digest esperado de la propuesta no es el de su bundle", + "the proposal's signing_message is not its binding's": "el signing_message de la propuesta no es el de su vinculación", "the prover": "el probador", "the prover accepted the job but named no job id": "el probador aceptó el trabajo pero no indicó ningún id de trabajo", "The prover answered with an unknown state ({state}).": "El probador respondió con un estado desconocido ({state}).", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "La solicitud no se aprobó en Rand Wallet.", "The rescan could not be started.": "No se pudo iniciar el reescaneo.", "The rescan did not finish": "El reescaneo no terminó", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "no se pudieron abrir los perfiles de firmante guardados; no se cambió nada. Restaura el almacenamiento de este dispositivo o borra la billetera y restáurala desde su clave", "The scan was cancelled.": "El escaneo se canceló.", "The side panel could not be opened.": "No se pudo abrir el panel lateral.", "The site sent an invoke this wallet cannot read: {reason}.": "El sitio envió una invocación que esta billetera no puede leer: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "El intercambio no se completó.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "El intercambio sigue en curso. Cerrar la billetera antes de que diga enviado lo detiene, y no se pierde nada.", "The theme could not be saved.": "No se pudo guardar el tema.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transacción lleva una acción; una propuesta multifirma privada es un pago solo desde su bundle", + "the transaction carries no bundle": "la transacción no lleva ningún bundle", + "the transaction does not hash to the proposal's binding": "el hash de la transacción no es la vinculación de la propuesta", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "la transacción es para la cadena {tx}, la propuesta dice la cadena {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "La clave de transacción revela este único pago (su monto y su destinatario) a quien se la des. No puede gastar nada.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Es posible que la transacción ya esté en la cadena. Revisa Actividad antes de volver a retirar: retirar dos veces quemaría dos veces.", "The transfer could not be proved.": "No se pudo generar la prueba de la transferencia.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "La billetera recibió este mensaje: {message}", "The withdrawal could not be proved.": "No se pudo generar la prueba del retiro.", "The withdrawal was submitted, but the node did not return a usable hash.": "El retiro se envió, pero el nodo no devolvió un hash utilizable.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "no se puede comprobar el valor del witness: su nk o su salt no son los de esta propuesta", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "el valor del witness no coincide con las salidas: el activo {asset} tiene {held} de entrada y {spent} de salida con la comisión y las quemas", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "el valor del witness no coincide con la transacción: la entrada {input} no genera el nullifier del bundle", "Theme": "Tema", "There is no pool for that pair.": "No hay ningún pool para ese par.", "There is no program {program}… on this chain. Nothing was sent.": "No existe el programa {program}… en esta cadena. No se envió nada.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "Este dispositivo o, donde no pueda, los probadores de {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Esto olvida las notas y el historial leídos de la cadena anterior y lee este nodo desde el principio. Tus claves y tu contraseña no se modifican.", "This key can spend your funds": "Esta clave puede gastar tus fondos", + "this key is not one of the policy's signers": "esta clave no es de ninguno de los firmantes de la política", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Esta clave es tu billetera. Cualquiera que la tenga puede gastar tus fondos y, sin ella, perder el dispositivo significa perder los fondos. Puedes volver a verla en Ajustes con tu contraseña.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Esto sale del pool blindado. La dirección de destino y el monto se vuelven públicos en la otra cadena.", "This link is too long for a QR code — copy it instead.": "Este enlace es demasiado largo para un código QR: cópialo en su lugar.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "No se encontró esta nota.", "This passkey does not support the PRF extension.": "Esta llave de acceso no admite la extensión PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Esta prueba ya se envió desde otra ventana. Revisa Actividad antes de volver a enviar.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Esta propuesta caduca en la altura {expires} y la cadena va por {head}: es tarde para firmar. Pide una propuesta nueva.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Esta propuesta está ligada a un génesis distinto del que sirve el nodo de esta billetera.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "esta propuesta es de otra política: nombra {theirs}, esta política es {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Esta propuesta es para la cadena {theirs}; esta billetera está en la cadena {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "este probador cobra {fee} RAND por prueba, y esta versión de la billetera no paga comisiones a probadores: vincula un probador que no cobre nada o genera la prueba en este dispositivo", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Este probador cobra una comisión que esta versión de la billetera no paga. Vincula un probador que no cobre nada o envía desde la app de escritorio.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Este probador no acepta este tipo de trabajo ({reason}). Vincula otro probador en Ajustes o envía desde la app de escritorio.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Esto elimina la billetera de este dispositivo. Necesitarás tu clave de recuperación para restaurarla.", "This screen could not be shown.": "No se pudo mostrar esta pantalla.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Esta pantalla llegará en una tarea posterior. Tu billetera está desbloqueada y lista.", + "this signer is already saved as {label}": "este firmante ya está guardado como {label}", + "This signer profile has no key on this device: remove it and add it again.": "Este perfil de firmante no tiene clave en este dispositivo: quítalo y vuelve a añadirlo.", "This tab is showing what it has so far, and will refresh when that finishes.": "Esta pestaña muestra lo que tiene hasta ahora y se actualizará cuando eso termine.", "This takes a few minutes; keep this window open.": "Esto tarda unos minutos; mantén esta ventana abierta.", "This transaction was not found.": "No se encontró esta transacción.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "Escribe {word} para confirmar", "Type it once more.": "Escríbela una vez más.", "Type the last four characters of that address": "Escribe los últimos cuatro caracteres de esa dirección", + "Type the last four characters of the account fingerprint": "Escribe los últimos cuatro caracteres de la huella de la cuenta", "unavailable in this app": "no disponible en esta app", "unknown": "desconocido", "unknown parameter {name}": "parámetro desconocido {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "Débil", "Welcome back": "Te damos la bienvenida de nuevo", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Lo que el pool te paga llega como una nota que tu billetera encuentra en su próxima sincronización.", + "What this proposal does, as checked against your policy": "Lo que hace esta propuesta, comprobado contra tu política", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Donde este dispositivo no pueda generar una prueba, vincula un probador. Tu clave de gasto se queda aquí de todos modos; un probador que ejecutes tú mismo (la app de escritorio o rand-prover en tu propia máquina) además mantiene tu historial en privado.", "Where to?": "¿A dónde?", "Which address?": "¿Qué dirección?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Tu probador no ha terminado tras {n} minuto generando la prueba. Reanuda más tarde o cancela.", many: "Tu probador no ha terminado tras {n} minutos generando la prueba. Reanuda más tarde o cancela.", other: "Tu probador no ha terminado tras {n} minutos generando la prueba. Reanuda más tarde o cancela." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Tu probador no ha empezado esta prueba tras {n} minuto en su cola. Reanuda más tarde o cancela.", many: "Tu probador no ha empezado esta prueba tras {n} minutos en su cola. Reanuda más tarde o cancela.", other: "Tu probador no ha empezado esta prueba tras {n} minutos en su cola. Reanuda más tarde o cancela." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "No se pudo abrir la vinculación de tu probador. Bloquea y desbloquea la billetera, o vuelve a vincular el probador en Ajustes.", + "Your share — send it to whoever proves the proposal": "Tu parte de firma — envíasela a quien pruebe la propuesta", "Your wallet picks up what it pays you on its next sync.": "Tu billetera recoge lo que te paga en su próxima sincronización.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "El historial de tu billetera se leyó de {chain}: no se cambió nada, y esta billetera no enviará ni pedirá fondos hasta que ambas coincidan. Cambia de nodo en Ajustes o reescanea esta billetera para la nueva cadena.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Esa es la dirección de los probadores de {name}. No necesitan vinculación: este monedero los usa de forma predeterminada.", diff --git a/ui/locales/fa.js b/ui/locales/fa.js index c4876f6..b53ac84 100644 --- a/ui/locales/fa.js +++ b/ui/locales/fa.js @@ -9,6 +9,7 @@ // {n}/{count} key is a plain string (one and other read the same). export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "{blocks} بلوک مانده تا اثبات شود.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} همتا", "{count} peers": "{count} همتا", @@ -37,6 +38,7 @@ export default { "{label} is off": "{label} خاموش است", "{label} is on": "{label} روشن است", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} دیگر این کیف پول را باز نمی‌کند (رمز عبور آن تغییر کرده است). با رمز عبور قفل را باز کنید، سپس {label} را دوباره در تنظیمات روشن کنید.", + "{matched} of {total} payments matched.": "{matched} از {total} پرداخت مطابقت دارد.", "{member} answered with another key than the one this wallet pins": "{member} با کلیدی غیر از کلیدی که این کیف پول ثبت کرده پاسخ داد", "{member} did not answer": "{member} پاسخ نداد", "{member} does not take this wallet's jobs": "{member} کارهای این کیف پول را نمی‌پذیرد", @@ -57,6 +59,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} قابل برداشت نیست", "{symbol} has {decimals} decimal places — that is more.": "{symbol} دارای {decimals} رقم اعشار است — این مقدار بیشتر است.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} به شکل کوینی که انتخاب می‌کنید، روی شبکه‌ی خود آن کوین آزاد می‌شود.", + "{threshold} of {count} · signer {index}": "{threshold} از {count} · امضاکننده {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} از {count} · این کلید امضاکننده {index} است", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} روی شبکه‌ی {chain} دارای {decimals} رقم اعشار است: مقدار و کارمزد رله‌کننده باید مضربی از {unit} باشند", "{units} units of asset {asset}": "{units} واحد از دارایی {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} اول پاسخ داد — شبکه‌ی {chain} · ارتفاع {height}.", @@ -89,7 +93,12 @@ export default { "A prover address must be https://.": "آدرس اثبات‌گر باید با https:// باشد.", "A prover is making the proof": "یک اثبات‌گر در حال ساختن اثبات است", "A prover sees your history": "اثبات‌گر تاریخچه‌ی شما را می‌بیند", + "a share is index:hex": "سهم امضا به شکل index:hex است", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "یک کیف پول محافظت‌شده برای شبکه‌ی Rand. موجودی‌ها و تاریخچه خصوصی می‌مانند — هر ارسال روی همین دستگاه اثبات می‌شود.", + "a signer key is a rand-node keygen file or 64 hex characters": "کلید امضاکننده یک فایل keygen از rand-node یا ۶۴ نویسه هگز است", + "a signer label is 1-64 characters": "برچسب امضاکننده ۱-۶۴ نویسه است", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "نمایه امضاکننده در گاوصندوق رمزگذاری‌شده این کیف پول نگهداری می‌شود. هر امضا دوباره رمز عبور شما را می‌خواهد.", + "a signer seed is 64 hex characters": "seed امضاکننده ۶۴ نویسه هگز است", "A site": "یک سایت", "a spend key is 64 hex characters, or a wallet.key.json": "کلید خرج 64 نویسه‌ی هگز است، یا یک wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "ابتدا هش تراکنش امتحان می‌شود؛ اگر چیزی پیدا نشود، همان هش به‌عنوان بلاک امتحان می‌شود. ورودیِ تمام‌رقمی، بلاک را بر اساس ارتفاع پیدا می‌کند.", @@ -106,8 +115,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "برداشت به شبکه‌ای که در این فهرست نیست ممکن است رد شود. تا زمانی که اثبات نشود چیزی خرج نمی‌شود.", "About": "درباره", "about {left} left": "حدود {left} باقی مانده", + "Account fingerprint — compare it with every other signer before you save": "اثر انگشت حساب — پیش از ذخیره، آن را با هر امضاکننده دیگر مقایسه کنید", "Activity": "فعالیت", "Add a contact": "افزودن مخاطب", + "Add a signer": "افزودن امضاکننده", + "Add payment": "افزودن پرداخت", + "Add the policy file and your signer key to sign for a private multisig account.": "فایل سیاست و کلید امضاکننده خود را اضافه کنید تا برای یک حساب چندامضایی خصوصی امضا کنید.", "Address": "آدرس", "Address copied": "آدرس کپی شد", "Address or payment link": "آدرس یا لینک پرداخت", @@ -116,14 +129,18 @@ export default { "almost done": "تقریباً تمام شد", "also tried {endpoints}": "{endpoints} هم امتحان شد", "Amount": "مقدار", + "Amount in RAND": "مقدار به RAND", "amount must be greater than zero": "مقدار باید بیشتر از صفر باشد", "amount overflow": "سرریز مقدار", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "مقدار پرداخت‌های غیر RAND قابل بررسی نیست: مقدار را برای آن‌ها خالی بگذارید.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "این گیرنده هم RAND و هم یک توکن دریافت می‌کند: پرداخت‌های RAND و توکن به یک گیرنده به پیشنهادهای جداگانه نیاز دارند.", "amounts overflow": "سرریز مقادیر", "an action is {n} characters, more than {max}": "یک اقدام {n} نویسه است، بیش از {max}", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "یک آدرس (rand1…)، یک لینک پرداخت (randpay:…)، یا نام یک مخاطب ذخیره‌شده.", "An address here is hexadecimal — 0-9 and a-f only.": "آدرس در اینجا هگزادسیمال است — فقط 0-9 و a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "آدرس در این شبکه 20 بایت است — 40 نویسه‌ی هگز. این یکی {length} است.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "آدرس در این شبکه 32 بایت است — 64 نویسه‌ی هگز. این یکی {length} است.", + "An amount is a number of RAND, such as 1.5.": "مقدار یک عدد RAND است، مانند 1.5.", "an unknown chain": "یک شبکه‌ی ناشناخته", "another tab changed this wallet while it was syncing; try again": "زبانه‌ی دیگری هنگام همگام‌سازی این کیف پول را تغییر داد؛ دوباره امتحان کنید", "Another tab is syncing": "زبانه‌ی دیگری در حال همگام‌سازی است", @@ -190,11 +207,16 @@ export default { "Chains known": "شبکه‌های شناخته‌شده", "Change back to you": "باقی‌مانده به خودتان", "Change it": "تغییرش دهید", + "Check": "بررسی", + "Check a share": "بررسی یک سهم امضا", "Check Activity": "فعالیت را بررسی کنید", "Check Activity first": "ابتدا فعالیت را بررسی کنید", "Check before saving": "پیش از ذخیره بررسی کنید", "Check fingerprint": "بررسی اثر انگشت", + "Check payments": "بررسی پرداخت‌ها", + "Check proposal": "بررسی پیشنهاد", "Check settings": "بررسی تنظیمات", + "Check share": "بررسی سهم امضا", "Checking it against the chain before anything is proved.": "پیش از هر اثباتی، با زنجیره مقایسه می‌شود.", "Checking the pool and your notes…": "در حال بررسی استخر و نوت‌های شما…", "Checking the swap…": "در حال بررسی سواپ…", @@ -221,6 +243,7 @@ export default { "Continue": "ادامه", "Copied": "کپی شد", "Copied as JSON": "به‌صورت JSON کپی شد", + "Copy": "کپی", "Copy address": "کپی آدرس", "Copy as JSON": "کپی به‌صورت JSON", "Copy link": "کپی لینک", @@ -269,9 +292,13 @@ export default { "Enter an amount to send.": "مقداری برای ارسال وارد کنید.", "Enter an amount.": "یک مقدار وارد کنید.", "Enter at least 10 characters": "دست‌کم 10 نویسه وارد کنید", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "هر پرداختی را که قصد انجامش را دارید وارد کنید: آدرس و مقدار به RAND. دو پرداخت به یک آدرس یک‌بار و با مجموع وارد می‌شود. خروجی‌ای که با هیچ‌کدام مطابقت نداشته باشد قابل امضا نیست.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "آدرس هر دریافت‌کننده را وارد کنید: خروجی‌ای که با آدرس واردشده مطابقت ندارد یا آدرسش خالی است قابل امضا نیست.", + "Enter the address of every payment.": "آدرس هر پرداخت را وارد کنید.", "Enter the address this should be released to.": "آدرسی را که این مبلغ باید به آن آزاد شود وارد کنید.", "Enter the address you are sending to.": "آدرسی را که به آن ارسال می‌کنید وارد کنید.", "Enter the amount as a number, for example 1.25.": "مقدار را به‌صورت عدد وارد کنید، برای نمونه 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "مقدار هر پرداخت را وارد کنید: همین است که جلوی تقسیم یا پرداخت باد‌شده را می‌گیرد.", "Enter the first 4 characters of your recovery key.": "4 نویسه‌ی نخست کلید بازیابی خود را وارد کنید.", "Enter the relayer fee as a number, for example 1.25.": "کارمزد رله‌کننده را به‌صورت عدد وارد کنید، برای نمونه 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "رمز عبور این کیف پول را وارد کنید — جفت‌سازی با آن مهر و موم می‌شود.", @@ -281,6 +308,8 @@ export default { "Enter your recovery key.": "کلید بازیابی خود را وارد کنید.", "Epoch": "دوره", "Exactly those four letters, in capitals.": "دقیقاً همان چهار حرف، با حروف بزرگ.", + "expected recipient {address}: {reason}": "دریافت‌کننده مورد انتظار {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients باید فهرستی از آدرس‌ها باشد", "Explore": "کاوش", "Export spend key": "صدور کلید خرج", "Exporting the spend key is behind your password.": "صدور کلید خرج نیازمند رمز عبور شماست.", @@ -289,6 +318,7 @@ export default { "Fee": "کارمزد", "fee must be at least {fee} RAND (the bridge burn floor)": "کارمزد باید دست‌کم {fee} RAND باشد (حداقل سوزاندن پل)", "fee must be at least {fee} RAND (the bundle floor)": "کارمزد باید دست‌کم {fee} RAND باشد (حداقل بسته)", + "Fee: {fee} RAND · expires at height {height}": "کارمزد: {fee} RAND · در ارتفاع {height} منقضی می‌شود", "Filter activity by asset": "فیلتر فعالیت بر اساس دارایی", "Fingerprint": "اثر انگشت", "fingerprint {fp}": "اثر انگشت {fp}", @@ -326,9 +356,11 @@ export default { "I understand anyone with this key can spend my funds": "متوجه هستم که هر کس این کلید را داشته باشد می‌تواند دارایی‌هایم را خرج کند", "Import wallet": "وارد کردن کیف پول", "Incorrect password.": "رمز عبور نادرست است.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "ورودی‌ها {held} دارند، اما مقدار + کارمزد {need} است", "inputs hold {held}, but fee + RAND deposit is {need}": "ورودی‌ها {held} دارند، اما کارمزد + واریز RAND برابر {need} است", "insufficient balance: have {have}, need {need}": "موجودی ناکافی: دارید {have}، لازم است {need}", + "Intended payments": "پرداخت‌های موردنظر", "is on chain {chain}, not {expected}": "روی شبکه‌ی {chain} است، نه {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "نمی‌تواند موجودی یا فعالیت شما را ببیند و نمی‌تواند چیزی را جابه‌جا کند.", "It cannot spend.": "نمی‌تواند خرج کند.", @@ -351,6 +383,7 @@ export default { "key file has no spend_key": "فایل کلید spend_key ندارد", "key file must be version 2": "فایل کلید باید نسخه‌ی 2 باشد", "Key hidden — enter your password to view it again.": "کلید پنهان شد — برای دیدن دوباره‌ی آن رمز عبور خود را وارد کنید.", + "Label": "برچسب", "Language": "زبان", "Leave empty to let the payer choose.": "خالی بگذارید تا پرداخت‌کننده انتخاب کند.", "Light": "روشن", @@ -361,11 +394,15 @@ export default { "machines": "دستگاه‌ها", "malformed parameter {parameter}": "پارامتر {parameter} بدشکل است", "Manage contacts": "مدیریت مخاطبان", + "Manage multisig signers": "مدیریت امضاکنندگان چندامضایی", "Max": "حداکثر", "Memo": "یادداشت", "memo \"{memo}\"": "یادداشت «{memo}»", "memo is {n} bytes, at most {max}": "یادداشت {n} بایت است، حداکثر {max}", "More is on the way": "موارد بیشتری در راه است", + "Multisig share": "سهم امضای چندامضایی", + "Multisig signers": "امضاکنندگان چندامضایی", + "Multisig signing is not available in this app.": "امضای چندامضایی در این برنامه در دسترس نیست.", "My own prover · {name}": "اثبات‌گر خودم · {name}", "Name": "نام", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "نام‌هایی برای آدرس‌هایی که اغلب به آن‌ها پرداخت می‌کنید، که فقط روی همین دستگاه نگه داشته می‌شوند. به یک مخاطب با نامش ارسال کنید؛ اثر انگشت آن هنگام ذخیره بررسی می‌شود.", @@ -389,12 +426,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "هنوز مخاطبی نیست. یکی را از تنظیمات ذخیره کنید، یا یک آدرس بچسبانید.", "no endpoint could answer {method}": "هیچ نقطه‌ی دسترسی‌ای نتوانست به {method} پاسخ دهد", "no endpoint could answer {method}; tried {endpoints}": "هیچ نقطه‌ی دسترسی‌ای نتوانست به {method} پاسخ دهد؛ امتحان‌شده‌ها: {endpoints}", + "no expected recipient given: the payee was NOT checked": "هیچ دریافت‌کننده مورد انتظاری داده نشد: دریافت‌کننده بررسی نشد (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "هیچ گره‌ای در {url} پاسخ نداد، پس انقضا قابل بررسی نیست و این پیشنهاد امضا نمی‌شود. وقتی گره در دسترس بود دوباره تلاش کنید.", "No proof is pending.": "هیچ اثباتی در انتظار نیست.", "No prover": "بدون اثبات‌گر", "No prover is paired.": "هیچ اثبات‌گری جفت نشده است.", "No RAND to send yet": "هنوز RAND برای ارسال نیست", "No registry assets": "دارایی ثبت‌شده‌ای نیست", "no reply": "بدون پاسخ", + "No signer profiles yet": "هنوز نمایه امضاکننده‌ای نیست", + "No such signer profile.": "چنین نمایه امضاکننده‌ای وجود ندارد.", "No transaction or block matches that.": "هیچ تراکنش یا بلاکی با آن مطابقت ندارد.", "no wallet on this device": "کیف پولی روی این دستگاه نیست", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "هیچ‌یک از لینک‌های داخلی اثبات‌گر RandProtocol کلیدی را که این کیف پول برای آن ثبت کرده نام نمی‌برد؛ از آن‌ها استفاده نمی‌شود.", @@ -441,9 +482,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "یک اثبات روی این دستگاه اجرا می‌شود — حدود دو دقیقه. می‌توانید صفحه‌های دیگر را ببینید؛ بستن کیف پول آن را متوقف می‌کند.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "فقط {locked} از آن کوین روی شبکه‌ی {chain} قفل شده است؛ پشتوانه‌ی دیگری یا مقدار کمتری انتخاب کنید", "Open durian.market": "باز کردن durian.market", + "Open file": "باز کردن فایل", "Open in explorer": "باز کردن در کاوشگر", "Open in randscan": "باز کردن در randscan", "Open in side panel": "باز کردن در پنل کناری", + "Open in the side panel to choose files": "برای انتخاب فایل‌ها در پنل کناری باز کنید", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Rand Wallet را به‌جای تایپ رمز عبور با {label} باز کنید. رمز عبور شما همچنان کار می‌کند و راه بازگرداندن دسترسی باقی می‌ماند.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "کیف پول را به‌جای تایپ رمز عبور با {label} باز کنید. رمز عبور شما همچنان کار می‌کند و راه بازگرداندن دسترسی باقی می‌ماند.", "Open this window from a site that asked to connect.": "این پنجره را از سایتی باز کنید که درخواست اتصال داده است.", @@ -453,6 +496,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "یا از اثبات‌گرهای RandProtocol استفاده کنید — {n} دستگاه که اعتبارسنج‌ها اداره می‌کنند، هر کدام با کلید خودش.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "یا از اثبات‌گرهای RandProtocol استفاده کنید — دستگاه‌هایی که اعتبارسنج‌ها اداره می‌کنند، هر کدام با کلید خودش.", "Outcome unknown": "نتیجه نامشخص", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "خروجی {slot} به‌عنوان خروجی این حساب باز نمی‌شود: امضای مقداری که دیده نمی‌شود رد می‌شود", "own={value}, expected 1 or absent": "own={value}، انتظار می‌رفت 1 یا خالی باشد", "Paid": "پرداخت‌شده", "Pair your own prover": "اثبات‌گر خودتان را جفت کنید", @@ -473,17 +517,20 @@ export default { "Paste your recovery key": "کلید بازیابی خود را بچسبانید", "path is not {n} levels": "مسیر {n} سطحی نیست", "Pay me on Rand": "روی Rand به من پرداخت کنید", + "Payee address": "آدرس دریافت‌کننده", "Payment link": "لینک پرداخت", "Payment link · fingerprint {fingerprint}": "لینک پرداخت · اثر انگشت {fingerprint}", "Payment link copied": "لینک پرداخت کپی شد", "Payment links cannot be read in this app; paste the address instead.": "لینک‌های پرداخت در این برنامه خوانده نمی‌شوند؛ به‌جای آن آدرس را بچسبانید.", "Payment links cannot be read in this app.": "لینک‌های پرداخت در این برنامه خوانده نمی‌شوند.", + "Payments changed: check them again before signing.": "پرداخت‌ها تغییر کردند: پیش از امضا دوباره بررسی‌شان کنید.", "Peers": "همتاها", "Pending": "در انتظار", "Permission to reach that host was not granted, so the node was left as it was.": "اجازه‌ی دسترسی به آن میزبان داده نشد، بنابراین گره همان‌طور که بود باقی ماند.", "Permission to reach that host was not granted.": "اجازه‌ی دسترسی به آن میزبان داده نشد.", "Permission to reach that prover was not granted, so nothing was saved.": "اجازه‌ی دسترسی به آن اثبات‌گر داده نشد، بنابراین چیزی ذخیره نشد.", "Point the camera at a Rand QR code.": "دوربین را به سمت یک کد QR از Rand بگیرید.", + "Policy file": "فایل سیاست", "Pool fee": "کارمزد استخر", "Price impact": "اثر قیمتی", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "قیمت‌گذاری با استخرهای durian.market در این شبکه ({pct}% کارمزد استخر). سواپ تراکنشی است که این کیف پول خودش اثبات و ارسال می‌کند.", @@ -493,6 +540,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "اثبات‌ها فقط روی این دستگاه ساخته می‌شوند. هر جا نتواند اثباتی بسازد، ارسال منتظر می‌ماند تا اثبات‌گری را جفت کنید یا دوباره از اثبات‌گر RandProtocol استفاده کنید.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "اثبات‌هایی که این دستگاه نمی‌تواند بسازد به {name} می‌روند. اثر انگشت آن {fingerprint} است — بررسی کنید که اثبات‌گر همین را نشان دهد.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "اثبات‌هایی که این دستگاه نمی‌تواند بسازد به یکی از آن‌ها می‌روند؛ هر کدام که یک ارسال را اثبات کند کلید مشاهده‌ی آن کیف پول را می‌بیند.", + "Proposal (proposal.json)": "پیشنهاد (proposal.json)", "Prove and send": "اثبات و ارسال", "Prove for my other devices": "اثبات برای دستگاه‌های دیگرم", "Prover": "اثبات‌گر", @@ -541,6 +589,7 @@ export default { "Relayer fee (optional)": "کارمزد رله‌کننده (اختیاری)", "Remove": "حذف", "Remove {name}": "حذف {name}", + "Remove this signer": "حذف این امضاکننده", "Request RAND": "درخواست RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "برای امتحان ارسال، دریافت و اثبات در این شبکه، از فاست RAND درخواست کنید.", "Rescan": "اسکن مجدد", @@ -575,8 +624,12 @@ export default { "Running on": "در حال اجرا روی", "Save": "ذخیره", "Save an address you pay often, and send to it by name.": "آدرسی را که اغلب به آن پرداخت می‌کنید ذخیره کنید و با نام به آن ارسال کنید.", + "Save as file": "ذخیره به‌صورت فایل", + "Save signer": "ذخیره امضاکننده", "Save your recovery key": "کلید بازیابی خود را ذخیره کنید", "Saved": "ذخیره شد", + "Saved to {path}": "در {path} ذخیره شد", + "Saved.": "ذخیره شد.", "Scan": "اسکن", "Scan a payment link": "اسکن لینک پرداخت", "Scan a QR code": "اسکن کد QR", @@ -597,6 +650,7 @@ export default { "Sent.": "ارسال شد.", "Set on this device when this wallet was created.": "هنگام ساختن این کیف پول روی این دستگاه تعیین شد.", "Settings": "تنظیمات", + "Share": "سهم امضا", "Share payment link": "اشتراک‌گذاری لینک پرداخت", "shielded address decodes to {n} bytes, expected {expected}": "آدرس محافظت‌شده به {n} بایت رمزگشایی می‌شود، انتظار می‌رفت {expected}", "shielded address is not base58": "آدرس محافظت‌شده base58 نیست", @@ -608,6 +662,12 @@ export default { "Show the full address": "نمایش آدرس کامل", "Show viewing key": "نمایش کلید مشاهده", "Show viewing key for 10 seconds": "نمایش کلید مشاهده به مدت 10 ثانیه", + "Sign": "امضا", + "Sign a proposal": "امضای یک پیشنهاد", + "signer {index} is not in this policy (it has {count} signers)": "امضاکننده {index} در این سیاست نیست (این سیاست {count} امضاکننده دارد)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "سهم امضای امضاکننده {index} معتبر است. {have} از {threshold} در دست است: امضاکنندگان {list}.", + "signer {index}'s signature does not verify over this proposal": "امضای امضاکننده {index} برای این پیشنهاد معتبر نیست", + "Signer key file, or its 64-hex seed": "فایل کلید امضاکننده، یا seed ۶۴ نویسه‌ای هگز آن", "So the address above is one you have actually read.": "تا آدرس بالا آدرسی باشد که واقعاً خوانده‌اید.", "Something went wrong": "مشکلی پیش آمد", "Something went wrong.": "مشکلی پیش آمد.", @@ -640,6 +700,7 @@ export default { "That could not be copied.": "کپی آن ممکن نشد.", "That could not be saved.": "ذخیره‌ی آن ممکن نشد.", "That does not match — reveal the key again and check.": "مطابقت ندارد — کلید را دوباره نمایش دهید و بررسی کنید.", + "That file is larger than a proposal, a policy or a key file can be.": "آن فایل از اندازه‌ای که یک پیشنهاد، سیاست یا فایل کلید می‌تواند داشته باشد بزرگ‌تر است.", "That is more than your balance of {balance} {symbol}.": "این بیش از موجودی {balance} {symbol} شماست.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "این نه 64 نویسه‌ی هگز (یک هش) است و نه ارتفاع بلاک (فقط رقم).", "That is not a pairing link.": "این لینک جفت‌سازی نیست.", @@ -675,6 +736,8 @@ export default { "The fee could not be worked out.": "محاسبه‌ی کارمزد ممکن نشد.", "the genesis hash is not hex of a plausible length": "هش پیدایش هگزی با طول قابل‌قبول نیست", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "ورودی دقیقاً زمانی `none` است که بسته هیچ توکنی نمی‌سوزاند (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "آدرس فایل کلید همان نیست که seed آن مشتق می‌کند", + "the key file's public_key is not the one its seed derives": "public_key فایل کلید همان نیست که seed آن مشتق می‌کند", "The key you exported when this wallet was created.": "کلیدی که هنگام ساختن این کیف پول صادر کردید.", "The language could not be saved.": "ذخیره‌ی زبان ممکن نشد.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "لینک {linked} {symbol} درخواست می‌کند؛ شما {typed} {symbol} وارد کردید.", @@ -715,6 +778,7 @@ export default { "The password that unlocks this wallet on this device.": "رمز عبوری که قفل این کیف پول را روی این دستگاه باز می‌کند.", "The payer decides": "پرداخت‌کننده تصمیم می‌گیرد", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "اثبات در انتظار پاک شد (کیف پول قفل شد، یا جای دیگری لغو شد)، بنابراین چیزی ارسال نشد. دوباره ارسال کنید.", + "the policy does not parse: {reason}": "سیاست تجزیه نمی‌شود: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "استخر از زمانی که این صفحه آن را خواند تغییر کرده است. چیزی ارسال نشد؛ سایت می‌تواند دوباره قیمت بدهد.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "استخر در حین اثبات تغییر کرد. چیزی ارسال نشد؛ سایت می‌تواند دوباره قیمت بدهد.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "استخر به اندازه‌ی کافی برای پرداخت این مبلغ ندارد. چیزی ارسال نشد؛ سایت می‌تواند دوباره قیمت بدهد.", @@ -725,6 +789,12 @@ export default { "the proof does not verify: {reason}": "اثبات تأیید نمی‌شود: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "اثبات چکیده‌ای منتشر کرد که این کیف پول نساخته بود ({published} به‌جای {expected})؛ از آن استفاده نمی‌شود", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "اثبات روی این دستگاه اجرا می‌شود. می‌توانید صفحه‌های دیگر را ببینید — ادامه پیدا می‌کند — اما بستن کیف پول آن را متوقف می‌کند.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "بررسی پیشنهاد پاسخ ناقصی برگرداند، پس این پیشنهاد امضا نمی‌شود.", + "the proposal does not parse: {reason}": "پیشنهاد تجزیه نمی‌شود: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "پیشنهاد در {expires} منقضی می‌شود، بیرون از بازه bundle خود ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "auth_commit پیشنهاد حساب این سیاست نیست", + "the proposal's expected digest is not its bundle's": "digest مورد انتظار پیشنهاد همان digest مربوط به bundle آن نیست", + "the proposal's signing_message is not its binding's": "signing_message پیشنهاد همان signing_message مربوط به binding آن نیست", "the prover": "اثبات‌گر", "the prover accepted the job but named no job id": "اثبات‌گر کار را پذیرفت اما شناسه‌ی کاری اعلام نکرد", "The prover answered with an unknown state ({state}).": "اثبات‌گر با وضعیتی ناشناخته پاسخ داد ({state}).", @@ -786,6 +856,7 @@ export default { "The request was not approved in Rand Wallet.": "درخواست در Rand Wallet تأیید نشد.", "The rescan could not be started.": "اسکن مجدد آغاز نشد.", "The rescan did not finish": "اسکن مجدد تمام نشد", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "نمایه‌های امضاکننده ذخیره‌شده باز نشدند؛ چیزی تغییر نکرد. حافظه این دستگاه را بازیابی کنید یا کیف پول را پاک کنید و از کلیدش بازیابی کنید", "The scan was cancelled.": "اسکن لغو شد.", "The side panel could not be opened.": "پنل کناری باز نشد.", "The site sent an invoke this wallet cannot read: {reason}.": "سایت فراخوانی‌ای فرستاد که این کیف پول نمی‌تواند بخواند: {reason}.", @@ -796,6 +867,10 @@ export default { "The swap did not go through.": "سواپ انجام نشد.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "سواپ ادامه دارد. بستن کیف پول پیش از آنکه «ارسال شد» را نشان دهد آن را متوقف می‌کند و چیزی از دست نمی‌رود.", "The theme could not be saved.": "ذخیره‌ی پوسته ممکن نشد.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "تراکنش یک اقدام دارد؛ پیشنهاد چندامضایی خصوصی فقط پرداختی از bundle خودش است", + "the transaction carries no bundle": "تراکنش هیچ bundle ای ندارد", + "the transaction does not hash to the proposal's binding": "هش تراکنش همان binding پیشنهاد نیست", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "تراکنش برای شبکه {tx} است، پیشنهاد شبکه {proposal} را می‌گوید", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "کلید تراکنش همین یک پرداخت — مقدار و گیرنده‌ی آن — را برای هر کسی که آن را به او بدهید آشکار می‌کند. نمی‌تواند چیزی خرج کند.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "ممکن است تراکنش از قبل روی زنجیره باشد. پیش از برداشت دوباره فعالیت را بررسی کنید — برداشت دوباره یعنی دو بار سوزاندن.", "The transfer could not be proved.": "اثبات انتقال ممکن نشد.", @@ -813,6 +888,9 @@ export default { "The wallet was told: {message}": "به کیف پول گفته شد: {message}", "The withdrawal could not be proved.": "اثبات برداشت ممکن نشد.", "The withdrawal was submitted, but the node did not return a usable hash.": "برداشت ثبت شد، اما گره هش قابل استفاده‌ای برنگرداند.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "مقدار witness قابل بررسی نیست: nk یا salt آن مال این پیشنهاد نیست", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "مقدار witness با خروجی‌ها نمی‌خواند: دارایی {asset} ورودی {held} و خروجی {spent} با کارمزد و سوزاندن‌ها دارد", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "مقدار witness با تراکنش نمی‌خواند: ورودی {input} به nullifier مربوط به bundle نمی‌رسد", "Theme": "پوسته", "There is no pool for that pair.": "برای این جفت استخری وجود ندارد.", "There is no program {program}… on this chain. Nothing was sent.": "برنامه‌ی {program}… در این شبکه وجود ندارد. چیزی ارسال نشد.", @@ -854,6 +932,7 @@ export default { "This device, or where it cannot · {name} provers": "این دستگاه، یا هر جا نتواند · اثبات‌گرهای {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "با این کار نوت‌ها و تاریخچه‌ای که از زنجیره‌ی قبلی خوانده شده فراموش می‌شود و این گره از ابتدا خوانده می‌شود. کلیدها و رمز عبور شما دست نمی‌خورند.", "This key can spend your funds": "این کلید می‌تواند دارایی‌های شما را خرج کند", + "this key is not one of the policy's signers": "این کلید یکی از امضاکنندگان سیاست نیست", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "این کلید همان کیف پول شماست. هر کس آن را داشته باشد می‌تواند دارایی‌های شما را خرج کند و بدون آن، گم شدن دستگاه یعنی از دست رفتن دارایی‌ها. می‌توانید آن را دوباره در تنظیمات با رمز عبور خود ببینید.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "این تراکنش استخر محافظت‌شده را ترک می‌کند. آدرس مقصد و مقدار در شبکه‌ی دیگر عمومی می‌شوند.", "This link is too long for a QR code — copy it instead.": "این لینک برای کد QR بیش از حد طولانی است — به‌جای آن کپی‌اش کنید.", @@ -869,6 +948,10 @@ export default { "This note was not found.": "این نوت پیدا نشد.", "This passkey does not support the PRF extension.": "این کلید عبور از افزونه‌ی PRF پشتیبانی نمی‌کند.", "This proof was already submitted from another window. Check Activity before sending again.": "این اثبات از قبل از پنجره‌ی دیگری ارسال شده است. پیش از ارسال دوباره فعالیت را بررسی کنید.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "این پیشنهاد در ارتفاع {expires} منقضی می‌شود و شبکه در {head} است: برای امضا دیر شده است. پیشنهاد جدید بخواهید.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "این پیشنهاد به genesis دیگری بسته شده است، نه آنچه گره این کیف پول ارائه می‌دهد.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "این پیشنهاد برای سیاست دیگری است: {theirs} را نام می‌برد، این سیاست {ours} است", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "این پیشنهاد برای شبکه {theirs} است؛ این کیف پول روی شبکه {ours} است.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "این اثبات‌گر برای هر اثبات {fee} RAND کارمزد می‌گیرد و این نسخه از کیف پول کارمزد اثبات‌گر را نمی‌پردازد: اثبات‌گری را جفت کنید که کارمزد نمی‌گیرد، یا روی این دستگاه اثبات کنید", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "این اثبات‌گر کارمزد می‌گیرد که این نسخه از کیف پول آن را نمی‌پردازد. اثبات‌گری را جفت کنید که کارمزد نمی‌گیرد، یا از برنامه‌ی دسکتاپ ارسال کنید.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "این اثبات‌گر این نوع کار را نمی‌پذیرد ({reason}). اثبات‌گر دیگری را در تنظیمات جفت کنید، یا از برنامه‌ی دسکتاپ ارسال کنید.", @@ -880,6 +963,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "این کار کیف پول را از این دستگاه حذف می‌کند. برای بازیابی آن به کلید بازیابی خود نیاز خواهید داشت.", "This screen could not be shown.": "نمایش این صفحه ممکن نشد.", "This screen ships in a later task. Your wallet is unlocked and ready.": "این صفحه در مرحله‌ای بعدی می‌آید. کیف پول شما باز است و آماده.", + "this signer is already saved as {label}": "این امضاکننده از قبل با نام {label} ذخیره شده است", + "This signer profile has no key on this device: remove it and add it again.": "این نمایه امضاکننده روی این دستگاه کلیدی ندارد: آن را حذف کنید و دوباره اضافه کنید.", "This tab is showing what it has so far, and will refresh when that finishes.": "این زبانه آنچه تاکنون دارد را نشان می‌دهد و وقتی آن کار تمام شود تازه‌سازی می‌شود.", "This takes a few minutes; keep this window open.": "این کار چند دقیقه طول می‌کشد؛ این پنجره را باز نگه دارید.", "This transaction was not found.": "این تراکنش پیدا نشد.", @@ -926,6 +1011,7 @@ export default { "Type {word} to confirm": "برای تأیید {word} را تایپ کنید", "Type it once more.": "یک بار دیگر تایپ کنید.", "Type the last four characters of that address": "چهار نویسه‌ی آخر آن آدرس را تایپ کنید", + "Type the last four characters of the account fingerprint": "چهار نویسه آخر اثر انگشت حساب را بنویسید", "unavailable in this app": "در این برنامه در دسترس نیست", "unknown": "نامشخص", "unknown parameter {name}": "پارامتر ناشناخته {name}", @@ -962,6 +1048,7 @@ export default { "Weak": "ضعیف", "Welcome back": "خوش برگشتید", "What the pool pays you arrives as a note your wallet finds on its next sync.": "آنچه استخر به شما می‌پردازد به شکل نوتی می‌رسد که کیف پول شما در همگام‌سازی بعدی پیدا می‌کند.", + "What this proposal does, as checked against your policy": "این پیشنهاد چه می‌کند، بررسی‌شده در برابر سیاست شما", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "هر جا این دستگاه نتواند اثباتی بسازد، یک اثبات‌گر جفت کنید. کلید خرج شما در هر صورت اینجا می‌ماند؛ اثبات‌گری که خودتان اجرا می‌کنید — برنامه‌ی دسکتاپ، یا rand-prover روی دستگاه خودتان — تاریخچه‌تان را هم پیش خودتان نگه می‌دارد.", "Where to?": "به کجا؟", "Which address?": "کدام آدرس؟", @@ -1008,6 +1095,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "اثبات‌گر شما پس از {n} دقیقه اثبات هنوز تمام نکرده است. بعداً ادامه دهید، یا لغو کنید.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "اثبات‌گر شما پس از {n} دقیقه در صف هنوز این اثبات را شروع نکرده است. بعداً ادامه دهید، یا لغو کنید.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "جفت‌سازی اثبات‌گر شما باز نشد. کیف پول را قفل و دوباره باز کنید، یا اثبات‌گر را دوباره در تنظیمات جفت کنید.", + "Your share — send it to whoever proves the proposal": "سهم امضای شما — آن را برای کسی بفرستید که پیشنهاد را اثبات می‌کند", "Your wallet picks up what it pays you on its next sync.": "کیف پول شما آنچه را به شما پرداخت می‌شود در همگام‌سازی بعدی دریافت می‌کند.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "تاریخچه‌ی کیف پول شما از {chain} خوانده شده است — چیزی تغییر نکرده است و این کیف پول تا زمانی که این دو با هم یکی نشوند دارایی ارسال یا درخواست نمی‌کند. گره را در تنظیمات عوض کنید، یا این کیف پول را برای شبکه‌ی جدید دوباره اسکن کنید.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "این نشانی اثبات‌گرهای {name} است. نیازی به جفت‌کردن ندارند: این کیف پول به‌طور پیش‌فرض از آن‌ها استفاده می‌کند.", diff --git a/ui/locales/fr.js b/ui/locales/fr.js index 60c03ee..360990d 100644 --- a/ui/locales/fr.js +++ b/ui/locales/fr.js @@ -10,6 +10,7 @@ // (Intl.PluralRules('fr')): one (0 and 1), many, other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Il reste {blocks} blocs pour la prouver.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} pair", many: "{count} pairs", other: "{count} pairs" }, "{count} peers": { one: "{count} pair", many: "{count} pairs", other: "{count} pairs" }, @@ -38,6 +39,7 @@ export default { "{label} is off": "{label} est désactivé", "{label} is on": "{label} est activé", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} n'ouvre plus ce portefeuille (son mot de passe a changé). Déverrouillez-le avec le mot de passe, puis réactivez {label} dans les Paramètres.", + "{matched} of {total} payments matched.": "{matched} paiements sur {total} correspondent.", "{member} answered with another key than the one this wallet pins": "{member} a répondu avec une autre clé que celle que ce portefeuille a enregistrée", "{member} did not answer": "{member} n'a pas répondu", "{member} does not take this wallet's jobs": "{member} n'accepte pas les tâches de ce portefeuille", @@ -58,6 +60,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} ne peut pas être retiré", "{symbol} has {decimals} decimal places — that is more.": "{symbol} a {decimals} décimales ; ce montant en a davantage.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} est versé dans la monnaie que vous choisissez, sur la chaîne propre à cette monnaie.", + "{threshold} of {count} · signer {index}": "{threshold} sur {count} · signataire {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} sur {count} · cette clé est le signataire {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} sur la chaîne {chain} a {decimals} décimales : le montant et les frais du relayeur doivent être des multiples de {unit}", "{units} units of asset {asset}": "{units} unités de l'actif {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} a répondu en premier : chaîne {chain} · hauteur {height}.", @@ -90,7 +94,12 @@ export default { "A prover address must be https://.": "L'adresse d'un prouveur doit être en https://.", "A prover is making the proof": "Un prouveur génère la preuve", "A prover sees your history": "Un prouveur voit votre historique", + "a share is index:hex": "une part de signature est index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Un portefeuille blindé pour le réseau Rand. Les soldes et l'historique restent privés : chaque envoi est prouvé sur cet appareil.", + "a signer key is a rand-node keygen file or 64 hex characters": "une clé de signataire est un fichier keygen de rand-node ou 64 caractères hexadécimaux", + "a signer label is 1-64 characters": "une étiquette de signataire compte 1-64 caractères", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Un profil de signataire est conservé dans le coffre chiffré de ce portefeuille. Chaque signature redemande votre mot de passe.", + "a signer seed is 64 hex characters": "une graine de signataire compte 64 caractères hexadécimaux", "A site": "Un site", "a spend key is 64 hex characters, or a wallet.key.json": "une clé de dépense fait 64 caractères hexadécimaux, ou est un wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "La recherche essaie d'abord un hash de transaction ; si rien n'est trouvé, le même hash est essayé comme bloc. Une saisie composée uniquement de chiffres recherche un bloc par sa hauteur.", @@ -107,8 +116,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Un retrait vers une chaîne qu'il ne prend pas en charge peut être refusé. Rien n'est dépensé avant que la preuve soit faite.", "About": "À propos", "about {left} left": "environ {left} restant", + "Account fingerprint — compare it with every other signer before you save": "Empreinte du compte — comparez-la avec celle de chaque autre signataire avant d'enregistrer", "Activity": "Activité", "Add a contact": "Ajouter un contact", + "Add a signer": "Ajouter un signataire", + "Add payment": "Ajouter un paiement", + "Add the policy file and your signer key to sign for a private multisig account.": "Ajoutez le fichier de politique et votre clé de signataire pour signer pour un compte multisig privé.", "Address": "Adresse", "Address copied": "Adresse copiée", "Address or payment link": "Adresse ou lien de paiement", @@ -117,14 +130,18 @@ export default { "almost done": "presque terminé", "also tried {endpoints}": "également essayé : {endpoints}", "Amount": "Montant", + "Amount in RAND": "Montant en RAND", "amount must be greater than zero": "le montant doit être supérieur à zéro", "amount overflow": "dépassement de capacité du montant", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Les montants ne peuvent pas être vérifiés pour les paiements hors RAND : laissez le montant vide pour ceux-là.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Ce bénéficiaire reçoit à la fois des RAND et un jeton : les paiements en RAND et en jetons à un même bénéficiaire exigent des propositions séparées.", "amounts overflow": "dépassement de capacité des montants", "an action is {n} characters, more than {max}": { one: "une action fait {n} caractère, plus que {max}", many: "une action fait {n} caractères, plus que {max}", other: "une action fait {n} caractères, plus que {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Une adresse (rand1…), un lien de paiement (randpay:…) ou le nom d'un contact enregistré.", "An address here is hexadecimal — 0-9 and a-f only.": "Ici, une adresse est hexadécimale : uniquement 0-9 et a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Une adresse sur cette chaîne fait 20 octets, soit 40 caractères hexadécimaux. Celle-ci en fait {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Une adresse sur cette chaîne fait 32 octets, soit 64 caractères hexadécimaux. Celle-ci en fait {length}.", + "An amount is a number of RAND, such as 1.5.": "Un montant est un nombre de RAND, comme 1.5.", "an unknown chain": "une chaîne inconnue", "another tab changed this wallet while it was syncing; try again": "un autre onglet a modifié ce portefeuille pendant la synchronisation ; réessayez", "Another tab is syncing": "Un autre onglet synchronise", @@ -191,11 +208,16 @@ export default { "Chains known": "Chaînes connues", "Change back to you": "Monnaie qui vous revient", "Change it": "Le modifier", + "Check": "Vérifier", + "Check a share": "Vérifier une part de signature", "Check Activity": "Consultez l'Activité", "Check Activity first": "Consultez d'abord l'Activité", "Check before saving": "Vérifier avant d'enregistrer", "Check fingerprint": "Vérifier l'empreinte", + "Check payments": "Vérifier les paiements", + "Check proposal": "Vérifier la proposition", "Check settings": "Vérifier les paramètres", + "Check share": "Vérifier la part de signature", "Checking it against the chain before anything is proved.": "Vérification auprès de la chaîne avant toute preuve.", "Checking the pool and your notes…": "Vérification du pool et de vos notes…", "Checking the swap…": "Vérification de l'échange…", @@ -222,6 +244,7 @@ export default { "Continue": "Continuer", "Copied": "Copié", "Copied as JSON": "Copié en JSON", + "Copy": "Copier", "Copy address": "Copier l'adresse", "Copy as JSON": "Copier en JSON", "Copy link": "Copier le lien", @@ -270,9 +293,13 @@ export default { "Enter an amount to send.": "Saisissez un montant à envoyer.", "Enter an amount.": "Saisissez un montant.", "Enter at least 10 characters": "Saisissez au moins 10 caractères", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Saisissez chaque paiement que vous comptez faire : l'adresse et le montant en RAND. Un paiement à la même adresse deux fois est saisi une seule fois, avec le total. Une sortie qui ne correspond à aucun ne peut pas être signée.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Saisissez l'adresse de chaque bénéficiaire : une sortie qui ne correspond pas à l'adresse saisie, ou dont l'adresse est vide, ne peut pas être signée.", + "Enter the address of every payment.": "Saisissez l'adresse de chaque paiement.", "Enter the address this should be released to.": "Saisissez l'adresse à laquelle verser les fonds.", "Enter the address you are sending to.": "Saisissez l'adresse du destinataire.", "Enter the amount as a number, for example 1.25.": "Saisissez le montant sous forme de nombre, par exemple 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Saisissez le montant de chaque paiement : c'est ce qui empêche un fractionnement ou un paiement gonflé.", "Enter the first 4 characters of your recovery key.": "Saisissez les 4 premiers caractères de votre clé de récupération.", "Enter the relayer fee as a number, for example 1.25.": "Saisissez les frais du relayeur sous forme de nombre, par exemple 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Saisissez le mot de passe de ce portefeuille : l'association est scellée avec lui.", @@ -282,6 +309,8 @@ export default { "Enter your recovery key.": "Saisissez votre clé de récupération.", "Epoch": "Époque", "Exactly those four letters, in capitals.": "Exactement ces quatre lettres, en majuscules.", + "expected recipient {address}: {reason}": "destinataire attendu {address} : {reason}", + "expected_recipients must be a list of addresses": "expected_recipients doit être une liste d'adresses", "Explore": "Explorer", "Export spend key": "Exporter la clé de dépense", "Exporting the spend key is behind your password.": "L'exportation de la clé de dépense est protégée par votre mot de passe.", @@ -290,6 +319,7 @@ export default { "Fee": "Frais", "fee must be at least {fee} RAND (the bridge burn floor)": "les frais doivent être d'au moins {fee} RAND (le minimum de brûlage du pont)", "fee must be at least {fee} RAND (the bundle floor)": "les frais doivent être d'au moins {fee} RAND (le minimum du lot)", + "Fee: {fee} RAND · expires at height {height}": "Frais : {fee} RAND · expire à la hauteur {height}", "Filter activity by asset": "Filtrer l'activité par actif", "Fingerprint": "Empreinte", "fingerprint {fp}": "empreinte {fp}", @@ -327,9 +357,11 @@ export default { "I understand anyone with this key can spend my funds": "Je comprends que quiconque possède cette clé peut dépenser mes fonds", "Import wallet": "Importer un portefeuille", "Incorrect password.": "Mot de passe incorrect.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "les entrées contiennent {held}, mais montant + frais font {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "les entrées contiennent {held}, mais frais + dépôt RAND font {need}", "insufficient balance: have {have}, need {need}": "solde insuffisant : disponible {have}, requis {need}", + "Intended payments": "Paiements prévus", "is on chain {chain}, not {expected}": "est sur la chaîne {chain}, et non {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Il ne peut voir ni votre solde ni votre activité, et ne peut rien déplacer.", "It cannot spend.": "Elle ne peut pas dépenser.", @@ -352,6 +384,7 @@ export default { "key file has no spend_key": "le fichier de clé n'a pas de spend_key", "key file must be version 2": "le fichier de clé doit être en version 2", "Key hidden — enter your password to view it again.": "Clé masquée : saisissez votre mot de passe pour l'afficher à nouveau.", + "Label": "Étiquette", "Language": "Langue", "Leave empty to let the payer choose.": "Laissez vide pour laisser le payeur choisir.", "Light": "Clair", @@ -362,11 +395,15 @@ export default { "machines": "machines", "malformed parameter {parameter}": "paramètre mal formé {parameter}", "Manage contacts": "Gérer les contacts", + "Manage multisig signers": "Gérer les signataires multisig", "Max": "Max", "Memo": "Mémo", "memo \"{memo}\"": "mémo « {memo} »", "memo is {n} bytes, at most {max}": { one: "le mémo fait {n} octet, au plus {max}", many: "le mémo fait {n} octets, au plus {max}", other: "le mémo fait {n} octets, au plus {max}" }, "More is on the way": "D'autres arrivent", + "Multisig share": "Part de signature multisig", + "Multisig signers": "Signataires multisig", + "Multisig signing is not available in this app.": "La signature multisig n'est pas disponible dans cette app.", "My own prover · {name}": "Mon propre prouveur · {name}", "Name": "Nom", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Des noms pour les adresses que vous payez souvent, conservés uniquement sur cet appareil. Envoyez à un contact par son nom ; son empreinte est vérifiée lorsque vous l'enregistrez.", @@ -390,12 +427,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Aucun contact pour l'instant. Enregistrez-en un depuis les Paramètres, ou collez une adresse.", "no endpoint could answer {method}": "aucun point d'accès n'a pu répondre à {method}", "no endpoint could answer {method}; tried {endpoints}": "aucun point d'accès n'a pu répondre à {method} ; essayés : {endpoints}", + "no expected recipient given: the payee was NOT checked": "aucun destinataire attendu n'a été indiqué : le bénéficiaire n'a PAS été vérifié", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Aucun nœud n'a répondu à {url}, l'expiration ne peut donc pas être vérifiée et cette proposition n'est pas signée. Réessayez quand le nœud sera joignable.", "No proof is pending.": "Aucune preuve n'est en attente.", "No prover": "Aucun prouveur", "No prover is paired.": "Aucun prouveur n'est associé.", "No RAND to send yet": "Pas encore de RAND à envoyer", "No registry assets": "Aucun actif dans le registre", "no reply": "pas de réponse", + "No signer profiles yet": "Aucun profil de signataire pour l'instant", + "No such signer profile.": "Ce profil de signataire n'existe pas.", "No transaction or block matches that.": "Aucune transaction ni aucun bloc ne correspond.", "no wallet on this device": "aucun portefeuille sur cet appareil", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Aucun des liens de prouveur RandProtocol intégrés n'indique la clé que ce portefeuille a enregistrée pour lui ; ils ne sont pas utilisés.", @@ -442,9 +483,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Une preuve s'exécute sur cet appareil, environ deux minutes. Vous pouvez consulter d'autres écrans ; fermer le portefeuille l'arrête.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "seulement {locked} est bloqué dans cette monnaie sur la chaîne {chain} ; choisissez une autre garantie ou un montant plus faible", "Open durian.market": "Ouvrir durian.market", + "Open file": "Ouvrir un fichier", "Open in explorer": "Ouvrir dans l'explorateur", "Open in randscan": "Ouvrir dans randscan", "Open in side panel": "Ouvrir dans le panneau latéral", + "Open in the side panel to choose files": "Ouvrez dans le panneau latéral pour choisir des fichiers", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Ouvrez Rand Wallet avec {label} au lieu de saisir votre mot de passe. Votre mot de passe fonctionne toujours et reste le moyen de rétablir l'accès.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Ouvrez le portefeuille avec {label} au lieu de saisir votre mot de passe. Votre mot de passe fonctionne toujours et reste le moyen de rétablir l'accès.", "Open this window from a site that asked to connect.": "Ouvrez cette fenêtre depuis un site qui a demandé à se connecter.", @@ -454,6 +497,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Ou utilisez les prouveurs RandProtocol : {n} machine gérée par les validateurs, avec sa propre clé.", many: "Ou utilisez les prouveurs RandProtocol : {n} machines gérées par les validateurs, chacune avec sa propre clé.", other: "Ou utilisez les prouveurs RandProtocol : {n} machines gérées par les validateurs, chacune avec sa propre clé." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Ou utilisez les prouveurs RandProtocol : des machines gérées par les validateurs, chacune avec sa propre clé.", "Outcome unknown": "Résultat inconnu", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "la sortie {slot} ne s'ouvre pas comme celle de ce compte : refus de signer une valeur qu'elle ne peut pas voir", "own={value}, expected 1 or absent": "own={value}, attendu : 1 ou absent", "Paid": "Payé", "Pair your own prover": "Associez votre propre prouveur", @@ -474,17 +518,20 @@ export default { "Paste your recovery key": "Collez votre clé de récupération", "path is not {n} levels": { one: "le chemin ne compte pas {n} niveau", many: "le chemin ne compte pas {n} niveaux", other: "le chemin ne compte pas {n} niveaux" }, "Pay me on Rand": "Payez-moi sur Rand", + "Payee address": "Adresse du bénéficiaire", "Payment link": "Lien de paiement", "Payment link · fingerprint {fingerprint}": "Lien de paiement · empreinte {fingerprint}", "Payment link copied": "Lien de paiement copié", "Payment links cannot be read in this app; paste the address instead.": "Cette application ne peut pas lire les liens de paiement ; collez plutôt l'adresse.", "Payment links cannot be read in this app.": "Cette application ne peut pas lire les liens de paiement.", + "Payments changed: check them again before signing.": "Les paiements ont changé : vérifiez-les de nouveau avant de signer.", "Peers": "Pairs", "Pending": "En attente", "Permission to reach that host was not granted, so the node was left as it was.": "L'autorisation de joindre cet hôte n'a pas été accordée ; le nœud est donc resté inchangé.", "Permission to reach that host was not granted.": "L'autorisation de joindre cet hôte n'a pas été accordée.", "Permission to reach that prover was not granted, so nothing was saved.": "L'autorisation de joindre ce prouveur n'a pas été accordée ; rien n'a donc été enregistré.", "Point the camera at a Rand QR code.": "Pointez la caméra vers un code QR Rand.", + "Policy file": "Fichier de politique", "Pool fee": "Frais du pool", "Price impact": "Impact sur le prix", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Prix fixé par les pools durian.market sur cette chaîne ({pct} % de frais de pool). L'échange est une transaction que ce portefeuille prouve et envoie lui-même.", @@ -494,6 +541,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Les preuves sont générées uniquement sur cet appareil. Lorsqu'il ne peut pas en générer une, l'envoi attend que vous associiez un prouveur ou que vous utilisiez de nouveau le prouveur RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Les preuves que cet appareil ne peut pas générer sont confiées à {name}. Son empreinte est {fingerprint} : vérifiez que le prouveur affiche la même.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Les preuves que cet appareil ne peut pas générer sont confiées à l'un d'eux ; chacun de ceux qui prouvent un envoi voit la clé de visualisation de ce portefeuille.", + "Proposal (proposal.json)": "Proposition (proposal.json)", "Prove and send": "Prouver et envoyer", "Prove for my other devices": "Prouver pour mes autres appareils", "Prover": "Prouveur", @@ -542,6 +590,7 @@ export default { "Relayer fee (optional)": "Frais du relayeur (facultatif)", "Remove": "Supprimer", "Remove {name}": "Supprimer {name}", + "Remove this signer": "Retirer ce signataire", "Request RAND": "Demander des RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Demandez des RAND au faucet pour essayer l'envoi, la réception et la génération de preuves sur ce réseau.", "Rescan": "Réanalyser", @@ -576,8 +625,12 @@ export default { "Running on": "Exécuté sur", "Save": "Enregistrer", "Save an address you pay often, and send to it by name.": "Enregistrez une adresse que vous payez souvent et envoyez-lui des fonds par son nom.", + "Save as file": "Enregistrer comme fichier", + "Save signer": "Enregistrer le signataire", "Save your recovery key": "Sauvegardez votre clé de récupération", "Saved": "Enregistré", + "Saved to {path}": "Enregistré dans {path}", + "Saved.": "Enregistré.", "Scan": "Scanner", "Scan a payment link": "Scanner un lien de paiement", "Scan a QR code": "Scanner un code QR", @@ -598,6 +651,7 @@ export default { "Sent.": "Envoyé.", "Set on this device when this wallet was created.": "Défini sur cet appareil lors de la création de ce portefeuille.", "Settings": "Paramètres", + "Share": "Part de signature", "Share payment link": "Partager le lien de paiement", "shielded address decodes to {n} bytes, expected {expected}": { one: "l'adresse blindée se décode en {n} octet, {expected} attendus", many: "l'adresse blindée se décode en {n} octets, {expected} attendus", other: "l'adresse blindée se décode en {n} octets, {expected} attendus" }, "shielded address is not base58": "l'adresse blindée n'est pas en base58", @@ -609,6 +663,12 @@ export default { "Show the full address": "Afficher l'adresse complète", "Show viewing key": "Afficher la clé de visualisation", "Show viewing key for 10 seconds": "Afficher la clé de visualisation pendant 10 secondes", + "Sign": "Signer", + "Sign a proposal": "Signer une proposition", + "signer {index} is not in this policy (it has {count} signers)": "le signataire {index} n'est pas dans cette politique (elle compte {count} signataires)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "La part de signature du signataire {index} est valide. {have} sur {threshold} réunies : signataires {list}.", + "signer {index}'s signature does not verify over this proposal": "la signature du signataire {index} n'est pas valide pour cette proposition", + "Signer key file, or its 64-hex seed": "Fichier de clé de signataire, ou sa graine de 64 hex", "So the address above is one you have actually read.": "Ainsi, l'adresse ci-dessus est bien une adresse que vous avez lue.", "Something went wrong": "Une erreur s'est produite", "Something went wrong.": "Une erreur s'est produite.", @@ -641,6 +701,7 @@ export default { "That could not be copied.": "Impossible de copier.", "That could not be saved.": "Impossible d'enregistrer.", "That does not match — reveal the key again and check.": "Cela ne correspond pas : affichez de nouveau la clé et vérifiez.", + "That file is larger than a proposal, a policy or a key file can be.": "Ce fichier est plus grand que ne peut l'être une proposition, une politique ou un fichier de clé.", "That is more than your balance of {balance} {symbol}.": "C'est plus que votre solde de {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Ce n'est ni 64 caractères hexadécimaux (un hash) ni une hauteur de bloc (chiffres uniquement).", "That is not a pairing link.": "Ce n'est pas un lien d'association.", @@ -676,6 +737,8 @@ export default { "The fee could not be worked out.": "Impossible de calculer les frais.", "the genesis hash is not hex of a plausible length": "le hash de genèse n'est pas une valeur hexadécimale de longueur plausible", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "l'inflow vaut `none` exactement lorsque le lot ne brûle aucun jeton (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "l'adresse du fichier de clé n'est pas celle que dérive sa graine", + "the key file's public_key is not the one its seed derives": "le public_key du fichier de clé n'est pas celui que dérive sa graine", "The key you exported when this wallet was created.": "La clé que vous avez exportée lors de la création de ce portefeuille.", "The language could not be saved.": "Impossible d'enregistrer la langue.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Le lien demande {linked} {symbol} ; vous avez saisi {typed} {symbol}.", @@ -716,6 +779,7 @@ export default { "The password that unlocks this wallet on this device.": "Le mot de passe qui déverrouille ce portefeuille sur cet appareil.", "The payer decides": "Le payeur décide", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "La preuve en attente a été effacée (le portefeuille s'est verrouillé, ou elle a été annulée ailleurs) ; rien n'a donc été envoyé. Renvoyez.", + "the policy does not parse: {reason}": "la politique ne peut pas être analysée : {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Le pool a changé depuis que cette page l'a lu. Rien n'a été envoyé ; le site peut proposer une nouvelle cotation.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Le pool a changé pendant la génération de la preuve. Rien n'a été envoyé ; le site peut proposer une nouvelle cotation.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Le pool ne détient pas assez pour verser ce montant. Rien n'a été envoyé ; le site peut proposer une nouvelle cotation.", @@ -726,6 +790,12 @@ export default { "the proof does not verify: {reason}": "la preuve ne se vérifie pas : {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "la preuve a publié un condensé que ce portefeuille n'a pas construit ({published} au lieu de {expected}) ; elle n'est pas utilisée", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "La preuve s'exécute sur cet appareil. Vous pouvez consulter d'autres écrans (elle continue), mais fermer le portefeuille l'arrête.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La vérification de la proposition a renvoyé une réponse incomplète, cette proposition n'est donc pas signée.", + "the proposal does not parse: {reason}": "la proposition ne peut pas être analysée : {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "la proposition expire à {expires}, hors de la fenêtre de son lot ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "l'auth_commit de la proposition n'est pas le compte de cette politique", + "the proposal's expected digest is not its bundle's": "le digest attendu de la proposition n'est pas celui de son lot", + "the proposal's signing_message is not its binding's": "le signing_message de la proposition n'est pas celui de sa liaison", "the prover": "le prouveur", "the prover accepted the job but named no job id": "le prouveur a accepté la tâche mais n'a indiqué aucun identifiant de tâche", "The prover answered with an unknown state ({state}).": "Le prouveur a répondu avec un état inconnu ({state}).", @@ -787,6 +857,7 @@ export default { "The request was not approved in Rand Wallet.": "La demande n'a pas été approuvée dans Rand Wallet.", "The rescan could not be started.": "Impossible de lancer la réanalyse.", "The rescan did not finish": "La réanalyse ne s'est pas terminée", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "les profils de signataire enregistrés n'ont pas pu être ouverts ; rien n'a été modifié. Restaurez le stockage de cet appareil, ou effacez le portefeuille et restaurez-le à partir de sa clé", "The scan was cancelled.": "La lecture a été annulée.", "The side panel could not be opened.": "Impossible d'ouvrir le panneau latéral.", "The site sent an invoke this wallet cannot read: {reason}.": "Le site a envoyé une invocation que ce portefeuille ne sait pas lire : {reason}.", @@ -797,6 +868,10 @@ export default { "The swap did not go through.": "L'échange n'a pas abouti.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "L'échange se poursuit. Fermer le portefeuille avant qu'il n'indique « envoyé » l'arrête, et rien n'est perdu.", "The theme could not be saved.": "Impossible d'enregistrer le thème.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transaction porte une action ; une proposition multisig privée est un paiement depuis son seul lot", + "the transaction carries no bundle": "la transaction ne porte aucun lot", + "the transaction does not hash to the proposal's binding": "le hash de la transaction n'est pas la liaison de la proposition", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "la transaction est pour la chaîne {tx}, la proposition indique la chaîne {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "La clé de transaction divulgue ce seul paiement (son montant et son destinataire) à la personne à qui vous la donnez. Elle ne peut rien dépenser.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "La transaction est peut-être déjà sur la chaîne. Consultez l'Activité avant de retirer à nouveau : retirer deux fois brûlerait deux fois.", "The transfer could not be proved.": "Impossible de prouver le transfert.", @@ -814,6 +889,9 @@ export default { "The wallet was told: {message}": "Le portefeuille a reçu ce message : {message}", "The withdrawal could not be proved.": "Impossible de prouver le retrait.", "The withdrawal was submitted, but the node did not return a usable hash.": "Le retrait a été soumis, mais le nœud n'a pas renvoyé de hash utilisable.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "la valeur du witness ne peut pas être vérifiée : son nk ou son sel n'est pas celui de cette proposition", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "la valeur du witness ne correspond pas aux sorties : l'actif {asset} a {held} en entrée et {spent} en sortie avec les frais et les brûlages", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "la valeur du witness ne correspond pas à la transaction : l'entrée {input} n'annule pas vers le nullifier du lot", "Theme": "Thème", "There is no pool for that pair.": "Il n'existe aucun pool pour cette paire.", "There is no program {program}… on this chain. Nothing was sent.": "Il n'existe aucun programme {program}… sur cette chaîne. Rien n'a été envoyé.", @@ -855,6 +933,7 @@ export default { "This device, or where it cannot · {name} provers": "Cet appareil ou, à défaut, les prouveurs {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Cela oublie les notes et l'historique lus sur l'ancienne chaîne et lit ce nœud depuis le début. Vos clés et votre mot de passe ne sont pas modifiés.", "This key can spend your funds": "Cette clé peut dépenser vos fonds", + "this key is not one of the policy's signers": "cette clé n'est pas l'un des signataires de la politique", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Cette clé est votre portefeuille. Quiconque la possède peut dépenser vos fonds et, sans elle, un appareil perdu signifie des fonds perdus. Vous pouvez l'afficher de nouveau dans les Paramètres avec votre mot de passe.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Ceci quitte le pool blindé. L'adresse de destination et le montant deviennent publics sur l'autre chaîne.", "This link is too long for a QR code — copy it instead.": "Ce lien est trop long pour un code QR : copiez-le plutôt.", @@ -870,6 +949,10 @@ export default { "This note was not found.": "Cette note est introuvable.", "This passkey does not support the PRF extension.": "Cette clé d'accès ne prend pas en charge l'extension PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Cette preuve a déjà été soumise depuis une autre fenêtre. Consultez l'Activité avant de renvoyer.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Cette proposition expire à la hauteur {expires} et la chaîne en est à {head} : trop tard pour signer. Demandez une nouvelle proposition.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Cette proposition est liée à un autre genesis que celui que sert le nœud de ce portefeuille.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "cette proposition est pour une autre politique : elle nomme {theirs}, cette politique est {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Cette proposition est pour la chaîne {theirs} ; ce portefeuille est sur la chaîne {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "ce prouveur facture {fee} RAND par preuve, et cette version du portefeuille ne paie pas les frais de prouveur : associez un prouveur gratuit, ou prouvez sur cet appareil", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Ce prouveur facture des frais, que cette version du portefeuille ne paie pas. Associez un prouveur gratuit, ou envoyez depuis l'application de bureau.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Ce prouveur n'accepte pas ce type de tâche ({reason}). Associez un autre prouveur dans les Paramètres, ou envoyez depuis l'application de bureau.", @@ -881,6 +964,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Cela supprime le portefeuille de cet appareil. Vous aurez besoin de votre clé de récupération pour le restaurer.", "This screen could not be shown.": "Impossible d'afficher cet écran.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Cet écran arrivera dans une étape ultérieure. Votre portefeuille est déverrouillé et prêt.", + "this signer is already saved as {label}": "ce signataire est déjà enregistré sous le nom {label}", + "This signer profile has no key on this device: remove it and add it again.": "Ce profil de signataire n'a pas de clé sur cet appareil : retirez-le et ajoutez-le de nouveau.", "This tab is showing what it has so far, and will refresh when that finishes.": "Cet onglet affiche ce qu'il a pour l'instant et s'actualisera une fois l'opération terminée.", "This takes a few minutes; keep this window open.": "Cela prend quelques minutes ; gardez cette fenêtre ouverte.", "This transaction was not found.": "Cette transaction est introuvable.", @@ -927,6 +1012,7 @@ export default { "Type {word} to confirm": "Saisissez {word} pour confirmer", "Type it once more.": "Saisissez-le une nouvelle fois.", "Type the last four characters of that address": "Saisissez les quatre derniers caractères de cette adresse", + "Type the last four characters of the account fingerprint": "Saisissez les quatre derniers caractères de l'empreinte du compte", "unavailable in this app": "indisponible dans cette application", "unknown": "inconnu", "unknown parameter {name}": "paramètre inconnu {name}", @@ -963,6 +1049,7 @@ export default { "Weak": "Faible", "Welcome back": "Bon retour", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Ce que le pool vous verse arrive sous forme d'une note que votre portefeuille trouve lors de sa prochaine synchronisation.", + "What this proposal does, as checked against your policy": "Ce que fait cette proposition, vérifié par rapport à votre politique", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Lorsque cet appareil ne peut pas générer une preuve, associez un prouveur. Votre clé de dépense reste ici dans tous les cas ; un prouveur que vous exécutez vous-même (l'application de bureau, ou rand-prover sur votre propre machine) garde aussi votre historique pour vous.", "Where to?": "Vers où ?", "Which address?": "Quelle adresse ?", @@ -1009,6 +1096,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Votre prouveur n'a pas terminé après {n} minute de preuve. Reprenez plus tard, ou annulez.", many: "Votre prouveur n'a pas terminé après {n} minutes de preuve. Reprenez plus tard, ou annulez.", other: "Votre prouveur n'a pas terminé après {n} minutes de preuve. Reprenez plus tard, ou annulez." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Votre prouveur n'a pas commencé cette preuve après {n} minute dans sa file d'attente. Reprenez plus tard, ou annulez.", many: "Votre prouveur n'a pas commencé cette preuve après {n} minutes dans sa file d'attente. Reprenez plus tard, ou annulez.", other: "Votre prouveur n'a pas commencé cette preuve après {n} minutes dans sa file d'attente. Reprenez plus tard, ou annulez." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Impossible d'ouvrir l'association de votre prouveur. Verrouillez puis déverrouillez le portefeuille, ou associez de nouveau le prouveur dans les Paramètres.", + "Your share — send it to whoever proves the proposal": "Votre part de signature — envoyez-la à qui prouve la proposition", "Your wallet picks up what it pays you on its next sync.": "Votre portefeuille récupère ce qu'il vous verse lors de sa prochaine synchronisation.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "L'historique de votre portefeuille a été lu sur {chain} : rien n'a été modifié, et ce portefeuille n'enverra ni ne demandera de fonds tant que les deux ne concordent pas. Changez de nœud dans les Paramètres, ou réanalysez ce portefeuille pour la nouvelle chaîne.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "C'est l'adresse des prouveurs {name}. Ils ne demandent aucune association : ce portefeuille les utilise par défaut.", diff --git a/ui/locales/hi.js b/ui/locales/hi.js index f3637d7..c0932c1 100644 --- a/ui/locales/hi.js +++ b/ui/locales/hi.js @@ -8,6 +8,7 @@ // memo — मेमो, amount — राशि, recipient — प्राप्तकर्ता, rescan — फिर से स्कैन करें, burn — बर्न. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "इसे प्रूव करने के लिए {blocks} ब्लॉक बाकी हैं।", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} पीयर", "{count} peers": "{count} पीयर", @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} बंद है", "{label} is on": "{label} चालू है", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} अब यह वॉलेट नहीं खोलता (इसका पासवर्ड बदल गया है)। पासवर्ड से अनलॉक करें, फिर सेटिंग्स में {label} दोबारा चालू करें।", + "{matched} of {total} payments matched.": "{total} में से {matched} भुगतान मेल खाते हैं।", "{member} answered with another key than the one this wallet pins": "{member} ने उस की से अलग की के साथ जवाब दिया जिसे यह वॉलेट पिन करता है", "{member} did not answer": "{member} ने जवाब नहीं दिया", "{member} does not take this wallet's jobs": "{member} इस वॉलेट के काम नहीं लेता", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} निकाला नहीं जा सकता", "{symbol} has {decimals} decimal places — that is more.": "{symbol} में {decimals} दशमलव स्थान हैं — यह उससे ज़्यादा है।", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} आपके चुने हुए कॉइन के रूप में, उसी कॉइन की अपनी चेन पर जारी होता है।", + "{threshold} of {count} · signer {index}": "{count} में से {threshold} · हस्ताक्षरकर्ता {index}", + "{threshold} of {count} · this key is signer {index}": "{count} में से {threshold} · यह की हस्ताक्षरकर्ता {index} है", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "चेन {chain} पर {token} में {decimals} दशमलव हैं: राशि और रिलेयर शुल्क {unit} के गुणज होने चाहिए", "{units} units of asset {asset}": "एसेट {asset} की {units} इकाइयाँ", "{url} answered first — chain {chain} · height {height}.": "{url} ने सबसे पहले जवाब दिया — चेन {chain} · ऊँचाई {height}।", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "प्रूवर का पता https:// होना चाहिए।", "A prover is making the proof": "एक प्रूवर प्रूफ़ बना रहा है", "A prover sees your history": "प्रूवर आपका इतिहास देखता है", + "a share is index:hex": "हस्ताक्षर अंश index:hex रूप में होता है", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand नेटवर्क के लिए शील्डेड वॉलेट। बैलेंस और इतिहास निजी रहते हैं — हर भेजने का प्रूफ़ इसी डिवाइस पर बनता है।", + "a signer key is a rand-node keygen file or 64 hex characters": "हस्ताक्षरकर्ता की एक rand-node keygen फ़ाइल या 64 हेक्स अक्षर होती है", + "a signer label is 1-64 characters": "हस्ताक्षरकर्ता का लेबल 1-64 अक्षरों का होता है", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "हस्ताक्षरकर्ता प्रोफ़ाइल इस वॉलेट के एन्क्रिप्टेड वॉल्ट में रखी जाती है। हर हस्ताक्षर पर आपका पासवर्ड फिर माँगा जाता है।", + "a signer seed is 64 hex characters": "हस्ताक्षरकर्ता का सीड 64 हेक्स अक्षरों का होता है", "A site": "एक साइट", "a spend key is 64 hex characters, or a wallet.key.json": "स्पेंड की 64 hex अक्षरों की होती है, या एक wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "पहले लेन-देन हैश आज़माया जाता है; कुछ न मिले तो वही हैश ब्लॉक के रूप में आज़माया जाता है। सिर्फ़ अंक हों तो ऊँचाई से ब्लॉक खोजा जाता है।", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "जो इसकी सूची में नहीं है, उसकी ओर निकासी अस्वीकार हो सकती है। प्रूफ़ बनने तक कुछ खर्च नहीं होता।", "About": "परिचय", "about {left} left": "लगभग {left} बाकी", + "Account fingerprint — compare it with every other signer before you save": "खाते का फ़िंगरप्रिंट — सहेजने से पहले इसे हर दूसरे हस्ताक्षरकर्ता से मिलाएँ", "Activity": "गतिविधि", "Add a contact": "संपर्क जोड़ें", + "Add a signer": "हस्ताक्षरकर्ता जोड़ें", + "Add payment": "भुगतान जोड़ें", + "Add the policy file and your signer key to sign for a private multisig account.": "निजी मल्टीसिग खाते के लिए हस्ताक्षर करने हेतु पॉलिसी फ़ाइल और अपनी हस्ताक्षरकर्ता की जोड़ें।", "Address": "पता", "Address copied": "पता कॉपी हो गया", "Address or payment link": "पता या भुगतान लिंक", @@ -115,14 +128,18 @@ export default { "almost done": "लगभग पूरा", "also tried {endpoints}": "{endpoints} भी आज़माए", "Amount": "राशि", + "Amount in RAND": "राशि (RAND में)", "amount must be greater than zero": "राशि शून्य से ज़्यादा होनी चाहिए", "amount overflow": "राशि की सीमा पार हो गई", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND के अलावा अन्य भुगतानों की राशि जाँची नहीं जा सकती: उनके लिए राशि खाली छोड़ें।", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "यह प्राप्तकर्ता RAND और एक टोकन दोनों प्राप्त करता है: एक ही प्राप्तकर्ता को RAND और टोकन भुगतानों के लिए अलग-अलग प्रस्ताव चाहिए।", "amounts overflow": "राशियों की सीमा पार हो गई", "an action is {n} characters, more than {max}": "एक एक्शन {n} अक्षरों का है, {max} से ज़्यादा", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "एक पता (rand1…), भुगतान लिंक (randpay:…), या सहेजे गए संपर्क का नाम।", "An address here is hexadecimal — 0-9 and a-f only.": "यहाँ पता हेक्साडेसिमल होता है — केवल 0-9 और a-f।", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "इस चेन पर पता 20 बाइट का होता है — 40 hex अक्षर। यह {length} है।", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "इस चेन पर पता 32 बाइट का होता है — 64 hex अक्षर। यह {length} है।", + "An amount is a number of RAND, such as 1.5.": "राशि RAND की एक संख्या होती है, जैसे 1.5।", "an unknown chain": "अज्ञात चेन", "another tab changed this wallet while it was syncing; try again": "सिंक के दौरान किसी दूसरे टैब ने यह वॉलेट बदल दिया; फिर से कोशिश करें", "Another tab is syncing": "दूसरा टैब सिंक कर रहा है", @@ -189,11 +206,16 @@ export default { "Chains known": "ज्ञात चेन", "Change back to you": "आपको वापस मिलने वाला बचा हिस्सा", "Change it": "इसे बदलें", + "Check": "जाँचें", + "Check a share": "हस्ताक्षर अंश जाँचें", "Check Activity": "गतिविधि देखें", "Check Activity first": "पहले गतिविधि देखें", "Check before saving": "सहेजने से पहले जाँचें", "Check fingerprint": "फ़िंगरप्रिंट जाँचें", + "Check payments": "भुगतान जाँचें", + "Check proposal": "प्रस्ताव जाँचें", "Check settings": "सेटिंग्स जाँचें", + "Check share": "हस्ताक्षर अंश जाँचें", "Checking it against the chain before anything is proved.": "कोई भी प्रूफ़ बनने से पहले इसे चेन से मिलाया जा रहा है।", "Checking the pool and your notes…": "पूल और आपके नोट जाँचे जा रहे हैं…", "Checking the swap…": "स्वैप जाँचा जा रहा है…", @@ -220,6 +242,7 @@ export default { "Continue": "जारी रखें", "Copied": "कॉपी हो गया", "Copied as JSON": "JSON के रूप में कॉपी हो गया", + "Copy": "कॉपी करें", "Copy address": "पता कॉपी करें", "Copy as JSON": "JSON के रूप में कॉपी करें", "Copy link": "लिंक कॉपी करें", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "भेजने के लिए राशि डालें।", "Enter an amount.": "राशि डालें।", "Enter at least 10 characters": "कम से कम 10 अक्षर डालें", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "जो भी भुगतान आप करना चाहते हैं, हर एक दर्ज करें: पता और RAND में राशि। एक ही पते पर दो बार भुगतान कुल राशि के साथ एक बार दर्ज होता है। जो आउटपुट इनमें से किसी से मेल नहीं खाता, उस पर हस्ताक्षर नहीं हो सकते।", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "हर प्राप्तकर्ता का पता दर्ज करें: जो आउटपुट दर्ज किए पते से मेल नहीं खाता, या जिसका पता खाली है, उस पर हस्ताक्षर नहीं हो सकते।", + "Enter the address of every payment.": "हर भुगतान का पता दर्ज करें।", "Enter the address this should be released to.": "वह पता डालें जिस पर इसे रिलीज़ किया जाना है।", "Enter the address you are sending to.": "वह पता डालें जिस पर आप भेज रहे हैं।", "Enter the amount as a number, for example 1.25.": "राशि संख्या में डालें, जैसे 1.25।", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "हर भुगतान की राशि दर्ज करें: यही भुगतान को बाँटने या बढ़ाए जाने से रोकती है।", "Enter the first 4 characters of your recovery key.": "अपनी रिकवरी की के पहले 4 अक्षर डालें।", "Enter the relayer fee as a number, for example 1.25.": "रिलेयर शुल्क संख्या में डालें, जैसे 1.25।", "Enter this wallet's password — the pairing is sealed under it.": "इस वॉलेट का पासवर्ड डालें — पेयरिंग इसी से सील है।", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "अपनी रिकवरी की डालें।", "Epoch": "एपॉक", "Exactly those four letters, in capitals.": "ठीक वही चार अक्षर, बड़े अक्षरों (कैपिटल) में।", + "expected recipient {address}: {reason}": "अपेक्षित प्राप्तकर्ता {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients पतों की सूची होनी चाहिए", "Explore": "खोजें", "Export spend key": "स्पेंड की एक्सपोर्ट करें", "Exporting the spend key is behind your password.": "स्पेंड की एक्सपोर्ट करने के लिए आपका पासवर्ड चाहिए।", @@ -288,6 +317,7 @@ export default { "Fee": "शुल्क", "fee must be at least {fee} RAND (the bridge burn floor)": "शुल्क कम से कम {fee} RAND होना चाहिए (ब्रिज बर्न की न्यूनतम सीमा)", "fee must be at least {fee} RAND (the bundle floor)": "शुल्क कम से कम {fee} RAND होना चाहिए (बंडल की न्यूनतम सीमा)", + "Fee: {fee} RAND · expires at height {height}": "शुल्क: {fee} RAND · ऊँचाई {height} पर समाप्त", "Filter activity by asset": "एसेट के अनुसार गतिविधि फ़िल्टर करें", "Fingerprint": "फ़िंगरप्रिंट", "fingerprint {fp}": "फ़िंगरप्रिंट {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "मैं समझता/समझती हूँ कि इस की वाला कोई भी मेरे फ़ंड खर्च कर सकता है", "Import wallet": "वॉलेट इंपोर्ट करें", "Incorrect password.": "गलत पासवर्ड।", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "इनपुट में {held} है, लेकिन राशि + शुल्क {need} है", "inputs hold {held}, but fee + RAND deposit is {need}": "इनपुट में {held} है, लेकिन शुल्क + RAND जमा {need} है", "insufficient balance: have {have}, need {need}": "बैलेंस कम है: है {have}, चाहिए {need}", + "Intended payments": "इच्छित भुगतान", "is on chain {chain}, not {expected}": "चेन {chain} पर है, {expected} पर नहीं", "It cannot see your balance or your activity, and it cannot move anything.": "यह आपका बैलेंस या गतिविधि नहीं देख सकता, और कुछ भी नहीं भेज सकता।", "It cannot spend.": "यह खर्च नहीं कर सकती।", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "की फ़ाइल में spend_key नहीं है", "key file must be version 2": "की फ़ाइल संस्करण 2 की होनी चाहिए", "Key hidden — enter your password to view it again.": "की छिपा दी गई — इसे फिर से देखने के लिए अपना पासवर्ड डालें।", + "Label": "लेबल", "Language": "भाषा", "Leave empty to let the payer choose.": "भुगतान करने वाले को चुनने देने के लिए खाली छोड़ें।", "Light": "लाइट", @@ -360,11 +393,15 @@ export default { "machines": "मशीनें", "malformed parameter {parameter}": "गलत पैरामीटर {parameter}", "Manage contacts": "संपर्क प्रबंधित करें", + "Manage multisig signers": "मल्टीसिग हस्ताक्षरकर्ता प्रबंधित करें", "Max": "अधिकतम", "Memo": "मेमो", "memo \"{memo}\"": "मेमो \"{memo}\"", "memo is {n} bytes, at most {max}": "मेमो {n} बाइट का है, अधिकतम {max}", "More is on the way": "और आ रहा है", + "Multisig share": "मल्टीसिग हस्ताक्षर अंश", + "Multisig signers": "मल्टीसिग हस्ताक्षरकर्ता", + "Multisig signing is not available in this app.": "इस ऐप में मल्टीसिग हस्ताक्षर उपलब्ध नहीं है।", "My own prover · {name}": "मेरा अपना प्रूवर · {name}", "Name": "नाम", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "जिन पतों पर आप अक्सर भुगतान करते हैं, उनके नाम, केवल इसी डिवाइस पर रखे जाते हैं। संपर्क को नाम से भेजें; सहेजते समय उसका फ़िंगरप्रिंट जाँचा जाता है।", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "अभी कोई संपर्क नहीं। सेटिंग्स से एक सहेजें, या पता पेस्ट करें।", "no endpoint could answer {method}": "कोई भी endpoint {method} का जवाब नहीं दे सका", "no endpoint could answer {method}; tried {endpoints}": "कोई भी endpoint {method} का जवाब नहीं दे सका; आज़माए गए: {endpoints}", + "no expected recipient given: the payee was NOT checked": "कोई अपेक्षित प्राप्तकर्ता नहीं दिया गया: प्राप्तकर्ता की जाँच नहीं हुई (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url} पर किसी नोड ने जवाब नहीं दिया, इसलिए समाप्ति की जाँच नहीं हो सकती और इस प्रस्ताव पर हस्ताक्षर नहीं किए गए। नोड उपलब्ध होने पर फिर कोशिश करें।", "No proof is pending.": "कोई प्रूफ़ लंबित नहीं है।", "No prover": "कोई प्रूवर नहीं", "No prover is paired.": "कोई प्रूवर पेयर नहीं है।", "No RAND to send yet": "भेजने के लिए अभी कोई RAND नहीं", "No registry assets": "रजिस्ट्री में कोई एसेट नहीं", "no reply": "कोई जवाब नहीं", + "No signer profiles yet": "अभी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं", + "No such signer profile.": "ऐसी कोई हस्ताक्षरकर्ता प्रोफ़ाइल नहीं है।", "No transaction or block matches that.": "इससे कोई लेन-देन या ब्लॉक मेल नहीं खाता।", "no wallet on this device": "इस डिवाइस पर कोई वॉलेट नहीं", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "बिल्ट-इन RandProtocol प्रूवर लिंक में से कोई भी वह की नहीं बताता जिसे यह वॉलेट उसके लिए पिन करता है; इन्हें इस्तेमाल नहीं किया जा रहा।", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "एक प्रूफ़ इस डिवाइस पर चलता है — लगभग दो मिनट। आप दूसरी स्क्रीन देख सकते हैं; वॉलेट बंद करने पर यह रुक जाता है।", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "चेन {chain} पर उस कॉइन में केवल {locked} लॉक है; कोई दूसरा बैकिंग या छोटी राशि चुनें", "Open durian.market": "durian.market खोलें", + "Open file": "फ़ाइल खोलें", "Open in explorer": "एक्सप्लोरर में खोलें", "Open in randscan": "randscan में खोलें", "Open in side panel": "साइड पैनल में खोलें", + "Open in the side panel to choose files": "फ़ाइलें चुनने के लिए साइड पैनल में खोलें", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "पासवर्ड टाइप करने की जगह {label} से Rand Wallet खोलें। आपका पासवर्ड अब भी काम करता है, और पहुँच वापस पाने का तरीका बना रहता है।", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "पासवर्ड टाइप करने की जगह {label} से वॉलेट खोलें। आपका पासवर्ड अब भी काम करता है, और पहुँच वापस पाने का तरीका बना रहता है।", "Open this window from a site that asked to connect.": "यह विंडो उस साइट से खोलें जिसने कनेक्ट करने को कहा था।", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "या RandProtocol प्रूवर इस्तेमाल करें — वैलिडेटरों द्वारा चलाई जाने वाली {n} मशीनें, हर एक अपनी की के साथ।", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "या RandProtocol प्रूवर इस्तेमाल करें — वैलिडेटरों द्वारा चलाई जाने वाली मशीनें, हर एक अपनी की के साथ।", "Outcome unknown": "परिणाम अज्ञात", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "आउटपुट {slot} इस खाते के रूप में नहीं खुलता: जो मान दिखता नहीं उस पर हस्ताक्षर से इनकार", "own={value}, expected 1 or absent": "own={value}, अपेक्षित 1 या अनुपस्थित", "Paid": "भुगतान हुआ", "Pair your own prover": "अपना प्रूवर पेयर करें", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "अपनी रिकवरी की पेस्ट करें", "path is not {n} levels": "पाथ {n} स्तरों का नहीं है", "Pay me on Rand": "मुझे Rand पर भुगतान करें", + "Payee address": "प्राप्तकर्ता का पता", "Payment link": "भुगतान लिंक", "Payment link · fingerprint {fingerprint}": "भुगतान लिंक · फ़िंगरप्रिंट {fingerprint}", "Payment link copied": "भुगतान लिंक कॉपी हो गया", "Payment links cannot be read in this app; paste the address instead.": "इस ऐप में भुगतान लिंक पढ़े नहीं जा सकते; इसकी जगह पता पेस्ट करें।", "Payment links cannot be read in this app.": "इस ऐप में भुगतान लिंक पढ़े नहीं जा सकते।", + "Payments changed: check them again before signing.": "भुगतान बदल गए: हस्ताक्षर से पहले उन्हें फिर जाँचें।", "Peers": "पीयर", "Pending": "लंबित", "Permission to reach that host was not granted, so the node was left as it was.": "उस होस्ट तक पहुँचने की अनुमति नहीं मिली, इसलिए नोड पहले जैसा ही छोड़ा गया।", "Permission to reach that host was not granted.": "उस होस्ट तक पहुँचने की अनुमति नहीं मिली।", "Permission to reach that prover was not granted, so nothing was saved.": "उस प्रूवर तक पहुँचने की अनुमति नहीं मिली, इसलिए कुछ भी सहेजा नहीं गया।", "Point the camera at a Rand QR code.": "कैमरे को Rand QR कोड की ओर करें।", + "Policy file": "पॉलिसी फ़ाइल", "Pool fee": "पूल शुल्क", "Price impact": "कीमत पर असर", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "कीमत इस चेन पर durian.market पूल तय करते हैं ({pct}% पूल शुल्क)। स्वैप एक लेन-देन है जिसका प्रूफ़ यह वॉलेट खुद बनाता और भेजता है।", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "प्रूफ़ केवल इसी डिवाइस पर बनते हैं। जहाँ यह प्रूफ़ नहीं बना सकता, वहाँ भेजना तब तक रुका रहता है जब तक आप कोई प्रूवर पेयर न करें या फिर से RandProtocol प्रूवर इस्तेमाल न करें।", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "जो प्रूफ़ यह डिवाइस नहीं बना सकता, वे {name} को जाते हैं। इसका फ़िंगरप्रिंट {fingerprint} है — जाँचें कि प्रूवर भी यही दिखाता है।", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "जो प्रूफ़ यह डिवाइस नहीं बना सकता, वे इनमें से किसी एक को जाते हैं; जो भी भेजने का प्रूफ़ बनाता है, वह उस वॉलेट की व्यूइंग की देखता है।", + "Proposal (proposal.json)": "प्रस्ताव (proposal.json)", "Prove and send": "प्रूफ़ बनाएँ और भेजें", "Prove for my other devices": "मेरे दूसरे डिवाइस के लिए प्रूफ़ बनाएँ", "Prover": "प्रूवर", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "रिलेयर शुल्क (वैकल्पिक)", "Remove": "हटाएँ", "Remove {name}": "{name} हटाएँ", + "Remove this signer": "इस हस्ताक्षरकर्ता को हटाएँ", "Request RAND": "RAND माँगें", "Request RAND from the faucet to try sending, receiving and proving on this network.": "इस नेटवर्क पर भेजना, प्राप्त करना और प्रूफ़ बनाना आज़माने के लिए फ़ॉसेट से RAND माँगें।", "Rescan": "फिर से स्कैन करें", @@ -574,8 +623,12 @@ export default { "Running on": "चल रहा है", "Save": "सहेजें", "Save an address you pay often, and send to it by name.": "जिस पते पर आप अक्सर भुगतान करते हैं, उसे सहेजें, और नाम से भेजें।", + "Save as file": "फ़ाइल के रूप में सहेजें", + "Save signer": "हस्ताक्षरकर्ता सहेजें", "Save your recovery key": "अपनी रिकवरी की सहेजें", "Saved": "सहेजा गया", + "Saved to {path}": "{path} में सहेजा गया", + "Saved.": "सहेजा गया।", "Scan": "स्कैन करें", "Scan a payment link": "भुगतान लिंक स्कैन करें", "Scan a QR code": "QR कोड स्कैन करें", @@ -596,6 +649,7 @@ export default { "Sent.": "भेजा गया।", "Set on this device when this wallet was created.": "यह वॉलेट बनाते समय इसी डिवाइस पर सेट किया गया।", "Settings": "सेटिंग्स", + "Share": "हस्ताक्षर अंश", "Share payment link": "भुगतान लिंक साझा करें", "shielded address decodes to {n} bytes, expected {expected}": "शील्डेड पता डिकोड होकर {n} बाइट का बनता है, अपेक्षित {expected}", "shielded address is not base58": "शील्डेड पता base58 नहीं है", @@ -607,6 +661,12 @@ export default { "Show the full address": "पूरा पता दिखाएँ", "Show viewing key": "व्यूइंग की दिखाएँ", "Show viewing key for 10 seconds": "व्यूइंग की 10 सेकंड के लिए दिखाएँ", + "Sign": "हस्ताक्षर करें", + "Sign a proposal": "प्रस्ताव पर हस्ताक्षर करें", + "signer {index} is not in this policy (it has {count} signers)": "हस्ताक्षरकर्ता {index} इस पॉलिसी में नहीं है (इसमें {count} हस्ताक्षरकर्ता हैं)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "हस्ताक्षरकर्ता {index} का हस्ताक्षर अंश मान्य है। {threshold} में से {have} हाथ में हैं: हस्ताक्षरकर्ता {list}।", + "signer {index}'s signature does not verify over this proposal": "हस्ताक्षरकर्ता {index} का हस्ताक्षर इस प्रस्ताव के लिए मान्य नहीं है", + "Signer key file, or its 64-hex seed": "हस्ताक्षरकर्ता की फ़ाइल, या उसका 64-हेक्स सीड", "So the address above is one you have actually read.": "ताकि ऊपर वाला पता वही हो जिसे आपने सच में पढ़ा है।", "Something went wrong": "कुछ गलत हो गया", "Something went wrong.": "कुछ गलत हो गया।", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "यह कॉपी नहीं हो सका।", "That could not be saved.": "यह सहेजा नहीं जा सका।", "That does not match — reveal the key again and check.": "यह मेल नहीं खाता — की फिर से दिखाएँ और जाँचें।", + "That file is larger than a proposal, a policy or a key file can be.": "वह फ़ाइल किसी प्रस्ताव, पॉलिसी या की फ़ाइल के संभव आकार से बड़ी है।", "That is more than your balance of {balance} {symbol}.": "यह आपके {balance} {symbol} बैलेंस से ज़्यादा है।", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "यह न 64 hex अक्षर (हैश) है, न ब्लॉक ऊँचाई (केवल अंक)।", "That is not a pairing link.": "यह पेयरिंग लिंक नहीं है।", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "शुल्क तय नहीं हो सका।", "the genesis hash is not hex of a plausible length": "genesis हैश उचित लंबाई का hex नहीं है", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow ठीक तभी `none` होता है जब बंडल कोई टोकन बर्न नहीं करता (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "की फ़ाइल का पता वह नहीं है जो उसका सीड व्युत्पन्न करता है", + "the key file's public_key is not the one its seed derives": "की फ़ाइल की public_key वह नहीं है जो उसका सीड व्युत्पन्न करता है", "The key you exported when this wallet was created.": "वह की जो आपने यह वॉलेट बनाते समय एक्सपोर्ट की थी।", "The language could not be saved.": "भाषा सहेजी नहीं जा सकी।", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "लिंक {linked} {symbol} माँगता है; आपने {typed} {symbol} टाइप किया।", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "वह पासवर्ड जो इस डिवाइस पर इस वॉलेट को अनलॉक करता है।", "The payer decides": "भुगतान करने वाला तय करेगा", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "लंबित प्रूफ़ हटा दिया गया (वॉलेट लॉक हो गया, या कहीं और रद्द किया गया), इसलिए कुछ नहीं भेजा गया। फिर से भेजें।", + "the policy does not parse: {reason}": "पॉलिसी पार्स नहीं होती: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "इस पेज के पढ़ने के बाद पूल बदल गया। कुछ नहीं भेजा गया; साइट फिर से कोट दे सकती है।", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "प्रूफ़ बनते समय पूल बदल गया। कुछ नहीं भेजा गया; साइट फिर से कोट दे सकती है।", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "पूल में इसका भुगतान करने के लिए पर्याप्त राशि नहीं है। कुछ नहीं भेजा गया; साइट फिर से कोट दे सकती है।", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "प्रूफ़ सत्यापित नहीं होता: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "प्रूफ़ ने ऐसा digest प्रकाशित किया जो इस वॉलेट ने नहीं बनाया ({expected} की जगह {published}); इसे इस्तेमाल नहीं किया जा रहा", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "प्रूफ़ इस डिवाइस पर बनता है। आप दूसरी स्क्रीन देख सकते हैं — यह चलता रहता है — लेकिन वॉलेट बंद करने पर यह रुक जाता है।", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "प्रस्ताव की जाँच ने अधूरा उत्तर लौटाया, इसलिए इस प्रस्ताव पर हस्ताक्षर नहीं किए गए।", + "the proposal does not parse: {reason}": "प्रस्ताव पार्स नहीं होता: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "प्रस्ताव {expires} पर समाप्त होता है, जो उसके बंडल की सीमा ({from}, {to}] से बाहर है", + "the proposal's auth_commit is not this policy's account": "प्रस्ताव का auth_commit इस पॉलिसी का खाता नहीं है", + "the proposal's expected digest is not its bundle's": "प्रस्ताव का अपेक्षित डाइजेस्ट उसके बंडल का नहीं है", + "the proposal's signing_message is not its binding's": "प्रस्ताव का signing_message उसकी बाइंडिंग का नहीं है", "the prover": "प्रूवर", "the prover accepted the job but named no job id": "प्रूवर ने काम स्वीकार किया लेकिन कोई job id नहीं बताई", "The prover answered with an unknown state ({state}).": "प्रूवर ने अज्ञात स्थिति ({state}) के साथ जवाब दिया।", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "Rand Wallet में अनुरोध स्वीकृत नहीं हुआ।", "The rescan could not be started.": "फिर से स्कैन शुरू नहीं हो सका।", "The rescan did not finish": "फिर से स्कैन पूरा नहीं हुआ", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "सहेजी गई हस्ताक्षरकर्ता प्रोफ़ाइलें खोली नहीं जा सकीं; कुछ नहीं बदला। इस डिवाइस का स्टोरेज बहाल करें, या वॉलेट मिटाकर उसकी की से बहाल करें", "The scan was cancelled.": "स्कैन रद्द हो गया।", "The side panel could not be opened.": "साइड पैनल खुल नहीं सका।", "The site sent an invoke this wallet cannot read: {reason}.": "साइट ने ऐसा invoke भेजा जिसे यह वॉलेट नहीं पढ़ सकता: {reason}।", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "स्वैप नहीं हो सका।", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "स्वैप चलता रहता है। भेजा गया दिखने से पहले वॉलेट बंद करने पर यह रुक जाता है, और कुछ नहीं खोता।", "The theme could not be saved.": "थीम सहेजी नहीं जा सकी।", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "लेन-देन में एक एक्शन है; निजी मल्टीसिग प्रस्ताव केवल अपने बंडल से किया गया भुगतान होता है", + "the transaction carries no bundle": "लेन-देन में कोई बंडल नहीं है", + "the transaction does not hash to the proposal's binding": "लेन-देन का हैश प्रस्ताव की बाइंडिंग का नहीं है", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "लेन-देन चेन {tx} के लिए है, प्रस्ताव चेन {proposal} बताता है", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "लेन-देन की यह एक भुगतान — इसकी राशि और इसका प्राप्तकर्ता — उसे बताती है जिसे आप यह देते हैं। यह कुछ भी खर्च नहीं कर सकती।", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "हो सकता है लेन-देन पहले ही चेन पर हो। फिर से निकालने से पहले गतिविधि देखें — दो बार निकालने पर दो बार बर्न होगा।", "The transfer could not be proved.": "ट्रांसफ़र का प्रूफ़ नहीं बन सका।", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "वॉलेट को बताया गया: {message}", "The withdrawal could not be proved.": "निकासी का प्रूफ़ नहीं बन सका।", "The withdrawal was submitted, but the node did not return a usable hash.": "निकासी सबमिट हुई, लेकिन नोड ने इस्तेमाल योग्य हैश नहीं लौटाया।", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "विटनेस का मान जाँचा नहीं जा सकता: उसका nk या सॉल्ट इस प्रस्ताव का नहीं है", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "विटनेस का मान आउटपुट से मेल नहीं खाता: एसेट {asset} में {held} इनपुट और शुल्क व बर्न सहित {spent} आउटपुट है", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "विटनेस का मान लेन-देन से मेल नहीं खाता: इनपुट {input} बंडल के नलिफ़ायर तक नहीं पहुँचता", "Theme": "थीम", "There is no pool for that pair.": "उस जोड़ी के लिए कोई पूल नहीं है।", "There is no program {program}… on this chain. Nothing was sent.": "इस चेन पर कोई प्रोग्राम {program}… नहीं है। कुछ नहीं भेजा गया।", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "यह डिवाइस, या जहाँ यह न कर सके · {name} प्रूवर", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "इससे पुरानी चेन से पढ़े गए नोट और इतिहास भुला दिए जाते हैं और इस नोड को शुरू से पढ़ा जाता है। आपकी की और आपका पासवर्ड नहीं बदलते।", "This key can spend your funds": "यह की आपके फ़ंड खर्च कर सकती है", + "this key is not one of the policy's signers": "यह की पॉलिसी के हस्ताक्षरकर्ताओं में से नहीं है", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "यह की ही आपका वॉलेट है। जिसके पास यह है, वह आपके फ़ंड खर्च कर सकता है, और इसके बिना डिवाइस खोने का मतलब फ़ंड खोना है। आप इसे अपने पासवर्ड से सेटिंग्स में फिर से देख सकते हैं।", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "यह शील्डेड पूल से बाहर जाता है। गंतव्य पता और राशि दूसरी चेन पर सार्वजनिक हो जाते हैं।", "This link is too long for a QR code — copy it instead.": "यह लिंक QR कोड के लिए बहुत लंबा है — इसकी जगह इसे कॉपी करें।", @@ -868,6 +947,10 @@ export default { "This note was not found.": "यह नोट नहीं मिला।", "This passkey does not support the PRF extension.": "यह पासकी PRF एक्सटेंशन सपोर्ट नहीं करती।", "This proof was already submitted from another window. Check Activity before sending again.": "यह प्रूफ़ पहले ही किसी दूसरी विंडो से सबमिट हो चुका है। फिर से भेजने से पहले गतिविधि देखें।", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "यह प्रस्ताव ऊँचाई {expires} पर समाप्त होता है और चेन {head} पर है: हस्ताक्षर के लिए बहुत देर हो चुकी। नया प्रस्ताव माँगें।", + "This proposal is bound to another genesis than the one this wallet's node serves.": "यह प्रस्ताव उस जेनेसिस से अलग जेनेसिस से बँधा है जिसे इस वॉलेट का नोड परोसता है।", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "यह प्रस्ताव किसी और पॉलिसी का है: इसमें {theirs} है, जबकि यह पॉलिसी {ours} है", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "यह प्रस्ताव चेन {theirs} के लिए है; यह वॉलेट चेन {ours} पर है।", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "यह प्रूवर हर प्रूफ़ के लिए {fee} RAND लेता है, और वॉलेट का यह संस्करण प्रूवर का शुल्क नहीं चुकाता: ऐसा प्रूवर पेयर करें जो कोई शुल्क न ले, या इसी डिवाइस पर प्रूफ़ बनाएँ", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "यह प्रूवर शुल्क लेता है, जिसे वॉलेट का यह संस्करण नहीं चुकाता। ऐसा प्रूवर पेयर करें जो कोई शुल्क न ले, या डेस्कटॉप ऐप से भेजें।", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "यह प्रूवर इस तरह का काम स्वीकार नहीं करता ({reason})। सेटिंग्स में कोई दूसरा प्रूवर पेयर करें, या डेस्कटॉप ऐप से भेजें।", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "इससे वॉलेट इस डिवाइस से हट जाता है। इसे वापस लाने के लिए आपको अपनी रिकवरी की चाहिए होगी।", "This screen could not be shown.": "यह स्क्रीन दिखाई नहीं जा सकी।", "This screen ships in a later task. Your wallet is unlocked and ready.": "यह स्क्रीन बाद के किसी चरण में आएगी। आपका वॉलेट अनलॉक है और तैयार है।", + "this signer is already saved as {label}": "यह हस्ताक्षरकर्ता पहले से {label} के रूप में सहेजा गया है", + "This signer profile has no key on this device: remove it and add it again.": "इस हस्ताक्षरकर्ता प्रोफ़ाइल की इस डिवाइस पर कोई की नहीं है: इसे हटाकर फिर जोड़ें।", "This tab is showing what it has so far, and will refresh when that finishes.": "यह टैब अब तक जो है वह दिखा रहा है, और वह पूरा होने पर रीफ़्रेश होगा।", "This takes a few minutes; keep this window open.": "इसमें कुछ मिनट लगते हैं; यह विंडो खुली रखें।", "This transaction was not found.": "यह लेन-देन नहीं मिला।", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "पुष्टि करने के लिए {word} टाइप करें", "Type it once more.": "इसे एक बार और टाइप करें।", "Type the last four characters of that address": "उस पते के आखिरी चार अक्षर टाइप करें", + "Type the last four characters of the account fingerprint": "खाते के फ़िंगरप्रिंट के अंतिम चार अक्षर लिखें", "unavailable in this app": "इस ऐप में उपलब्ध नहीं", "unknown": "अज्ञात", "unknown parameter {name}": "अज्ञात पैरामीटर {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "कमज़ोर", "Welcome back": "फिर से स्वागत है", "What the pool pays you arrives as a note your wallet finds on its next sync.": "पूल आपको जो भुगतान करता है, वह एक नोट के रूप में आता है जिसे आपका वॉलेट अगले सिंक पर ढूँढ लेता है।", + "What this proposal does, as checked against your policy": "यह प्रस्ताव क्या करता है, आपकी पॉलिसी के विरुद्ध जाँचा गया", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "जहाँ यह डिवाइस प्रूफ़ नहीं बना सकता, वहाँ प्रूवर पेयर करें। आपकी स्पेंड की हर हाल में यहीं रहती है; आपका खुद चलाया प्रूवर — डेस्कटॉप ऐप, या आपकी अपनी मशीन पर rand-prover — आपका इतिहास भी आप तक ही रखता है।", "Where to?": "कहाँ?", "Which address?": "कौन-सा पता?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "{n} मिनट तक प्रूफ़ बनाने के बाद भी आपका प्रूवर पूरा नहीं कर पाया है। बाद में फिर से शुरू करें, या रद्द करें।", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "कतार में {n} मिनट के बाद भी आपके प्रूवर ने यह प्रूफ़ शुरू नहीं किया है। बाद में फिर से शुरू करें, या रद्द करें।", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "आपके प्रूवर की पेयरिंग खुल नहीं सकी। वॉलेट को लॉक और अनलॉक करें, या सेटिंग्स में प्रूवर को फिर से पेयर करें।", + "Your share — send it to whoever proves the proposal": "आपका हस्ताक्षर अंश — इसे उसे भेजें जो प्रस्ताव प्रूव करता है", "Your wallet picks up what it pays you on its next sync.": "यह आपको जो भुगतान करता है, उसे आपका वॉलेट अगले सिंक पर ले लेता है।", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "आपके वॉलेट का इतिहास {chain} से पढ़ा गया था — कुछ नहीं बदला गया है, और जब तक दोनों मेल न खाएँ, यह वॉलेट फ़ंड नहीं भेजेगा या माँगेगा। सेटिंग्स में नोड बदलें, या नई चेन के लिए इस वॉलेट को फिर से स्कैन करें।", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "यह {name} प्रूवर का पता है। इन्हें पेयर करने की ज़रूरत नहीं: यह वॉलेट इन्हें डिफ़ॉल्ट रूप से इस्तेमाल करता है।", diff --git a/ui/locales/id.js b/ui/locales/id.js index 20b5d2c..5eb8942 100644 --- a/ui/locales/id.js +++ b/ui/locales/id.js @@ -1,6 +1,7 @@ // Bahasa Indonesia (id). See ui/locales/README.md. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Tersisa {blocks} blok untuk membuktikannya.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} peer", "{count} peers": "{count} peer", @@ -29,6 +30,7 @@ export default { "{label} is off": "{label} nonaktif", "{label} is on": "{label} aktif", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} tidak lagi membuka dompet ini (kata sandinya telah berubah). Buka dengan kata sandi, lalu aktifkan {label} lagi di Pengaturan.", + "{matched} of {total} payments matched.": "{matched} dari {total} pembayaran cocok.", "{member} answered with another key than the one this wallet pins": "{member} menjawab dengan kunci yang berbeda dari kunci yang disematkan dompet ini", "{member} did not answer": "{member} tidak menjawab", "{member} does not take this wallet's jobs": "{member} tidak menerima tugas dari dompet ini", @@ -49,6 +51,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} tidak dapat ditarik", "{symbol} has {decimals} decimal places — that is more.": "{symbol} memiliki {decimals} angka desimal — ini lebih dari itu.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} dilepas sebagai koin yang Anda pilih, di chain milik koin tersebut.", + "{threshold} of {count} · signer {index}": "{threshold} dari {count} · penanda tangan {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} dari {count} · kunci ini adalah penanda tangan {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} di chain {chain} memiliki {decimals} desimal: jumlah dan biaya relayer harus kelipatan {unit}", "{units} units of asset {asset}": "{units} unit aset {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} menjawab lebih dulu — chain {chain} · tinggi {height}.", @@ -81,7 +85,12 @@ export default { "A prover address must be https://.": "Alamat prover harus https://.", "A prover is making the proof": "Prover sedang membuat bukti", "A prover sees your history": "Prover dapat melihat riwayat Anda", + "a share is index:hex": "sebuah bagian tanda tangan berformat index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Dompet terlindung untuk jaringan Rand. Saldo dan riwayat tetap privat — setiap pengiriman dibuktikan di perangkat ini.", + "a signer key is a rand-node keygen file or 64 hex characters": "kunci penanda tangan adalah berkas keygen rand-node atau 64 karakter hex", + "a signer label is 1-64 characters": "label penanda tangan terdiri dari 1-64 karakter", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Profil penanda tangan disimpan di brankas terenkripsi dompet ini. Setiap tanda tangan meminta kata sandi Anda lagi.", + "a signer seed is 64 hex characters": "seed penanda tangan terdiri dari 64 karakter hex", "A site": "Sebuah situs", "a spend key is 64 hex characters, or a wallet.key.json": "kunci belanja terdiri dari 64 karakter hex, atau berkas wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Hash transaksi dicoba lebih dulu; jika tidak ditemukan, hash yang sama dicoba sebagai blok. Input yang seluruhnya angka mencari blok berdasarkan tinggi.", @@ -98,8 +107,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Penarikan ke chain yang tidak tercantum mungkin ditolak. Tidak ada yang dibelanjakan sampai penarikan dibuktikan.", "About": "Tentang", "about {left} left": "sekitar {left} lagi", + "Account fingerprint — compare it with every other signer before you save": "Sidik jari akun — bandingkan dengan setiap penanda tangan lain sebelum menyimpan", "Activity": "Aktivitas", "Add a contact": "Tambah kontak", + "Add a signer": "Tambah penanda tangan", + "Add payment": "Tambah pembayaran", + "Add the policy file and your signer key to sign for a private multisig account.": "Tambahkan berkas kebijakan dan kunci penanda tangan Anda untuk menandatangani akun multisig privat.", "Address": "Alamat", "Address copied": "Alamat disalin", "Address or payment link": "Alamat atau tautan pembayaran", @@ -108,14 +121,18 @@ export default { "almost done": "hampir selesai", "also tried {endpoints}": "juga mencoba {endpoints}", "Amount": "Jumlah", + "Amount in RAND": "Jumlah dalam RAND", "amount must be greater than zero": "jumlah harus lebih besar dari nol", "amount overflow": "jumlah melebihi batas", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Jumlah tidak dapat diperiksa untuk pembayaran non-RAND: kosongkan jumlahnya untuk pembayaran tersebut.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Penerima ini menerima RAND sekaligus token: pembayaran RAND dan token ke satu penerima memerlukan proposal terpisah.", "amounts overflow": "total jumlah melebihi batas", "an action is {n} characters, more than {max}": "sebuah aksi terdiri dari {n} karakter, lebih dari {max}", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Alamat (rand1…), tautan pembayaran (randpay:…), atau nama kontak yang tersimpan.", "An address here is hexadecimal — 0-9 and a-f only.": "Alamat di sini berupa heksadesimal — hanya 0-9 dan a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Alamat di chain ini berukuran 20 byte — 40 karakter hex. Yang ini {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Alamat di chain ini berukuran 32 byte — 64 karakter hex. Yang ini {length}.", + "An amount is a number of RAND, such as 1.5.": "Jumlah adalah bilangan RAND, misalnya 1.5.", "an unknown chain": "chain yang tidak dikenal", "another tab changed this wallet while it was syncing; try again": "tab lain mengubah dompet ini saat sinkronisasi; coba lagi", "Another tab is syncing": "Tab lain sedang sinkronisasi", @@ -182,11 +199,16 @@ export default { "Chains known": "Chain yang dikenal", "Change back to you": "Kembalian untuk Anda", "Change it": "Ubah", + "Check": "Periksa", + "Check a share": "Periksa bagian tanda tangan", "Check Activity": "Periksa Aktivitas", "Check Activity first": "Periksa Aktivitas dulu", "Check before saving": "Periksa sebelum menyimpan", "Check fingerprint": "Periksa sidik jari", + "Check payments": "Periksa pembayaran", + "Check proposal": "Periksa proposal", "Check settings": "Periksa pengaturan", + "Check share": "Periksa bagian tanda tangan", "Checking it against the chain before anything is proved.": "Memeriksanya terhadap chain sebelum ada yang dibuktikan.", "Checking the pool and your notes…": "Memeriksa pool dan catatan Anda…", "Checking the swap…": "Memeriksa penukaran…", @@ -213,6 +235,7 @@ export default { "Continue": "Lanjutkan", "Copied": "Disalin", "Copied as JSON": "Disalin sebagai JSON", + "Copy": "Salin", "Copy address": "Salin alamat", "Copy as JSON": "Salin sebagai JSON", "Copy link": "Salin tautan", @@ -261,9 +284,13 @@ export default { "Enter an amount to send.": "Masukkan jumlah yang akan dikirim.", "Enter an amount.": "Masukkan jumlah.", "Enter at least 10 characters": "Masukkan minimal 10 karakter", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Masukkan setiap pembayaran yang akan Anda lakukan: alamat dan jumlah dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan satu kali, dengan totalnya. Output yang tidak cocok dengan satu pun tidak dapat ditandatangani.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Masukkan alamat setiap penerima pembayaran: output yang tidak cocok dengan alamat yang Anda masukkan, atau yang alamatnya kosong, tidak dapat ditandatangani.", + "Enter the address of every payment.": "Masukkan alamat setiap pembayaran.", "Enter the address this should be released to.": "Masukkan alamat tujuan pelepasan ini.", "Enter the address you are sending to.": "Masukkan alamat yang Anda tuju.", "Enter the amount as a number, for example 1.25.": "Masukkan jumlah sebagai angka, misalnya 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Masukkan jumlah setiap pembayaran: inilah yang mencegah pemecahan atau pembayaran yang digelembungkan.", "Enter the first 4 characters of your recovery key.": "Masukkan 4 karakter pertama kunci pemulihan Anda.", "Enter the relayer fee as a number, for example 1.25.": "Masukkan biaya relayer sebagai angka, misalnya 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Masukkan kata sandi dompet ini — pemasangan disegel dengannya.", @@ -273,6 +300,8 @@ export default { "Enter your recovery key.": "Masukkan kunci pemulihan Anda.", "Epoch": "Epoch", "Exactly those four letters, in capitals.": "Tepat empat huruf itu, dalam huruf kapital.", + "expected recipient {address}: {reason}": "penerima yang diharapkan {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients harus berupa daftar alamat", "Explore": "Jelajahi", "Export spend key": "Ekspor kunci belanja", "Exporting the spend key is behind your password.": "Ekspor kunci belanja dilindungi kata sandi Anda.", @@ -281,6 +310,7 @@ export default { "Fee": "Biaya", "fee must be at least {fee} RAND (the bridge burn floor)": "biaya minimal {fee} RAND (batas bawah burn bridge)", "fee must be at least {fee} RAND (the bundle floor)": "biaya minimal {fee} RAND (batas bawah bundel)", + "Fee: {fee} RAND · expires at height {height}": "Biaya: {fee} RAND · kedaluwarsa pada ketinggian {height}", "Filter activity by asset": "Filter aktivitas berdasarkan aset", "Fingerprint": "Sidik jari", "fingerprint {fp}": "sidik jari {fp}", @@ -318,9 +348,11 @@ export default { "I understand anyone with this key can spend my funds": "Saya mengerti bahwa siapa pun yang memiliki kunci ini dapat membelanjakan dana saya", "Import wallet": "Impor dompet", "Incorrect password.": "Kata sandi salah.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "input berisi {held}, tetapi jumlah + biaya adalah {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "input berisi {held}, tetapi biaya + deposit RAND adalah {need}", "insufficient balance: have {have}, need {need}": "saldo tidak cukup: tersedia {have}, diperlukan {need}", + "Intended payments": "Pembayaran yang dimaksud", "is on chain {chain}, not {expected}": "berada di chain {chain}, bukan {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Prover ini tidak dapat melihat saldo atau aktivitas Anda, dan tidak dapat memindahkan apa pun.", "It cannot spend.": "Prover itu tidak dapat membelanjakan.", @@ -343,6 +375,7 @@ export default { "key file has no spend_key": "berkas kunci tidak memiliki spend_key", "key file must be version 2": "berkas kunci harus versi 2", "Key hidden — enter your password to view it again.": "Kunci disembunyikan — masukkan kata sandi Anda untuk melihatnya lagi.", + "Label": "Label", "Language": "Bahasa", "Leave empty to let the payer choose.": "Biarkan kosong agar pembayar yang memilih.", "Light": "Terang", @@ -353,11 +386,15 @@ export default { "machines": "mesin", "malformed parameter {parameter}": "parameter {parameter} tidak valid", "Manage contacts": "Kelola kontak", + "Manage multisig signers": "Kelola penanda tangan multisig", "Max": "Maks", "Memo": "Memo", "memo \"{memo}\"": "memo \"{memo}\"", "memo is {n} bytes, at most {max}": "memo berukuran {n} byte, maksimal {max}", "More is on the way": "Masih ada yang sedang diproses", + "Multisig share": "Bagian tanda tangan multisig", + "Multisig signers": "Penanda tangan multisig", + "Multisig signing is not available in this app.": "Penandatanganan multisig tidak tersedia di aplikasi ini.", "My own prover · {name}": "Prover saya sendiri · {name}", "Name": "Nama", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nama untuk alamat yang sering Anda bayar, hanya disimpan di perangkat ini. Kirim ke kontak berdasarkan nama; sidik jarinya diperiksa saat Anda menyimpannya.", @@ -381,12 +418,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Belum ada kontak. Simpan dari Pengaturan, atau tempel alamat.", "no endpoint could answer {method}": "tidak ada endpoint yang dapat menjawab {method}", "no endpoint could answer {method}; tried {endpoints}": "tidak ada endpoint yang dapat menjawab {method}; telah mencoba {endpoints}", + "no expected recipient given: the payee was NOT checked": "tidak ada penerima yang diharapkan: penerima pembayaran TIDAK diperiksa", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Tidak ada node yang menjawab di {url}, sehingga masa berlaku tidak dapat diperiksa dan proposal ini tidak ditandatangani. Coba lagi saat node dapat dijangkau.", "No proof is pending.": "Tidak ada bukti yang tertunda.", "No prover": "Tidak ada prover", "No prover is paired.": "Tidak ada prover yang dipasangkan.", "No RAND to send yet": "Belum ada RAND untuk dikirim", "No registry assets": "Tidak ada aset registri", "no reply": "tidak ada balasan", + "No signer profiles yet": "Belum ada profil penanda tangan", + "No such signer profile.": "Profil penanda tangan itu tidak ada.", "No transaction or block matches that.": "Tidak ada transaksi atau blok yang cocok.", "no wallet on this device": "tidak ada dompet di perangkat ini", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Tidak satu pun tautan prover RandProtocol bawaan menyebutkan kunci yang disematkan dompet ini; tautan itu tidak digunakan.", @@ -433,9 +474,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Satu bukti dibuat di perangkat ini — sekitar dua menit. Anda dapat membuka layar lain; menutup dompet akan menghentikannya.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "hanya {locked} yang terkunci dalam koin itu di chain {chain}; pilih jaminan lain atau jumlah yang lebih kecil", "Open durian.market": "Buka durian.market", + "Open file": "Buka berkas", "Open in explorer": "Buka di explorer", "Open in randscan": "Buka di randscan", "Open in side panel": "Buka di panel samping", + "Open in the side panel to choose files": "Buka di panel samping untuk memilih berkas", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Buka Rand Wallet dengan {label} alih-alih mengetik kata sandi. Kata sandi Anda tetap berlaku, dan tetap menjadi cara untuk memulihkan akses.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Buka dompet dengan {label} alih-alih mengetik kata sandi. Kata sandi Anda tetap berlaku, dan tetap menjadi cara untuk memulihkan akses.", "Open this window from a site that asked to connect.": "Buka jendela ini dari situs yang meminta untuk terhubung.", @@ -445,6 +488,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "Atau gunakan prover RandProtocol — {n} mesin yang dijalankan oleh validator, masing-masing dengan kuncinya sendiri.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Atau gunakan prover RandProtocol — mesin yang dijalankan oleh validator, masing-masing dengan kuncinya sendiri.", "Outcome unknown": "Hasil tidak diketahui", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "output {slot} tidak terbuka sebagai milik akun ini: menolak menandatangani nilai yang tidak dapat dilihatnya", "own={value}, expected 1 or absent": "own={value}, seharusnya 1 atau tidak ada", "Paid": "Dibayar", "Pair your own prover": "Pasangkan prover Anda sendiri", @@ -465,17 +509,20 @@ export default { "Paste your recovery key": "Tempel kunci pemulihan Anda", "path is not {n} levels": "path bukan {n} tingkat", "Pay me on Rand": "Bayar saya di Rand", + "Payee address": "Alamat penerima pembayaran", "Payment link": "Tautan pembayaran", "Payment link · fingerprint {fingerprint}": "Tautan pembayaran · sidik jari {fingerprint}", "Payment link copied": "Tautan pembayaran disalin", "Payment links cannot be read in this app; paste the address instead.": "Tautan pembayaran tidak dapat dibaca di aplikasi ini; tempel alamatnya saja.", "Payment links cannot be read in this app.": "Tautan pembayaran tidak dapat dibaca di aplikasi ini.", + "Payments changed: check them again before signing.": "Pembayaran berubah: periksa lagi sebelum menandatangani.", "Peers": "Peer", "Pending": "Tertunda", "Permission to reach that host was not granted, so the node was left as it was.": "Izin untuk menjangkau host itu tidak diberikan, jadi node dibiarkan seperti semula.", "Permission to reach that host was not granted.": "Izin untuk menjangkau host itu tidak diberikan.", "Permission to reach that prover was not granted, so nothing was saved.": "Izin untuk menjangkau prover itu tidak diberikan, jadi tidak ada yang disimpan.", "Point the camera at a Rand QR code.": "Arahkan kamera ke kode QR Rand.", + "Policy file": "Berkas kebijakan", "Pool fee": "Biaya pool", "Price impact": "Dampak harga", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Harga dari pool durian.market di chain ini (biaya pool {pct}%). Penukaran ini adalah transaksi yang dibuktikan dan dikirim sendiri oleh dompet ini.", @@ -485,6 +532,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Bukti hanya dibuat di perangkat ini. Jika perangkat ini tidak dapat membuatnya, pengiriman menunggu sampai Anda memasangkan prover atau menggunakan prover RandProtocol lagi.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Bukti yang tidak dapat dibuat perangkat ini dikirim ke {name}. Sidik jarinya {fingerprint} — pastikan prover menampilkan yang sama.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Bukti yang tidak dapat dibuat perangkat ini dikirim ke salah satu dari mereka; setiap prover yang membuktikan pengiriman melihat kunci tampilan dompet itu.", + "Proposal (proposal.json)": "Proposal (proposal.json)", "Prove and send": "Buktikan dan kirim", "Prove for my other devices": "Buat bukti untuk perangkat saya yang lain", "Prover": "Prover", @@ -533,6 +581,7 @@ export default { "Relayer fee (optional)": "Biaya relayer (opsional)", "Remove": "Hapus", "Remove {name}": "Hapus {name}", + "Remove this signer": "Hapus penanda tangan ini", "Request RAND": "Minta RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Minta RAND dari faucet untuk mencoba mengirim, menerima, dan membuktikan di jaringan ini.", "Rescan": "Pindai ulang", @@ -567,8 +616,12 @@ export default { "Running on": "Berjalan di", "Save": "Simpan", "Save an address you pay often, and send to it by name.": "Simpan alamat yang sering Anda bayar, dan kirim ke alamat itu berdasarkan nama.", + "Save as file": "Simpan sebagai berkas", + "Save signer": "Simpan penanda tangan", "Save your recovery key": "Simpan kunci pemulihan Anda", "Saved": "Disimpan", + "Saved to {path}": "Disimpan di {path}", + "Saved.": "Tersimpan.", "Scan": "Pindai", "Scan a payment link": "Pindai tautan pembayaran", "Scan a QR code": "Pindai kode QR", @@ -589,6 +642,7 @@ export default { "Sent.": "Terkirim.", "Set on this device when this wallet was created.": "Ditetapkan di perangkat ini saat dompet ini dibuat.", "Settings": "Pengaturan", + "Share": "Bagian tanda tangan", "Share payment link": "Bagikan tautan pembayaran", "shielded address decodes to {n} bytes, expected {expected}": "alamat terlindung didekode menjadi {n} byte, seharusnya {expected}", "shielded address is not base58": "alamat terlindung bukan base58", @@ -600,6 +654,12 @@ export default { "Show the full address": "Tampilkan alamat lengkap", "Show viewing key": "Tampilkan kunci tampilan", "Show viewing key for 10 seconds": "Tampilkan kunci tampilan selama 10 detik", + "Sign": "Tandatangani", + "Sign a proposal": "Tandatangani proposal", + "signer {index} is not in this policy (it has {count} signers)": "penanda tangan {index} tidak ada dalam kebijakan ini (kebijakan ini memiliki {count} penanda tangan)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Bagian tanda tangan penanda tangan {index} valid. {have} dari {threshold} sudah terkumpul: penanda tangan {list}.", + "signer {index}'s signature does not verify over this proposal": "tanda tangan penanda tangan {index} tidak valid untuk proposal ini", + "Signer key file, or its 64-hex seed": "Berkas kunci penanda tangan, atau seed 64 hex-nya", "So the address above is one you have actually read.": "Agar alamat di atas benar-benar telah Anda baca.", "Something went wrong": "Terjadi kesalahan", "Something went wrong.": "Terjadi kesalahan.", @@ -632,6 +692,7 @@ export default { "That could not be copied.": "Itu tidak dapat disalin.", "That could not be saved.": "Itu tidak dapat disimpan.", "That does not match — reveal the key again and check.": "Tidak cocok — tampilkan kunci lagi dan periksa.", + "That file is larger than a proposal, a policy or a key file can be.": "Berkas itu lebih besar dari yang mungkin untuk sebuah proposal, kebijakan, atau berkas kunci.", "That is more than your balance of {balance} {symbol}.": "Itu melebihi saldo Anda sebesar {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Itu bukan 64 karakter hex (hash) maupun tinggi blok (hanya angka).", "That is not a pairing link.": "Itu bukan tautan pemasangan.", @@ -667,6 +728,8 @@ export default { "The fee could not be worked out.": "Biaya tidak dapat dihitung.", "the genesis hash is not hex of a plausible length": "hash genesis bukan hex dengan panjang yang wajar", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow bernilai `none` tepat ketika bundel tidak mem-burn token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "alamat berkas kunci bukan yang diturunkan seed-nya", + "the key file's public_key is not the one its seed derives": "public_key berkas kunci bukan yang diturunkan seed-nya", "The key you exported when this wallet was created.": "Kunci yang Anda ekspor saat dompet ini dibuat.", "The language could not be saved.": "Bahasa tidak dapat disimpan.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Tautan meminta {linked} {symbol}; Anda mengetik {typed} {symbol}.", @@ -707,6 +770,7 @@ export default { "The password that unlocks this wallet on this device.": "Kata sandi yang membuka dompet ini di perangkat ini.", "The payer decides": "Pembayar yang menentukan", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "Bukti yang tertunda telah dihapus (dompet terkunci, atau dibatalkan di tempat lain), jadi tidak ada yang dikirim. Kirim lagi.", + "the policy does not parse: {reason}": "kebijakan tidak dapat diurai: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Pool berubah sejak halaman ini membacanya. Tidak ada yang dikirim; situs dapat memberikan penawaran lagi.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Pool berubah saat ini sedang dibuktikan. Tidak ada yang dikirim; situs dapat memberikan penawaran lagi.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Pool tidak memiliki cukup dana untuk membayar ini. Tidak ada yang dikirim; situs dapat memberikan penawaran lagi.", @@ -717,6 +781,12 @@ export default { "the proof does not verify: {reason}": "bukti tidak terverifikasi: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "bukti memublikasikan digest yang tidak disusun dompet ini ({published} untuk {expected}); tidak digunakan", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "Bukti dibuat di perangkat ini. Anda dapat membuka layar lain — prosesnya tetap berjalan — tetapi menutup dompet akan menghentikannya.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Pemeriksaan proposal mengembalikan jawaban yang tidak lengkap, sehingga proposal ini tidak ditandatangani.", + "the proposal does not parse: {reason}": "proposal tidak dapat diurai: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "proposal kedaluwarsa pada {expires}, di luar jendela bundle-nya ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "auth_commit proposal bukan akun kebijakan ini", + "the proposal's expected digest is not its bundle's": "digest yang diharapkan proposal bukan milik bundle-nya", + "the proposal's signing_message is not its binding's": "signing_message proposal bukan milik binding-nya", "the prover": "prover", "the prover accepted the job but named no job id": "prover menerima tugas tetapi tidak menyebutkan ID tugas", "The prover answered with an unknown state ({state}).": "Prover menjawab dengan status yang tidak dikenal ({state}).", @@ -778,6 +848,7 @@ export default { "The request was not approved in Rand Wallet.": "Permintaan tidak disetujui di Rand Wallet.", "The rescan could not be started.": "Pemindaian ulang tidak dapat dimulai.", "The rescan did not finish": "Pemindaian ulang tidak selesai", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "profil penanda tangan yang tersimpan tidak dapat dibuka; tidak ada yang diubah. Pulihkan penyimpanan perangkat ini, atau hapus dompet dan pulihkan dari kuncinya", "The scan was cancelled.": "Pemindaian dibatalkan.", "The side panel could not be opened.": "Panel samping tidak dapat dibuka.", "The site sent an invoke this wallet cannot read: {reason}.": "Situs mengirim invoke yang tidak dapat dibaca dompet ini: {reason}.", @@ -788,6 +859,10 @@ export default { "The swap did not go through.": "Penukaran tidak berhasil.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Penukaran tetap berjalan. Menutup dompet sebelum tertulis terkirim akan menghentikannya, dan tidak ada yang hilang.", "The theme could not be saved.": "Tema tidak dapat disimpan.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transaksi membawa sebuah aksi; proposal multisig privat adalah pembayaran dari bundle-nya saja", + "the transaction carries no bundle": "transaksi tidak membawa bundle", + "the transaction does not hash to the proposal's binding": "hash transaksi bukan binding proposal", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "transaksi ini untuk chain {tx}, proposal menyebut chain {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "Kunci transaksi mengungkapkan satu pembayaran ini — jumlah dan penerimanya — kepada siapa pun yang Anda beri. Kunci ini tidak dapat membelanjakan apa pun.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Transaksi mungkin sudah ada di chain. Periksa Aktivitas sebelum menarik lagi — menarik dua kali berarti mem-burn dua kali.", "The transfer could not be proved.": "Transfer tidak dapat dibuktikan.", @@ -805,6 +880,9 @@ export default { "The wallet was told: {message}": "Dompet menerima pesan: {message}", "The withdrawal could not be proved.": "Penarikan tidak dapat dibuktikan.", "The withdrawal was submitted, but the node did not return a usable hash.": "Penarikan telah dikirimkan, tetapi node tidak mengembalikan hash yang dapat digunakan.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "nilai witness tidak dapat diperiksa: nk atau salt-nya bukan milik proposal ini", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "nilai witness tidak cocok dengan output: aset {asset} memiliki {held} masuk dan {spent} keluar beserta biaya dan pembakaran", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "nilai witness tidak cocok dengan transaksi: input {input} tidak menghasilkan nullifier bundle", "Theme": "Tema", "There is no pool for that pair.": "Tidak ada pool untuk pasangan itu.", "There is no program {program}… on this chain. Nothing was sent.": "Tidak ada program {program}… di chain ini. Tidak ada yang dikirim.", @@ -846,6 +924,7 @@ export default { "This device, or where it cannot · {name} provers": "Perangkat ini, atau jika tidak bisa · prover {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Ini melupakan catatan dan riwayat yang dibaca dari chain lama dan membaca node ini dari awal. Kunci dan kata sandi Anda tidak tersentuh.", "This key can spend your funds": "Kunci ini dapat membelanjakan dana Anda", + "this key is not one of the policy's signers": "kunci ini bukan salah satu penanda tangan kebijakan", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Kunci ini adalah dompet Anda. Siapa pun yang memilikinya dapat membelanjakan dana Anda, dan tanpanya, perangkat yang hilang berarti dana yang hilang. Anda dapat melihatnya lagi di Pengaturan dengan kata sandi Anda.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Ini keluar dari pool terlindung. Alamat tujuan dan jumlahnya menjadi publik di chain lain.", "This link is too long for a QR code — copy it instead.": "Tautan ini terlalu panjang untuk kode QR — salin saja.", @@ -861,6 +940,10 @@ export default { "This note was not found.": "Catatan ini tidak ditemukan.", "This passkey does not support the PRF extension.": "Passkey ini tidak mendukung ekstensi PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Bukti ini sudah dikirimkan dari jendela lain. Periksa Aktivitas sebelum mengirim lagi.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Proposal ini kedaluwarsa pada ketinggian {expires} dan chain berada di {head}: terlambat untuk menandatangani. Minta proposal baru.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Proposal ini terikat pada genesis yang berbeda dari yang dilayani node dompet ini.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "proposal ini untuk kebijakan lain: menyebut {theirs}, kebijakan ini adalah {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Proposal ini untuk chain {theirs}; dompet ini berada di chain {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "prover ini mengenakan biaya {fee} RAND per bukti, dan versi dompet ini tidak membayar biaya prover: pasangkan prover yang tidak memungut biaya, atau buktikan di perangkat ini", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Prover ini mengenakan biaya, yang tidak dibayar oleh versi dompet ini. Pasangkan prover yang tidak memungut biaya, atau kirim dari aplikasi desktop.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Prover ini tidak menerima jenis tugas ini ({reason}). Pasangkan prover lain di Pengaturan, atau kirim dari aplikasi desktop.", @@ -872,6 +955,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Ini menghapus dompet dari perangkat ini. Anda akan memerlukan kunci pemulihan untuk memulihkannya.", "This screen could not be shown.": "Layar ini tidak dapat ditampilkan.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Layar ini hadir di tahap berikutnya. Dompet Anda sudah terbuka dan siap.", + "this signer is already saved as {label}": "penanda tangan ini sudah tersimpan sebagai {label}", + "This signer profile has no key on this device: remove it and add it again.": "Profil penanda tangan ini tidak memiliki kunci di perangkat ini: hapus lalu tambahkan lagi.", "This tab is showing what it has so far, and will refresh when that finishes.": "Tab ini menampilkan apa yang sudah ada, dan akan diperbarui setelah proses itu selesai.", "This takes a few minutes; keep this window open.": "Prosesnya beberapa menit; biarkan jendela ini terbuka.", "This transaction was not found.": "Transaksi ini tidak ditemukan.", @@ -918,6 +1003,7 @@ export default { "Type {word} to confirm": "Ketik {word} untuk konfirmasi", "Type it once more.": "Ketik sekali lagi.", "Type the last four characters of that address": "Ketik empat karakter terakhir alamat itu", + "Type the last four characters of the account fingerprint": "Ketik empat karakter terakhir sidik jari akun", "unavailable in this app": "tidak tersedia di aplikasi ini", "unknown": "tidak diketahui", "unknown parameter {name}": "parameter tidak dikenal {name}", @@ -954,6 +1040,7 @@ export default { "Weak": "Lemah", "Welcome back": "Selamat datang kembali", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Yang dibayarkan pool kepada Anda tiba sebagai catatan yang ditemukan dompet Anda pada sinkronisasi berikutnya.", + "What this proposal does, as checked against your policy": "Apa yang dilakukan proposal ini, diperiksa terhadap kebijakan Anda", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Jika perangkat ini tidak dapat membuat bukti, pasangkan prover. Kunci belanja Anda tetap di sini dalam kedua kasus; prover yang Anda jalankan sendiri — aplikasi desktop, atau rand-prover di mesin Anda sendiri — juga menjaga riwayat Anda tetap untuk Anda sendiri.", "Where to?": "Ke mana?", "Which address?": "Alamat mana?", @@ -1000,6 +1087,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "Prover Anda belum selesai setelah {n} menit pembuktian. Lanjutkan nanti, atau batalkan.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "Prover Anda belum memulai bukti ini setelah {n} menit dalam antrean. Lanjutkan nanti, atau batalkan.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Pemasangan prover Anda tidak dapat dibuka. Kunci lalu buka kembali dompet, atau pasangkan prover lagi di Pengaturan.", + "Your share — send it to whoever proves the proposal": "Bagian tanda tangan Anda — kirim ke yang membuktikan proposal", "Your wallet picks up what it pays you on its next sync.": "Dompet Anda mengambil apa yang dibayarkan kepada Anda pada sinkronisasi berikutnya.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "Riwayat dompet Anda dibaca dari {chain} — tidak ada yang diubah, dan dompet ini tidak akan mengirim atau meminta dana sampai keduanya sesuai. Ganti node di Pengaturan, atau pindai ulang dompet ini untuk chain baru.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Itu alamat prover {name}. Prover ini tidak perlu dipasangkan: dompet ini menggunakannya secara default.", diff --git a/ui/locales/it.js b/ui/locales/it.js index fc49acb..25f5333 100644 --- a/ui/locales/it.js +++ b/ui/locales/it.js @@ -8,6 +8,7 @@ // nullifier — nullifier, rescan — nuova scansione. Plural categories: one, many, other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Mancano {blocks} blocchi per dimostrarlo.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} peer", many: "{count} peer", other: "{count} peer" }, "{count} peers": { one: "{count} peer", many: "{count} peer", other: "{count} peer" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} è disattivato", "{label} is on": "{label} è attivo", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} non apre più questo portafoglio (la password è cambiata). Sblocca con la password, poi riattiva {label} nelle Impostazioni.", + "{matched} of {total} payments matched.": "{matched} pagamenti su {total} corrispondono.", "{member} answered with another key than the one this wallet pins": "{member} ha risposto con una chiave diversa da quella fissata da questo portafoglio", "{member} did not answer": "{member} non ha risposto", "{member} does not take this wallet's jobs": "{member} non accetta i lavori di questo portafoglio", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} non può essere prelevato", "{symbol} has {decimals} decimal places — that is more.": "{symbol} ha {decimals} cifre decimali — questo importo ne ha di più.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} viene rilasciato come la moneta che scegli, sulla catena di quella moneta.", + "{threshold} of {count} · signer {index}": "{threshold} di {count} · firmatario {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} di {count} · questa chiave è il firmatario {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} sulla catena {chain} ha {decimals} decimali: l'importo e la commissione del relayer devono essere multipli di {unit}", "{units} units of asset {asset}": "{units} unità dell'asset {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} ha risposto per primo — catena {chain} · altezza {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "L'indirizzo di un prover deve essere https://.", "A prover is making the proof": "Un prover sta generando la prova", "A prover sees your history": "Un prover vede la tua cronologia", + "a share is index:hex": "una quota di firma è index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Un portafoglio schermato per la rete Rand. Saldi e cronologia restano privati — ogni invio viene provato su questo dispositivo.", + "a signer key is a rand-node keygen file or 64 hex characters": "una chiave di firmatario è un file keygen di rand-node o 64 caratteri esadecimali", + "a signer label is 1-64 characters": "l'etichetta di un firmatario è di 1-64 caratteri", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Un profilo di firmatario è conservato nel vault cifrato di questo portafoglio. Ogni firma chiede di nuovo la tua password.", + "a signer seed is 64 hex characters": "un seed di firmatario è di 64 caratteri esadecimali", "A site": "Un sito", "a spend key is 64 hex characters, or a wallet.key.json": "una chiave di spesa è di 64 caratteri esadecimali, oppure un wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Si prova prima come hash di transazione; se non si trova nulla, lo stesso hash si prova come blocco. Solo cifre cerca un blocco per altezza.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Un prelievo verso una moneta non elencata può essere rifiutato. Non viene speso nulla finché non è provato.", "About": "Informazioni", "about {left} left": "circa {left} rimanenti", + "Account fingerprint — compare it with every other signer before you save": "Impronta dell'account — confrontala con quella di ogni altro firmatario prima di salvare", "Activity": "Attività", "Add a contact": "Aggiungi un contatto", + "Add a signer": "Aggiungi un firmatario", + "Add payment": "Aggiungi pagamento", + "Add the policy file and your signer key to sign for a private multisig account.": "Aggiungi il file della policy e la tua chiave di firmatario per firmare per un account multisig privato.", "Address": "Indirizzo", "Address copied": "Indirizzo copiato", "Address or payment link": "Indirizzo o link di pagamento", @@ -115,14 +128,18 @@ export default { "almost done": "quasi finito", "also tried {endpoints}": "provati anche {endpoints}", "Amount": "Importo", + "Amount in RAND": "Importo in RAND", "amount must be greater than zero": "l'importo deve essere maggiore di zero", "amount overflow": "overflow dell'importo", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Gli importi non si possono verificare per i pagamenti non in RAND: lascia vuoto l'importo per quelli.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Questo beneficiario riceve sia RAND sia un token: i pagamenti in RAND e in token a uno stesso beneficiario richiedono proposte separate.", "amounts overflow": "overflow degli importi", "an action is {n} characters, more than {max}": { one: "un'azione è di {n} carattere, più di {max}", many: "un'azione è di {n} caratteri, più di {max}", other: "un'azione è di {n} caratteri, più di {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Un indirizzo (rand1…), un link di pagamento (randpay:…) o il nome di un contatto salvato.", "An address here is hexadecimal — 0-9 and a-f only.": "Qui un indirizzo è esadecimale — solo 0-9 e a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Un indirizzo su questa catena è di 20 byte — 40 caratteri esadecimali. Questo ne ha {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Un indirizzo su questa catena è di 32 byte — 64 caratteri esadecimali. Questo ne ha {length}.", + "An amount is a number of RAND, such as 1.5.": "Un importo è un numero di RAND, come 1.5.", "an unknown chain": "una catena sconosciuta", "another tab changed this wallet while it was syncing; try again": "un'altra scheda ha modificato questo portafoglio durante la sincronizzazione; riprova", "Another tab is syncing": "Un'altra scheda sta sincronizzando", @@ -189,11 +206,16 @@ export default { "Chains known": "Catene note", "Change back to you": "Resto a te", "Change it": "Modificalo", + "Check": "Verifica", + "Check a share": "Verifica una quota di firma", "Check Activity": "Controlla Attività", "Check Activity first": "Controlla prima Attività", "Check before saving": "Verifica prima di salvare", "Check fingerprint": "Verifica l'impronta", + "Check payments": "Verifica i pagamenti", + "Check proposal": "Verifica la proposta", "Check settings": "Controlla le impostazioni", + "Check share": "Verifica la quota di firma", "Checking it against the chain before anything is proved.": "Verifica con la catena prima di provare qualsiasi cosa.", "Checking the pool and your notes…": "Verifica del pool e delle tue note…", "Checking the swap…": "Verifica dello scambio…", @@ -220,6 +242,7 @@ export default { "Continue": "Continua", "Copied": "Copiato", "Copied as JSON": "Copiato come JSON", + "Copy": "Copia", "Copy address": "Copia l'indirizzo", "Copy as JSON": "Copia come JSON", "Copy link": "Copia il link", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "Inserisci un importo da inviare.", "Enter an amount.": "Inserisci un importo.", "Enter at least 10 characters": "Inserisci almeno 10 caratteri", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Inserisci ogni pagamento che intendi fare: l'indirizzo e l'importo in RAND. Un pagamento allo stesso indirizzo due volte si inserisce una volta sola, con il totale. Un output che non corrisponde a nessuno non può essere firmato.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Inserisci l'indirizzo di ogni beneficiario: un output che non corrisponde all'indirizzo inserito, o il cui indirizzo è vuoto, non può essere firmato.", + "Enter the address of every payment.": "Inserisci l'indirizzo di ogni pagamento.", "Enter the address this should be released to.": "Inserisci l'indirizzo a cui deve essere rilasciato.", "Enter the address you are sending to.": "Inserisci l'indirizzo a cui stai inviando.", "Enter the amount as a number, for example 1.25.": "Inserisci l'importo come numero, per esempio 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Inserisci l'importo di ogni pagamento: è ciò che impedisce una divisione o un pagamento gonfiato.", "Enter the first 4 characters of your recovery key.": "Inserisci i primi 4 caratteri della tua chiave di recupero.", "Enter the relayer fee as a number, for example 1.25.": "Inserisci la commissione del relayer come numero, per esempio 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Inserisci la password di questo portafoglio — l'associazione è protetta da essa.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "Inserisci la tua chiave di recupero.", "Epoch": "Epoca", "Exactly those four letters, in capitals.": "Esattamente quelle quattro lettere, in maiuscolo.", + "expected recipient {address}: {reason}": "destinatario atteso {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients deve essere un elenco di indirizzi", "Explore": "Esplora", "Export spend key": "Esporta la chiave di spesa", "Exporting the spend key is behind your password.": "L'esportazione della chiave di spesa è protetta dalla tua password.", @@ -288,6 +317,7 @@ export default { "Fee": "Commissione", "fee must be at least {fee} RAND (the bridge burn floor)": "la commissione deve essere di almeno {fee} RAND (il minimo per il burn del bridge)", "fee must be at least {fee} RAND (the bundle floor)": "la commissione deve essere di almeno {fee} RAND (il minimo del bundle)", + "Fee: {fee} RAND · expires at height {height}": "Commissione: {fee} RAND · scade all'altezza {height}", "Filter activity by asset": "Filtra l'attività per asset", "Fingerprint": "Impronta", "fingerprint {fp}": "impronta {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "Ho capito che chiunque abbia questa chiave può spendere i miei fondi", "Import wallet": "Importa portafoglio", "Incorrect password.": "Password errata.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "gli input contengono {held}, ma importo + commissione fanno {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "gli input contengono {held}, ma commissione + deposito RAND fanno {need}", "insufficient balance: have {have}, need {need}": "saldo insufficiente: hai {have}, servono {need}", + "Intended payments": "Pagamenti previsti", "is on chain {chain}, not {expected}": "è sulla catena {chain}, non {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Non può vedere il tuo saldo né la tua attività, e non può spostare nulla.", "It cannot spend.": "Non può spendere.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "il file della chiave non ha spend_key", "key file must be version 2": "il file della chiave deve essere alla versione 2", "Key hidden — enter your password to view it again.": "Chiave nascosta — inserisci la password per vederla di nuovo.", + "Label": "Etichetta", "Language": "Lingua", "Leave empty to let the payer choose.": "Lascia vuoto per far scegliere a chi paga.", "Light": "Chiaro", @@ -360,11 +393,15 @@ export default { "machines": "macchine", "malformed parameter {parameter}": "parametro {parameter} non valido", "Manage contacts": "Gestisci i contatti", + "Manage multisig signers": "Gestisci i firmatari multisig", "Max": "Max", "Memo": "Memo", "memo \"{memo}\"": "memo «{memo}»", "memo is {n} bytes, at most {max}": { one: "il memo è di {n} byte, al massimo {max}", many: "il memo è di {n} byte, al massimo {max}", other: "il memo è di {n} byte, al massimo {max}" }, "More is on the way": "Altro in arrivo", + "Multisig share": "Quota di firma multisig", + "Multisig signers": "Firmatari multisig", + "Multisig signing is not available in this app.": "La firma multisig non è disponibile in questa app.", "My own prover · {name}": "Il mio prover · {name}", "Name": "Nome", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nomi per gli indirizzi che paghi spesso, conservati solo su questo dispositivo. Invia a un contatto per nome; la sua impronta viene verificata quando lo salvi.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Ancora nessun contatto. Salvane uno dalle Impostazioni, oppure incolla un indirizzo.", "no endpoint could answer {method}": "nessun endpoint ha potuto rispondere a {method}", "no endpoint could answer {method}; tried {endpoints}": "nessun endpoint ha potuto rispondere a {method}; provati {endpoints}", + "no expected recipient given: the payee was NOT checked": "nessun destinatario atteso indicato: il beneficiario NON è stato verificato", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Nessun nodo ha risposto a {url}, quindi la scadenza non si può verificare e questa proposta non viene firmata. Riprova quando il nodo è raggiungibile.", "No proof is pending.": "Nessuna prova in sospeso.", "No prover": "Nessun prover", "No prover is paired.": "Nessun prover associato.", "No RAND to send yet": "Ancora nessun RAND da inviare", "No registry assets": "Nessun asset nel registro", "no reply": "nessuna risposta", + "No signer profiles yet": "Ancora nessun profilo di firmatario", + "No such signer profile.": "Nessun profilo di firmatario del genere.", "No transaction or block matches that.": "Nessuna transazione o blocco corrisponde.", "no wallet on this device": "nessun portafoglio su questo dispositivo", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Nessuno dei link predefiniti dei prover di RandProtocol indica la chiave che questo portafoglio ha fissato per esso; non vengono usati.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Una prova viene eseguita su questo dispositivo — circa due minuti. Puoi guardare altre schermate; chiudere il portafoglio la interrompe.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "solo {locked} è bloccato in quella moneta sulla catena {chain}; scegli un'altra garanzia o un importo minore", "Open durian.market": "Apri durian.market", + "Open file": "Apri file", "Open in explorer": "Apri nell'explorer", "Open in randscan": "Apri in randscan", "Open in side panel": "Apri nel pannello laterale", + "Open in the side panel to choose files": "Apri nel pannello laterale per scegliere i file", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Apri Rand Wallet con {label} invece di digitare la password. La password funziona ancora, e resta il modo per ripristinare l'accesso.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Apri il portafoglio con {label} invece di digitare la password. La password funziona ancora, e resta il modo per ripristinare l'accesso.", "Open this window from a site that asked to connect.": "Apri questa finestra da un sito che ha chiesto di connettersi.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Oppure usa i prover di RandProtocol — {n} macchina gestita dai validatori, con la propria chiave.", many: "Oppure usa i prover di RandProtocol — {n} macchine gestite dai validatori, ognuna con la propria chiave.", other: "Oppure usa i prover di RandProtocol — {n} macchine gestite dai validatori, ognuna con la propria chiave." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Oppure usa i prover di RandProtocol — macchine gestite dai validatori, ognuna con la propria chiave.", "Outcome unknown": "Esito sconosciuto", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "l'output {slot} non si apre come di questo account: rifiuto di firmare un valore che non può vedere", "own={value}, expected 1 or absent": "own={value}, atteso 1 o assente", "Paid": "Pagato", "Pair your own prover": "Associa un tuo prover", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "Incolla la tua chiave di recupero", "path is not {n} levels": { one: "il percorso non è di {n} livello", many: "il percorso non è di {n} livelli", other: "il percorso non è di {n} livelli" }, "Pay me on Rand": "Pagami su Rand", + "Payee address": "Indirizzo del beneficiario", "Payment link": "Link di pagamento", "Payment link · fingerprint {fingerprint}": "Link di pagamento · impronta {fingerprint}", "Payment link copied": "Link di pagamento copiato", "Payment links cannot be read in this app; paste the address instead.": "I link di pagamento non possono essere letti in questa app; incolla invece l'indirizzo.", "Payment links cannot be read in this app.": "I link di pagamento non possono essere letti in questa app.", + "Payments changed: check them again before signing.": "I pagamenti sono cambiati: verificali di nuovo prima di firmare.", "Peers": "Peer", "Pending": "In attesa", "Permission to reach that host was not granted, so the node was left as it was.": "Il permesso di raggiungere quell'host non è stato concesso, quindi il nodo è rimasto com'era.", "Permission to reach that host was not granted.": "Il permesso di raggiungere quell'host non è stato concesso.", "Permission to reach that prover was not granted, so nothing was saved.": "Il permesso di raggiungere quel prover non è stato concesso, quindi non è stato salvato nulla.", "Point the camera at a Rand QR code.": "Inquadra un codice QR di Rand con la fotocamera.", + "Policy file": "File della policy", "Pool fee": "Commissione del pool", "Price impact": "Impatto sul prezzo", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Prezzo fissato dai pool di durian.market su questa catena (commissione del pool {pct}%). Lo scambio è una transazione che questo portafoglio prova e invia da sé.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Le prove vengono generate solo su questo dispositivo. Dove non può generarne una, l'invio attende finché non associ un prover o usi di nuovo il prover di RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Le prove che questo dispositivo non può generare vanno a {name}. La sua impronta è {fingerprint} — verifica che il prover mostri la stessa.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Le prove che questo dispositivo non può generare vanno a uno di essi; ognuno che genera la prova di un invio vede la chiave di visualizzazione di quel portafoglio.", + "Proposal (proposal.json)": "Proposta (proposal.json)", "Prove and send": "Prova e invia", "Prove for my other devices": "Genera prove per i miei altri dispositivi", "Prover": "Prover", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "Commissione del relayer (facoltativa)", "Remove": "Rimuovi", "Remove {name}": "Rimuovi {name}", + "Remove this signer": "Rimuovi questo firmatario", "Request RAND": "Richiedi RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Richiedi RAND dal faucet per provare a inviare, ricevere e generare prove su questa rete.", "Rescan": "Nuova scansione", @@ -574,8 +623,12 @@ export default { "Running on": "In esecuzione su", "Save": "Salva", "Save an address you pay often, and send to it by name.": "Salva un indirizzo che paghi spesso e invia a esso per nome.", + "Save as file": "Salva come file", + "Save signer": "Salva firmatario", "Save your recovery key": "Salva la tua chiave di recupero", "Saved": "Salvato", + "Saved to {path}": "Salvato in {path}", + "Saved.": "Salvato.", "Scan": "Scansiona", "Scan a payment link": "Scansiona un link di pagamento", "Scan a QR code": "Scansiona un codice QR", @@ -596,6 +649,7 @@ export default { "Sent.": "Inviato.", "Set on this device when this wallet was created.": "Impostata su questo dispositivo quando è stato creato questo portafoglio.", "Settings": "Impostazioni", + "Share": "Quota di firma", "Share payment link": "Condividi il link di pagamento", "shielded address decodes to {n} bytes, expected {expected}": { one: "l'indirizzo schermato si decodifica in {n} byte, attesi {expected}", many: "l'indirizzo schermato si decodifica in {n} byte, attesi {expected}", other: "l'indirizzo schermato si decodifica in {n} byte, attesi {expected}" }, "shielded address is not base58": "l'indirizzo schermato non è base58", @@ -607,6 +661,12 @@ export default { "Show the full address": "Mostra l'indirizzo completo", "Show viewing key": "Mostra la chiave di visualizzazione", "Show viewing key for 10 seconds": "Mostra la chiave di visualizzazione per 10 secondi", + "Sign": "Firma", + "Sign a proposal": "Firma una proposta", + "signer {index} is not in this policy (it has {count} signers)": "il firmatario {index} non è in questa policy (ha {count} firmatari)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "La quota di firma del firmatario {index} è valida. {have} di {threshold} in mano: firmatari {list}.", + "signer {index}'s signature does not verify over this proposal": "la firma del firmatario {index} non è valida per questa proposta", + "Signer key file, or its 64-hex seed": "File della chiave del firmatario, o il suo seed di 64 hex", "So the address above is one you have actually read.": "Così l'indirizzo qui sopra è uno che hai davvero letto.", "Something went wrong": "Qualcosa è andato storto", "Something went wrong.": "Qualcosa è andato storto.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "Impossibile copiarlo.", "That could not be saved.": "Impossibile salvarlo.", "That does not match — reveal the key again and check.": "Non corrisponde — mostra di nuovo la chiave e verifica.", + "That file is larger than a proposal, a policy or a key file can be.": "Quel file è più grande di quanto possa essere una proposta, una policy o un file di chiave.", "That is more than your balance of {balance} {symbol}.": "È più del tuo saldo di {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Non sono né 64 caratteri esadecimali (un hash) né un'altezza di blocco (solo cifre).", "That is not a pairing link.": "Non è un link di associazione.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "Impossibile calcolare la commissione.", "the genesis hash is not hex of a plausible length": "l'hash di genesis non è un esadecimale di lunghezza plausibile", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "l'inflow è `none` esattamente quando il bundle non brucia alcun token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "l'indirizzo del file di chiave non è quello che il suo seed deriva", + "the key file's public_key is not the one its seed derives": "il public_key del file di chiave non è quello che il suo seed deriva", "The key you exported when this wallet was created.": "La chiave che hai esportato quando è stato creato questo portafoglio.", "The language could not be saved.": "Impossibile salvare la lingua.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Il link chiede {linked} {symbol}; hai digitato {typed} {symbol}.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "La password che sblocca questo portafoglio su questo dispositivo.", "The payer decides": "Decide chi paga", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "La prova in sospeso è stata cancellata (il portafoglio si è bloccato, o è stata annullata altrove), quindi non è stato inviato nulla. Invia di nuovo.", + "the policy does not parse: {reason}": "la policy non si può analizzare: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Il pool è cambiato da quando questa pagina lo ha letto. Non è stato inviato nulla; il sito può fare una nuova quotazione.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Il pool è cambiato mentre veniva generata la prova. Non è stato inviato nulla; il sito può fare una nuova quotazione.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Il pool non contiene abbastanza per pagarlo. Non è stato inviato nulla; il sito può fare una nuova quotazione.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "la prova non si verifica: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "la prova ha pubblicato un digest che questo portafoglio non ha costruito ({published} invece di {expected}); non viene usata", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "La prova viene eseguita su questo dispositivo. Puoi guardare altre schermate — continua — ma chiudere il portafoglio la interrompe.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "La verifica della proposta ha restituito una risposta incompleta, quindi questa proposta non viene firmata.", + "the proposal does not parse: {reason}": "la proposta non si può analizzare: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "la proposta scade a {expires}, fuori dalla finestra del suo bundle ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "l'auth_commit della proposta non è l'account di questa policy", + "the proposal's expected digest is not its bundle's": "il digest atteso della proposta non è quello del suo bundle", + "the proposal's signing_message is not its binding's": "il signing_message della proposta non è quello del suo binding", "the prover": "il prover", "the prover accepted the job but named no job id": "il prover ha accettato il lavoro ma non ha indicato un id di lavoro", "The prover answered with an unknown state ({state}).": "Il prover ha risposto con uno stato sconosciuto ({state}).", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "La richiesta non è stata approvata in Rand Wallet.", "The rescan could not be started.": "Impossibile avviare la nuova scansione.", "The rescan did not finish": "La nuova scansione non è terminata", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "i profili di firmatario salvati non si sono potuti aprire; non è stato modificato nulla. Ripristina l'archiviazione di questo dispositivo, oppure cancella il portafoglio e ripristinalo dalla sua chiave", "The scan was cancelled.": "La scansione è stata annullata.", "The side panel could not be opened.": "Impossibile aprire il pannello laterale.", "The site sent an invoke this wallet cannot read: {reason}.": "Il sito ha inviato un invoke che questo portafoglio non sa leggere: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "Lo scambio non è andato a buon fine.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Lo scambio continua. Chiudere il portafoglio prima che indichi inviato lo interrompe, e non si perde nulla.", "The theme could not be saved.": "Impossibile salvare il tema.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "la transazione porta un'azione; una proposta multisig privata è un pagamento dal solo suo bundle", + "the transaction carries no bundle": "la transazione non porta alcun bundle", + "the transaction does not hash to the proposal's binding": "la transazione non ha come hash il binding della proposta", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "la transazione è per la catena {tx}, la proposta indica la catena {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "La chiave di transazione rivela solo questo pagamento — il suo importo e il suo destinatario — a chiunque tu la dia. Non può spendere nulla.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "La transazione potrebbe essere già on-chain. Controlla Attività prima di prelevare di nuovo — prelevando due volte bruceresti due volte.", "The transfer could not be proved.": "Impossibile provare il trasferimento.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "Il portafoglio ha ricevuto: {message}", "The withdrawal could not be proved.": "Impossibile provare il prelievo.", "The withdrawal was submitted, but the node did not return a usable hash.": "Il prelievo è stato inviato, ma il nodo non ha restituito un hash utilizzabile.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "il valore del witness non si può verificare: il suo nk o il suo salt non sono quelli di questa proposta", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "il valore del witness non corrisponde agli output: l'asset {asset} ha {held} in ingresso e {spent} in uscita con la commissione e i burn", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "il valore del witness non corrisponde alla transazione: l'input {input} non produce il nullifier del bundle", "Theme": "Tema", "There is no pool for that pair.": "Non esiste un pool per quella coppia.", "There is no program {program}… on this chain. Nothing was sent.": "Non esiste un programma {program}… su questa catena. Non è stato inviato nulla.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "Questo dispositivo, o dove non può · prover di {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Questo dimentica le note e la cronologia lette dalla vecchia catena e legge questo nodo dall'inizio. Le tue chiavi e la password non vengono toccate.", "This key can spend your funds": "Questa chiave può spendere i tuoi fondi", + "this key is not one of the policy's signers": "questa chiave non è uno dei firmatari della policy", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Questa chiave è il tuo portafoglio. Chiunque la possieda può spendere i tuoi fondi, e senza di essa un dispositivo perso significa fondi persi. Puoi vederla di nuovo nelle Impostazioni con la tua password.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Questo esce dal pool schermato. L'indirizzo di destinazione e l'importo diventano pubblici sull'altra catena.", "This link is too long for a QR code — copy it instead.": "Questo link è troppo lungo per un codice QR — copialo invece.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "Questa nota non è stata trovata.", "This passkey does not support the PRF extension.": "Questa passkey non supporta l'estensione PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Questa prova è già stata inviata da un'altra finestra. Controlla Attività prima di inviare di nuovo.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Questa proposta scade all'altezza {expires} e la catena è a {head}: troppo tardi per firmare. Chiedi una nuova proposta.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Questa proposta è legata a un genesis diverso da quello che serve il nodo di questo portafoglio.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "questa proposta è per un'altra policy: nomina {theirs}, questa policy è {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Questa proposta è per la catena {theirs}; questo portafoglio è sulla catena {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "questo prover applica {fee} RAND per prova, e questa versione del portafoglio non paga commissioni ai prover: associa un prover gratuito, oppure genera la prova su questo dispositivo", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Questo prover applica una commissione, che questa versione del portafoglio non paga. Associa un prover gratuito, oppure invia dall'app desktop.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Questo prover non accetta questo tipo di lavoro ({reason}). Associa un altro prover nelle Impostazioni, oppure invia dall'app desktop.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Questo rimuove il portafoglio da questo dispositivo. Ti servirà la chiave di recupero per ripristinarlo.", "This screen could not be shown.": "Impossibile mostrare questa schermata.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Questa schermata arriverà in un aggiornamento successivo. Il tuo portafoglio è sbloccato e pronto.", + "this signer is already saved as {label}": "questo firmatario è già salvato come {label}", + "This signer profile has no key on this device: remove it and add it again.": "Questo profilo di firmatario non ha una chiave su questo dispositivo: rimuovilo e aggiungilo di nuovo.", "This tab is showing what it has so far, and will refresh when that finishes.": "Questa scheda mostra ciò che ha finora, e si aggiornerà al termine.", "This takes a few minutes; keep this window open.": "Richiede qualche minuto; tieni aperta questa finestra.", "This transaction was not found.": "Questa transazione non è stata trovata.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "Digita {word} per confermare", "Type it once more.": "Digitala ancora una volta.", "Type the last four characters of that address": "Digita gli ultimi quattro caratteri di quell'indirizzo", + "Type the last four characters of the account fingerprint": "Digita gli ultimi quattro caratteri dell'impronta dell'account", "unavailable in this app": "non disponibile in questa app", "unknown": "sconosciuto", "unknown parameter {name}": "parametro sconosciuto {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "Debole", "Welcome back": "Bentornato", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Ciò che il pool ti paga arriva come una nota che il tuo portafoglio trova alla prossima sincronizzazione.", + "What this proposal does, as checked against your policy": "Cosa fa questa proposta, verificato rispetto alla tua policy", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Dove questo dispositivo non può generare una prova, associa un prover. La tua chiave di spesa resta comunque qui; un prover che esegui tu stesso — l'app desktop, o rand-prover sulla tua macchina — mantiene anche la tua cronologia per te.", "Where to?": "Verso dove?", "Which address?": "Quale indirizzo?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Il tuo prover non ha finito dopo {n} minuto di prova. Riprendi più tardi, oppure annulla.", many: "Il tuo prover non ha finito dopo {n} minuti di prova. Riprendi più tardi, oppure annulla.", other: "Il tuo prover non ha finito dopo {n} minuti di prova. Riprendi più tardi, oppure annulla." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Il tuo prover non ha avviato questa prova dopo {n} minuto in coda. Riprendi più tardi, oppure annulla.", many: "Il tuo prover non ha avviato questa prova dopo {n} minuti in coda. Riprendi più tardi, oppure annulla.", other: "Il tuo prover non ha avviato questa prova dopo {n} minuti in coda. Riprendi più tardi, oppure annulla." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Impossibile aprire l'associazione del tuo prover. Blocca e sblocca il portafoglio, oppure associa di nuovo il prover nelle Impostazioni.", + "Your share — send it to whoever proves the proposal": "La tua quota di firma — mandala a chi dimostra la proposta", "Your wallet picks up what it pays you on its next sync.": "Il tuo portafoglio rileva ciò che ti paga alla prossima sincronizzazione.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "La cronologia del tuo portafoglio è stata letta da {chain} — non è stato modificato nulla, e questo portafoglio non invierà né richiederà fondi finché le due non coincidono. Cambia nodo nelle Impostazioni, oppure esegui una nuova scansione di questo portafoglio per la nuova catena.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Questo è l'indirizzo dei prover {name}. Non serve associarli: questo wallet li usa per impostazione predefinita.", diff --git a/ui/locales/ja.js b/ui/locales/ja.js index e3d10a5..0fa7014 100644 --- a/ui/locales/ja.js +++ b/ui/locales/ja.js @@ -1,6 +1,7 @@ // Japanese (ja). Generated from en.js key order; one translation per key. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "証明まであと {blocks} ブロックです。", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} ピア", "{count} peers": "{count} ピア", @@ -29,6 +30,7 @@ export default { "{label} is off": "{label} はオフです", "{label} is on": "{label} はオンです", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} ではこのウォレットを開けなくなりました(パスワードが変更されました)。パスワードでロックを解除してから、設定で {label} をもう一度オンにしてください。", + "{matched} of {total} payments matched.": "{total} 件中 {matched} 件の支払いが一致しました。", "{member} answered with another key than the one this wallet pins": "{member} が、このウォレットが固定している鍵とは別の鍵で応答しました", "{member} did not answer": "{member} が応答しませんでした", "{member} does not take this wallet's jobs": "{member} はこのウォレットのジョブを受け付けません", @@ -49,6 +51,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} は出金できません", "{symbol} has {decimals} decimal places — that is more.": "{symbol} の小数点以下は {decimals} 桁です。入力はそれを超えています。", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} は、選んだコインとして、そのコイン自身のチェーン上でリリースされます。", + "{threshold} of {count} · signer {index}": "{threshold}/{count} · 署名者 {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold}/{count} · このキーは署名者 {index} です", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "チェーン {chain} 上の {token} は小数点以下 {decimals} 桁です: 金額とリレイヤー手数料は {unit} の倍数である必要があります", "{units} units of asset {asset}": "アセット {asset} の {units} 単位", "{url} answered first — chain {chain} · height {height}.": "{url} が最初に応答しました — チェーン {chain} · 高さ {height}。", @@ -81,7 +85,12 @@ export default { "A prover address must be https://.": "プルーバーのアドレスは https:// である必要があります。", "A prover is making the proof": "プルーバーが証明を作成しています", "A prover sees your history": "プルーバーはあなたの履歴を見られます", + "a share is index:hex": "署名シェアは index:hex 形式です", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand ネットワーク用のシールドウォレットです。残高と履歴は非公開のまま保たれ、すべての送信はこの端末上で証明されます。", + "a signer key is a rand-node keygen file or 64 hex characters": "署名者キーは rand-node の keygen ファイル、または 64 桁の16進文字です", + "a signer label is 1-64 characters": "署名者のラベルは 1-64 文字です", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "署名者プロファイルは、このウォレットの暗号化された保管庫に保存されます。署名のたびにパスワードを再度求められます。", + "a signer seed is 64 hex characters": "署名者のシードは 64 桁の16進文字です", "A site": "サイト", "a spend key is 64 hex characters, or a wallet.key.json": "支出キーは16進数64文字、または wallet.key.json です", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "まずトランザクションハッシュとして検索し、見つからなければ同じハッシュをブロックとして検索します。数字のみの場合はブロック高でブロックを検索します。", @@ -98,8 +107,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "一覧にないチェーンへの出金は拒否される場合があります。証明されるまで何も使われません。", "About": "情報", "about {left} left": "残り約 {left}", + "Account fingerprint — compare it with every other signer before you save": "アカウントのフィンガープリント — 保存する前に、他のすべての署名者と照合してください", "Activity": "アクティビティ", "Add a contact": "連絡先を追加", + "Add a signer": "署名者を追加", + "Add payment": "支払いを追加", + "Add the policy file and your signer key to sign for a private multisig account.": "プライベートなマルチシグアカウントで署名するには、ポリシーファイルと署名者キーを追加してください。", "Address": "アドレス", "Address copied": "アドレスをコピーしました", "Address or payment link": "アドレスまたは支払いリンク", @@ -108,14 +121,18 @@ export default { "almost done": "まもなく完了", "also tried {endpoints}": "{endpoints} も試しました", "Amount": "金額", + "Amount in RAND": "金額 (RAND)", "amount must be greater than zero": "金額はゼロより大きくする必要があります", "amount overflow": "金額がオーバーフローしました", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND 以外の支払いは金額を確認できません。その場合は金額を空欄にしてください。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "この受取人は RAND とトークンの両方を受け取ります。同じ受取人への RAND とトークンの支払いは、別々の提案にする必要があります。", "amounts overflow": "金額の合計がオーバーフローしました", "an action is {n} characters, more than {max}": "アクションが {n} 文字あり、上限の {max} を超えています", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "アドレス(rand1…)、支払いリンク(randpay:…)、または保存した連絡先の名前。", "An address here is hexadecimal — 0-9 and a-f only.": "ここでのアドレスは16進数です。0-9 と a-f のみ使えます。", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "このチェーンのアドレスは20バイト(16進数40文字)です。入力されたのは {length} です。", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "このチェーンのアドレスは32バイト(16進数64文字)です。入力されたのは {length} です。", + "An amount is a number of RAND, such as 1.5.": "金額は RAND の数値です。例: 1.5", "an unknown chain": "不明なチェーン", "another tab changed this wallet while it was syncing; try again": "同期中に別のタブがこのウォレットを変更しました。もう一度お試しください", "Another tab is syncing": "別のタブが同期中です", @@ -182,11 +199,16 @@ export default { "Chains known": "既知のチェーン", "Change back to you": "あなたへのお釣り", "Change it": "変更する", + "Check": "確認", + "Check a share": "署名シェアを確認", "Check Activity": "アクティビティを確認", "Check Activity first": "先にアクティビティを確認", "Check before saving": "保存前に確認", "Check fingerprint": "フィンガープリントを確認", + "Check payments": "支払いを確認", + "Check proposal": "提案を確認", "Check settings": "設定を確認", + "Check share": "署名シェアを確認", "Checking it against the chain before anything is proved.": "何かを証明する前に、チェーンと照合しています。", "Checking the pool and your notes…": "プールとノートを確認しています…", "Checking the swap…": "スワップを確認しています…", @@ -213,6 +235,7 @@ export default { "Continue": "続行", "Copied": "コピーしました", "Copied as JSON": "JSON としてコピーしました", + "Copy": "コピー", "Copy address": "アドレスをコピー", "Copy as JSON": "JSON としてコピー", "Copy link": "リンクをコピー", @@ -261,9 +284,13 @@ export default { "Enter an amount to send.": "送信する金額を入力してください。", "Enter an amount.": "金額を入力してください。", "Enter at least 10 characters": "10文字以上入力してください", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "行う予定の支払いをすべて入力してください。アドレスと RAND 建ての金額です。同じアドレスへの支払いが2回ある場合は、合計額で1件として入力します。どれにも一致しない出力には署名できません。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "すべての受取人のアドレスを入力してください。入力したアドレスと一致しない出力、またはアドレスが空の出力には署名できません。", + "Enter the address of every payment.": "すべての支払いのアドレスを入力してください。", "Enter the address this should be released to.": "リリース先のアドレスを入力してください。", "Enter the address you are sending to.": "送信先のアドレスを入力してください。", "Enter the amount as a number, for example 1.25.": "金額を数値で入力してください(例: 1.25)。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "すべての支払いの金額を入力してください。支払いの分割や水増しを防ぐのは金額です。", "Enter the first 4 characters of your recovery key.": "リカバリーキーの最初の4文字を入力してください。", "Enter the relayer fee as a number, for example 1.25.": "リレイヤー手数料を数値で入力してください(例: 1.25)。", "Enter this wallet's password — the pairing is sealed under it.": "このウォレットのパスワードを入力してください。ペアリングはこのパスワードで保護されます。", @@ -273,6 +300,8 @@ export default { "Enter your recovery key.": "リカバリーキーを入力してください。", "Epoch": "エポック", "Exactly those four letters, in capitals.": "その4文字を、大文字でそのとおりに入力してください。", + "expected recipient {address}: {reason}": "想定される受取人 {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients はアドレスのリストでなければなりません", "Explore": "探索", "Export spend key": "支出キーをエクスポート", "Exporting the spend key is behind your password.": "支出キーのエクスポートにはパスワードが必要です。", @@ -281,6 +310,7 @@ export default { "Fee": "手数料", "fee must be at least {fee} RAND (the bridge burn floor)": "手数料は {fee} RAND 以上である必要があります(ブリッジのバーン下限)", "fee must be at least {fee} RAND (the bundle floor)": "手数料は {fee} RAND 以上である必要があります(バンドルの下限)", + "Fee: {fee} RAND · expires at height {height}": "手数料: {fee} RAND · 高さ {height} で期限切れ", "Filter activity by asset": "アセットでアクティビティを絞り込む", "Fingerprint": "フィンガープリント", "fingerprint {fp}": "フィンガープリント {fp}", @@ -318,9 +348,11 @@ export default { "I understand anyone with this key can spend my funds": "このキーを持つ人は誰でも私の資金を使えることを理解しました", "Import wallet": "ウォレットをインポート", "Incorrect password.": "パスワードが正しくありません。", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "インプットは {held} ですが、金額 + 手数料は {need} です", "inputs hold {held}, but fee + RAND deposit is {need}": "インプットは {held} ですが、手数料 + RAND 入金額は {need} です", "insufficient balance: have {have}, need {need}": "残高が不足しています: 保有 {have}、必要 {need}", + "Intended payments": "予定している支払い", "is on chain {chain}, not {expected}": "チェーン {chain} 上にあり、{expected} ではありません", "It cannot see your balance or your activity, and it cannot move anything.": "あなたの残高やアクティビティは見えず、何も移動できません。", "It cannot spend.": "支出はできません。", @@ -343,6 +375,7 @@ export default { "key file has no spend_key": "キーファイルに spend_key がありません", "key file must be version 2": "キーファイルはバージョン 2 である必要があります", "Key hidden — enter your password to view it again.": "キーを非表示にしました。もう一度表示するにはパスワードを入力してください。", + "Label": "ラベル", "Language": "言語", "Leave empty to let the payer choose.": "支払う人に選ばせる場合は空欄のままにしてください。", "Light": "ライト", @@ -353,11 +386,15 @@ export default { "machines": "マシン", "malformed parameter {parameter}": "不正なパラメーター {parameter}", "Manage contacts": "連絡先を管理", + "Manage multisig signers": "マルチシグ署名者を管理", "Max": "最大", "Memo": "メモ", "memo \"{memo}\"": "メモ「{memo}」", "memo is {n} bytes, at most {max}": "メモは {n} バイトです。上限は {max} バイトです", "More is on the way": "さらに届く予定です", + "Multisig share": "マルチシグ署名シェア", + "Multisig signers": "マルチシグ署名者", + "Multisig signing is not available in this app.": "このアプリではマルチシグ署名を利用できません。", "My own prover · {name}": "自分のプルーバー · {name}", "Name": "名前", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "よく支払うアドレスに付ける名前です。この端末上にのみ保存されます。連絡先には名前で送信でき、保存時にフィンガープリントが確認されます。", @@ -381,12 +418,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "まだ連絡先はありません。設定から保存するか、アドレスを貼り付けてください。", "no endpoint could answer {method}": "どのエンドポイントも {method} に応答できませんでした", "no endpoint could answer {method}; tried {endpoints}": "どのエンドポイントも {method} に応答できませんでした。試したエンドポイント: {endpoints}", + "no expected recipient given: the payee was NOT checked": "想定される受取人が指定されていません: 受取人は確認されていません (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url} でノードから応答がなかったため、有効期限を確認できず、この提案には署名しません。ノードに接続できるようになってから再試行してください。", "No proof is pending.": "保留中の証明はありません。", "No prover": "プルーバーなし", "No prover is paired.": "ペアリングされたプルーバーはありません。", "No RAND to send yet": "送信できる RAND はまだありません", "No registry assets": "レジストリのアセットはありません", "no reply": "応答なし", + "No signer profiles yet": "署名者プロファイルはまだありません", + "No such signer profile.": "そのような署名者プロファイルはありません。", "No transaction or block matches that.": "一致するトランザクションまたはブロックはありません。", "no wallet on this device": "この端末にウォレットがありません", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "組み込みの RandProtocol プルーバーのリンクには、このウォレットが固定している鍵を示すものがありません。使用しません。", @@ -433,9 +474,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "証明を1つこの端末で実行します(約2分)。他の画面を見ることはできますが、ウォレットを閉じると停止します。", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "チェーン {chain} 上のそのコインにロックされているのは {locked} だけです。別の裏付けを選ぶか、金額を減らしてください", "Open durian.market": "durian.market を開く", + "Open file": "ファイルを開く", "Open in explorer": "エクスプローラーで開く", "Open in randscan": "randscan で開く", "Open in side panel": "サイドパネルで開く", + "Open in the side panel to choose files": "サイドパネルで開いてファイルを選択してください", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "パスワードを入力する代わりに {label} で Rand Wallet を開きます。パスワードも引き続き使え、アクセスを復元する手段として残ります。", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "パスワードを入力する代わりに {label} でウォレットを開きます。パスワードも引き続き使え、アクセスを復元する手段として残ります。", "Open this window from a site that asked to connect.": "接続を求めたサイトからこのウィンドウを開いてください。", @@ -445,6 +488,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "または RandProtocol のプルーバーを使います。バリデーターが運用する {n} 台のマシンで、それぞれ独自の鍵を持ちます。", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "または RandProtocol のプルーバーを使います。バリデーターが運用するマシンで、それぞれ独自の鍵を持ちます。", "Outcome unknown": "結果不明", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "出力 {slot} はこのアカウントのものとして開けません: 見えない値への署名を拒否します", "own={value}, expected 1 or absent": "own={value}、1 または省略が必要です", "Paid": "支払い済み", "Pair your own prover": "自分のプルーバーをペアリング", @@ -465,17 +509,20 @@ export default { "Paste your recovery key": "リカバリーキーを貼り付け", "path is not {n} levels": "パスが {n} 階層ではありません", "Pay me on Rand": "Rand で支払ってください", + "Payee address": "受取人のアドレス", "Payment link": "支払いリンク", "Payment link · fingerprint {fingerprint}": "支払いリンク · フィンガープリント {fingerprint}", "Payment link copied": "支払いリンクをコピーしました", "Payment links cannot be read in this app; paste the address instead.": "このアプリでは支払いリンクを読み取れません。代わりにアドレスを貼り付けてください。", "Payment links cannot be read in this app.": "このアプリでは支払いリンクを読み取れません。", + "Payments changed: check them again before signing.": "支払いが変更されました。署名する前にもう一度確認してください。", "Peers": "ピア", "Pending": "保留中", "Permission to reach that host was not granted, so the node was left as it was.": "そのホストへのアクセスが許可されなかったため、ノードは変更されていません。", "Permission to reach that host was not granted.": "そのホストへのアクセスが許可されませんでした。", "Permission to reach that prover was not granted, so nothing was saved.": "そのプルーバーへのアクセスが許可されなかったため、何も保存されていません。", "Point the camera at a Rand QR code.": "カメラを Rand の QR コードに向けてください。", + "Policy file": "ポリシーファイル", "Pool fee": "プール手数料", "Price impact": "価格への影響", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "このチェーン上の durian.market のプールによる価格です(プール手数料 {pct}%)。スワップは、このウォレット自身が証明して送信するトランザクションです。", @@ -485,6 +532,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "証明はこの端末でのみ作成されます。作成できない場合は、プルーバーをペアリングするか RandProtocol のプルーバーを再び使うまで、送信は待機します。", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "この端末で作成できない証明は {name} に送られます。フィンガープリントは {fingerprint} です。プルーバーに同じものが表示されていることを確認してください。", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "この端末で作成できない証明は、そのいずれかに送られます。送信を証明する各プルーバーは、そのウォレットの閲覧キーを見ることになります。", + "Proposal (proposal.json)": "提案 (proposal.json)", "Prove and send": "証明して送信", "Prove for my other devices": "他の端末のために証明する", "Prover": "プルーバー", @@ -533,6 +581,7 @@ export default { "Relayer fee (optional)": "リレイヤー手数料(任意)", "Remove": "削除", "Remove {name}": "{name} を削除", + "Remove this signer": "この署名者を削除", "Request RAND": "RAND をリクエスト", "Request RAND from the faucet to try sending, receiving and proving on this network.": "フォーセットから RAND をリクエストして、このネットワークで送信・受け取り・証明を試してみてください。", "Rescan": "再スキャン", @@ -567,8 +616,12 @@ export default { "Running on": "実行環境", "Save": "保存", "Save an address you pay often, and send to it by name.": "よく支払うアドレスを保存して、名前で送信できます。", + "Save as file": "ファイルとして保存", + "Save signer": "署名者を保存", "Save your recovery key": "リカバリーキーを保存", "Saved": "保存しました", + "Saved to {path}": "{path} に保存しました", + "Saved.": "保存しました。", "Scan": "スキャン", "Scan a payment link": "支払いリンクをスキャン", "Scan a QR code": "QR コードをスキャン", @@ -589,6 +642,7 @@ export default { "Sent.": "送信しました。", "Set on this device when this wallet was created.": "このウォレットの作成時にこの端末で設定されました。", "Settings": "設定", + "Share": "署名シェア", "Share payment link": "支払いリンクを共有", "shielded address decodes to {n} bytes, expected {expected}": "シールドアドレスをデコードすると {n} バイトですが、{expected} バイトが必要です", "shielded address is not base58": "シールドアドレスが base58 ではありません", @@ -600,6 +654,12 @@ export default { "Show the full address": "アドレス全体を表示", "Show viewing key": "閲覧キーを表示", "Show viewing key for 10 seconds": "閲覧キーを10秒間表示", + "Sign": "署名", + "Sign a proposal": "提案に署名", + "signer {index} is not in this policy (it has {count} signers)": "署名者 {index} はこのポリシーに含まれていません (署名者は {count} 人です)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "署名者 {index} の署名シェアは有効です。{threshold} 件中 {have} 件が揃っています: 署名者 {list}。", + "signer {index}'s signature does not verify over this proposal": "署名者 {index} の署名は、この提案に対して有効ではありません", + "Signer key file, or its 64-hex seed": "署名者キーファイル、または 64 桁16進のシード", "So the address above is one you have actually read.": "上のアドレスを実際に読んだことを確認するためです。", "Something went wrong": "問題が発生しました", "Something went wrong.": "問題が発生しました。", @@ -632,6 +692,7 @@ export default { "That could not be copied.": "コピーできませんでした。", "That could not be saved.": "保存できませんでした。", "That does not match — reveal the key again and check.": "一致しません — キーをもう一度表示して確認してください。", + "That file is larger than a proposal, a policy or a key file can be.": "そのファイルは、提案・ポリシー・キーファイルとして許容されるサイズを超えています。", "That is more than your balance of {balance} {symbol}.": "残高 {balance} {symbol} を超えています。", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "16進数64文字(ハッシュ)でもブロック高(数字のみ)でもありません。", "That is not a pairing link.": "ペアリングリンクではありません。", @@ -667,6 +728,8 @@ export default { "The fee could not be worked out.": "手数料を算出できませんでした。", "the genesis hash is not hex of a plausible length": "ジェネシスハッシュが妥当な長さの16進数ではありません", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow が `none` になるのは、バンドルがトークンをバーンしない場合(inflow.amount == 0)に限られます", + "the key file's address is not the one its seed derives": "キーファイルのアドレスが、そのシードから導出されるものと一致しません", + "the key file's public_key is not the one its seed derives": "キーファイルの public_key が、そのシードから導出されるものと一致しません", "The key you exported when this wallet was created.": "このウォレットの作成時にエクスポートしたキーです。", "The language could not be saved.": "言語を保存できませんでした。", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "リンクは {linked} {symbol} を要求していますが、入力されたのは {typed} {symbol} です。", @@ -707,6 +770,7 @@ export default { "The password that unlocks this wallet on this device.": "この端末でこのウォレットのロックを解除するパスワードです。", "The payer decides": "支払う人が決めます", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "保留中の証明が消去されたため(ウォレットがロックされたか、別の場所でキャンセルされました)、何も送信されていません。もう一度送信してください。", + "the policy does not parse: {reason}": "ポリシーを解析できません: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "このページが読み込んだ後にプールが変化しました。何も送信されていません。サイトで再度見積もりできます。", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "証明中にプールが変化しました。何も送信されていません。サイトで再度見積もりできます。", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "プールにはこれを支払うだけの残高がありません。何も送信されていません。サイトで再度見積もりできます。", @@ -717,6 +781,12 @@ export default { "the proof does not verify: {reason}": "証明が検証できません: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "証明が、このウォレットが構築していないダイジェストを公開しました({expected} に対して {published})。使用しません", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "証明はこの端末で実行されます。他の画面を見ることはでき、その間も続行されますが、ウォレットを閉じると停止します。", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案の確認が不完全な応答を返したため、この提案には署名しません。", + "the proposal does not parse: {reason}": "提案を解析できません: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "提案の期限 {expires} は、そのバンドルの範囲 ({from}, {to}] の外です", + "the proposal's auth_commit is not this policy's account": "提案の auth_commit が、このポリシーのアカウントと一致しません", + "the proposal's expected digest is not its bundle's": "提案の想定ダイジェストが、そのバンドルのものと一致しません", + "the proposal's signing_message is not its binding's": "提案の signing_message が、そのバインディングのものと一致しません", "the prover": "プルーバー", "the prover accepted the job but named no job id": "プルーバーはジョブを受け付けましたが、ジョブ ID を返しませんでした", "The prover answered with an unknown state ({state}).": "プルーバーが不明な状態({state})を返しました。", @@ -778,6 +848,7 @@ export default { "The request was not approved in Rand Wallet.": "リクエストは Rand Wallet で承認されませんでした。", "The rescan could not be started.": "再スキャンを開始できませんでした。", "The rescan did not finish": "再スキャンが完了しませんでした", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "保存された署名者プロファイルを開けませんでした。何も変更していません。この端末のストレージを復元するか、ウォレットを消去してキーから復元してください", "The scan was cancelled.": "スキャンはキャンセルされました。", "The side panel could not be opened.": "サイドパネルを開けませんでした。", "The site sent an invoke this wallet cannot read: {reason}.": "サイトから、このウォレットが読み取れないインボークが送られてきました: {reason}。", @@ -788,6 +859,10 @@ export default { "The swap did not go through.": "スワップは処理されませんでした。", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "スワップは続行されます。「送信済み」と表示される前にウォレットを閉じると停止しますが、何も失われません。", "The theme could not be saved.": "テーマを保存できませんでした。", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "トランザクションにアクションが含まれています。プライベートなマルチシグ提案は、そのバンドルだけからの支払いです", + "the transaction carries no bundle": "トランザクションにバンドルが含まれていません", + "the transaction does not hash to the proposal's binding": "トランザクションのハッシュが提案のバインディングと一致しません", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "トランザクションはチェーン {tx} 用ですが、提案はチェーン {proposal} を指しています", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "トランザクションキーは、この1件の支払い(金額と受取人)を、渡した相手に開示します。何も支出できません。", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "トランザクションは既にチェーン上にある可能性があります。再度出金する前にアクティビティを確認してください。二重に出金すると二重にバーンされます。", "The transfer could not be proved.": "送金を証明できませんでした。", @@ -805,6 +880,9 @@ export default { "The wallet was told: {message}": "ウォレットへのメッセージ: {message}", "The withdrawal could not be proved.": "出金を証明できませんでした。", "The withdrawal was submitted, but the node did not return a usable hash.": "出金は提出されましたが、ノードが有効なハッシュを返しませんでした。", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "ウィットネスの値を確認できません: その nk またはソルトがこの提案のものではありません", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "ウィットネスの値が出力と一致しません: アセット {asset} は、入力 {held} に対し、手数料とバーンを含めた出力が {spent} です", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "ウィットネスの値がトランザクションと一致しません: 入力 {input} はバンドルのヌリファイアに対応しません", "Theme": "テーマ", "There is no pool for that pair.": "そのペアのプールはありません。", "There is no program {program}… on this chain. Nothing was sent.": "このチェーンにはプログラム {program}… はありません。何も送信されていません。", @@ -846,6 +924,7 @@ export default { "This device, or where it cannot · {name} provers": "この端末、または作成できない場合は {name} のプルーバー", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "古いチェーンから読み込んだノートと履歴を消去し、このノードを最初から読み込みます。キーとパスワードには影響しません。", "This key can spend your funds": "このキーで資金を使えます", + "this key is not one of the policy's signers": "このキーはポリシーの署名者ではありません", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "このキーがあなたのウォレットです。持っている人は誰でも資金を使えます。また、これがなければ端末を失ったときに資金も失われます。設定でパスワードを入力すると、もう一度表示できます。", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "シールドプールから出ます。宛先アドレスと金額は相手側のチェーンで公開されます。", "This link is too long for a QR code — copy it instead.": "このリンクは QR コードには長すぎます。代わりにコピーしてください。", @@ -861,6 +940,10 @@ export default { "This note was not found.": "このノートは見つかりませんでした。", "This passkey does not support the PRF extension.": "このパスキーは PRF 拡張に対応していません。", "This proof was already submitted from another window. Check Activity before sending again.": "この証明は既に別のウィンドウから提出されています。再送信する前にアクティビティを確認してください。", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "この提案は高さ {expires} で期限切れとなり、チェーンは {head} にあります: 署名には遅すぎます。新しい提案を依頼してください。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "この提案は、このウォレットのノードが提供するものとは別のジェネシスに結び付けられています。", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "この提案は別のポリシー用です: {theirs} を指していますが、このポリシーは {ours} です", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "この提案はチェーン {theirs} 用ですが、このウォレットはチェーン {ours} にあります。", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "このプルーバーは証明1件につき {fee} RAND を請求しますが、このバージョンのウォレットはプルーバーの手数料を支払いません: 手数料のかからないプルーバーをペアリングするか、この端末で証明してください", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "このプルーバーは手数料を請求しますが、このバージョンのウォレットは支払いません。手数料のかからないプルーバーをペアリングするか、デスクトップアプリから送信してください。", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "このプルーバーはこの種類のジョブを受け付けません({reason})。設定で別のプルーバーをペアリングするか、デスクトップアプリから送信してください。", @@ -872,6 +955,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "この端末からウォレットを削除します。復元にはリカバリーキーが必要です。", "This screen could not be shown.": "この画面を表示できませんでした。", "This screen ships in a later task. Your wallet is unlocked and ready.": "この画面は今後のタスクで提供されます。ウォレットはロック解除済みで、使用できます。", + "this signer is already saved as {label}": "この署名者はすでに {label} として保存されています", + "This signer profile has no key on this device: remove it and add it again.": "この署名者プロファイルには、この端末にキーがありません。削除してから追加し直してください。", "This tab is showing what it has so far, and will refresh when that finishes.": "このタブにはこれまでの内容が表示されており、完了すると更新されます。", "This takes a few minutes; keep this window open.": "数分かかります。このウィンドウを開いたままにしてください。", "This transaction was not found.": "このトランザクションは見つかりませんでした。", @@ -918,6 +1003,7 @@ export default { "Type {word} to confirm": "確認のため {word} と入力してください", "Type it once more.": "もう一度入力してください。", "Type the last four characters of that address": "そのアドレスの最後の4文字を入力してください", + "Type the last four characters of the account fingerprint": "アカウントのフィンガープリントの末尾4文字を入力してください", "unavailable in this app": "このアプリでは利用できません", "unknown": "不明", "unknown parameter {name}": "不明なパラメーター {name}", @@ -954,6 +1040,7 @@ export default { "Weak": "弱い", "Welcome back": "おかえりなさい", "What the pool pays you arrives as a note your wallet finds on its next sync.": "プールから支払われる額は、次回の同期時にウォレットが見つけるノートとして届きます。", + "What this proposal does, as checked against your policy": "この提案が行うこと (あなたのポリシーと照合済み)", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "この端末で証明を作成できない場合は、プルーバーをペアリングしてください。どちらの場合も支出キーはここに残ります。自分で運用するプルーバー(デスクトップアプリ、または自分のマシン上の rand-prover)なら、履歴も自分だけのものにできます。", "Where to?": "送金先は?", "Which address?": "どのアドレスですか?", @@ -1000,6 +1087,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "プルーバーは {n} 分間証明していますが、まだ完了していません。後で再開するか、キャンセルしてください。", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "プルーバーのキューで {n} 分待っていますが、この証明はまだ開始されていません。後で再開するか、キャンセルしてください。", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "プルーバーとのペアリングを開けませんでした。ウォレットをロックしてからロック解除するか、設定でプルーバーをもう一度ペアリングしてください。", + "Your share — send it to whoever proves the proposal": "あなたの署名シェア — 提案を証明する人に送ってください", "Your wallet picks up what it pays you on its next sync.": "ウォレットは、支払われた額を次回の同期時に受け取ります。", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "ウォレットの履歴は {chain} から読み込まれました。何も変更されておらず、両者が一致するまで、このウォレットは資金の送信や請求を行いません。設定でノードを切り替えるか、新しいチェーン用にこのウォレットを再スキャンしてください。", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "これは {name} プルーバーのアドレスです。ペアリングは不要です。このウォレットは標準でこれらを使います。", diff --git a/ui/locales/ko.js b/ui/locales/ko.js index 00bc4c9..9c1d334 100644 --- a/ui/locales/ko.js +++ b/ui/locales/ko.js @@ -1,6 +1,7 @@ // 한국어 (ko). See ui/locales/README.md. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "증명까지 {blocks} 블록 남았습니다.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "피어 {count}개", "{count} peers": "피어 {count}개", @@ -29,6 +30,7 @@ export default { "{label} is off": "{label} 꺼짐", "{label} is on": "{label} 켜짐", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label}(으)로 더 이상 이 지갑을 열 수 없습니다(비밀번호가 변경됨). 비밀번호로 잠금을 해제한 다음 설정에서 {label}을(를) 다시 켜세요.", + "{matched} of {total} payments matched.": "결제 {total}건 중 {matched}건이 일치합니다.", "{member} answered with another key than the one this wallet pins": "{member}이(가) 이 지갑이 고정한 키와 다른 키로 응답했습니다", "{member} did not answer": "{member}이(가) 응답하지 않았습니다", "{member} does not take this wallet's jobs": "{member}은(는) 이 지갑의 작업을 받지 않습니다", @@ -49,6 +51,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol}은(는) 출금할 수 없습니다", "{symbol} has {decimals} decimal places — that is more.": "{symbol}의 소수 자릿수는 {decimals}자리입니다. 입력한 값은 그보다 많습니다.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol}은(는) 선택한 코인으로, 해당 코인의 체인에서 지급됩니다.", + "{threshold} of {count} · signer {index}": "{threshold}/{count} · 서명자 {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold}/{count} · 이 키는 서명자 {index}입니다", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "체인 {chain}의 {token}은(는) 소수 {decimals}자리입니다. 금액과 릴레이어 수수료는 {unit}의 배수여야 합니다", "{units} units of asset {asset}": "자산 {asset} {units}단위", "{url} answered first — chain {chain} · height {height}.": "{url}이(가) 먼저 응답했습니다 — 체인 {chain} · 높이 {height}.", @@ -81,7 +85,12 @@ export default { "A prover address must be https://.": "프루버 주소는 https://여야 합니다.", "A prover is making the proof": "프루버가 증명을 만드는 중", "A prover sees your history": "프루버가 내역을 볼 수 있습니다", + "a share is index:hex": "서명 조각은 index:hex 형식입니다", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand 네트워크용 차폐 지갑입니다. 잔액과 내역은 비공개로 유지되며, 모든 전송은 이 기기에서 증명됩니다.", + "a signer key is a rand-node keygen file or 64 hex characters": "서명자 키는 rand-node keygen 파일 또는 64자리 16진수 문자입니다", + "a signer label is 1-64 characters": "서명자 레이블은 1-64자입니다", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "서명자 프로필은 이 지갑의 암호화된 보관함에 저장됩니다. 서명할 때마다 비밀번호를 다시 묻습니다.", + "a signer seed is 64 hex characters": "서명자 시드는 64자리 16진수 문자입니다", "A site": "사이트", "a spend key is 64 hex characters, or a wallet.key.json": "지출 키는 16진수 64자 또는 wallet.key.json 파일입니다", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "먼저 거래 해시로 찾아보고, 없으면 같은 해시를 블록으로 찾습니다. 숫자만 입력하면 높이로 블록을 찾습니다.", @@ -98,8 +107,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "목록에 없는 체인으로 출금하면 거부될 수 있습니다. 증명되기 전까지는 아무것도 지출되지 않습니다.", "About": "정보", "about {left} left": "약 {left} 남음", + "Account fingerprint — compare it with every other signer before you save": "계정 지문 — 저장하기 전에 다른 모든 서명자와 비교하세요", "Activity": "활동", "Add a contact": "연락처 추가", + "Add a signer": "서명자 추가", + "Add payment": "결제 추가", + "Add the policy file and your signer key to sign for a private multisig account.": "비공개 멀티시그 계정으로 서명하려면 정책 파일과 서명자 키를 추가하세요.", "Address": "주소", "Address copied": "주소를 복사했습니다", "Address or payment link": "주소 또는 결제 링크", @@ -108,14 +121,18 @@ export default { "almost done": "거의 완료", "also tried {endpoints}": "{endpoints}도 시도함", "Amount": "금액", + "Amount in RAND": "금액 (RAND)", "amount must be greater than zero": "금액은 0보다 커야 합니다", "amount overflow": "금액 오버플로", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND가 아닌 결제는 금액을 확인할 수 없습니다. 이 경우 금액을 비워 두세요.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "이 수취인은 RAND와 토큰을 모두 받습니다. 한 수취인에게 보내는 RAND 결제와 토큰 결제는 별도의 제안이 필요합니다.", "amounts overflow": "금액 오버플로", "an action is {n} characters, more than {max}": "액션이 {n}자로, 최대 {max}자를 넘습니다", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "주소(rand1…), 결제 링크(randpay:…) 또는 저장된 연락처 이름.", "An address here is hexadecimal — 0-9 and a-f only.": "여기서 주소는 16진수입니다 — 0-9와 a-f만 사용합니다.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "이 체인의 주소는 20바이트, 즉 16진수 40자입니다. 입력한 값은 {length}자입니다.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "이 체인의 주소는 32바이트, 즉 16진수 64자입니다. 입력한 값은 {length}자입니다.", + "An amount is a number of RAND, such as 1.5.": "금액은 RAND 단위의 숫자입니다. 예: 1.5", "an unknown chain": "알 수 없는 체인", "another tab changed this wallet while it was syncing; try again": "동기화 중에 다른 탭에서 이 지갑을 변경했습니다. 다시 시도하세요", "Another tab is syncing": "다른 탭에서 동기화 중", @@ -182,11 +199,16 @@ export default { "Chains known": "알려진 체인", "Change back to you": "내게 돌아오는 잔돈", "Change it": "변경", + "Check": "확인", + "Check a share": "서명 조각 확인", "Check Activity": "활동 확인", "Check Activity first": "먼저 활동 확인", "Check before saving": "저장 전 확인", "Check fingerprint": "지문 확인", + "Check payments": "결제 확인", + "Check proposal": "제안 확인", "Check settings": "설정 확인", + "Check share": "서명 조각 확인", "Checking it against the chain before anything is proved.": "증명하기 전에 체인과 대조하는 중입니다.", "Checking the pool and your notes…": "풀과 노트를 확인하는 중…", "Checking the swap…": "스왑을 확인하는 중…", @@ -213,6 +235,7 @@ export default { "Continue": "계속", "Copied": "복사했습니다", "Copied as JSON": "JSON으로 복사했습니다", + "Copy": "복사", "Copy address": "주소 복사", "Copy as JSON": "JSON으로 복사", "Copy link": "링크 복사", @@ -261,9 +284,13 @@ export default { "Enter an amount to send.": "보낼 금액을 입력하세요.", "Enter an amount.": "금액을 입력하세요.", "Enter at least 10 characters": "10자 이상 입력하세요", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "보내려는 모든 결제를 입력하세요: 주소와 RAND 금액입니다. 같은 주소로 두 번 결제하는 경우 합계로 한 번만 입력합니다. 어느 것과도 일치하지 않는 출력은 서명할 수 없습니다.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "모든 수취인의 주소를 입력하세요. 입력한 주소와 일치하지 않거나 주소가 비어 있는 출력은 서명할 수 없습니다.", + "Enter the address of every payment.": "모든 결제의 주소를 입력하세요.", "Enter the address this should be released to.": "지급받을 주소를 입력하세요.", "Enter the address you are sending to.": "보낼 주소를 입력하세요.", "Enter the amount as a number, for example 1.25.": "금액을 숫자로 입력하세요(예: 1.25).", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "모든 결제의 금액을 입력하세요. 결제를 쪼개거나 부풀리는 것을 막아 주는 것이 금액입니다.", "Enter the first 4 characters of your recovery key.": "복구 키의 처음 4자를 입력하세요.", "Enter the relayer fee as a number, for example 1.25.": "릴레이어 수수료를 숫자로 입력하세요(예: 1.25).", "Enter this wallet's password — the pairing is sealed under it.": "이 지갑의 비밀번호를 입력하세요 — 페어링 정보가 이 비밀번호로 봉인됩니다.", @@ -273,6 +300,8 @@ export default { "Enter your recovery key.": "복구 키를 입력하세요.", "Epoch": "에포크", "Exactly those four letters, in capitals.": "정확히 그 네 글자를 대문자로 입력하세요.", + "expected recipient {address}: {reason}": "예상 수취인 {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients는 주소 목록이어야 합니다", "Explore": "탐색", "Export spend key": "지출 키 내보내기", "Exporting the spend key is behind your password.": "지출 키를 내보내려면 비밀번호가 필요합니다.", @@ -281,6 +310,7 @@ export default { "Fee": "수수료", "fee must be at least {fee} RAND (the bridge burn floor)": "수수료는 최소 {fee} RAND여야 합니다(브리지 소각 최저 수수료)", "fee must be at least {fee} RAND (the bundle floor)": "수수료는 최소 {fee} RAND여야 합니다(번들 최저 수수료)", + "Fee: {fee} RAND · expires at height {height}": "수수료: {fee} RAND · 높이 {height}에서 만료", "Filter activity by asset": "자산별로 활동 필터링", "Fingerprint": "지문", "fingerprint {fp}": "지문 {fp}", @@ -318,9 +348,11 @@ export default { "I understand anyone with this key can spend my funds": "이 키를 가진 사람은 누구나 내 자금을 지출할 수 있음을 이해합니다", "Import wallet": "지갑 가져오기", "Incorrect password.": "비밀번호가 올바르지 않습니다.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "입력 노트에 {held}이(가) 있지만, 금액 + 수수료는 {need}입니다", "inputs hold {held}, but fee + RAND deposit is {need}": "입력 노트에 {held}이(가) 있지만, 수수료 + RAND 입금액은 {need}입니다", "insufficient balance: have {have}, need {need}": "잔액 부족: 보유 {have}, 필요 {need}", + "Intended payments": "예정된 결제", "is on chain {chain}, not {expected}": "체인 {expected}이(가) 아니라 체인 {chain}에 있습니다", "It cannot see your balance or your activity, and it cannot move anything.": "잔액이나 활동을 볼 수 없으며, 아무것도 옮길 수 없습니다.", "It cannot spend.": "지출할 수 없습니다.", @@ -343,6 +375,7 @@ export default { "key file has no spend_key": "키 파일에 spend_key가 없습니다", "key file must be version 2": "키 파일은 버전 2여야 합니다", "Key hidden — enter your password to view it again.": "키가 숨겨졌습니다 — 다시 보려면 비밀번호를 입력하세요.", + "Label": "레이블", "Language": "언어", "Leave empty to let the payer choose.": "비워 두면 지불자가 정합니다.", "Light": "라이트", @@ -353,11 +386,15 @@ export default { "machines": "머신", "malformed parameter {parameter}": "잘못된 매개변수 {parameter}", "Manage contacts": "연락처 관리", + "Manage multisig signers": "멀티시그 서명자 관리", "Max": "최대", "Memo": "메모", "memo \"{memo}\"": "메모 \"{memo}\"", "memo is {n} bytes, at most {max}": "메모가 {n}바이트입니다. 최대 {max}바이트입니다", "More is on the way": "더 많은 기능이 준비 중입니다", + "Multisig share": "멀티시그 서명 조각", + "Multisig signers": "멀티시그 서명자", + "Multisig signing is not available in this app.": "이 앱에서는 멀티시그 서명을 사용할 수 없습니다.", "My own prover · {name}": "내 프루버 · {name}", "Name": "이름", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "자주 보내는 주소에 붙이는 이름으로, 이 기기에만 보관됩니다. 연락처 이름으로 보낼 수 있으며, 저장할 때 지문을 확인합니다.", @@ -381,12 +418,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "아직 연락처가 없습니다. 설정에서 저장하거나 주소를 붙여 넣으세요.", "no endpoint could answer {method}": "{method}에 응답할 수 있는 엔드포인트가 없습니다", "no endpoint could answer {method}; tried {endpoints}": "{method}에 응답할 수 있는 엔드포인트가 없습니다. 시도한 곳: {endpoints}", + "no expected recipient given: the payee was NOT checked": "예상 수취인이 지정되지 않았습니다: 수취인을 확인하지 않았습니다 (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url}에서 응답한 노드가 없어 만료를 확인할 수 없으므로 이 제안에는 서명하지 않습니다. 노드에 연결할 수 있을 때 다시 시도하세요.", "No proof is pending.": "대기 중인 증명이 없습니다.", "No prover": "프루버 없음", "No prover is paired.": "페어링된 프루버가 없습니다.", "No RAND to send yet": "아직 보낼 RAND가 없습니다", "No registry assets": "레지스트리 자산 없음", "no reply": "응답 없음", + "No signer profiles yet": "서명자 프로필이 아직 없습니다", + "No such signer profile.": "해당 서명자 프로필이 없습니다.", "No transaction or block matches that.": "일치하는 거래나 블록이 없습니다.", "no wallet on this device": "이 기기에 지갑이 없습니다", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "내장된 RandProtocol 프루버 링크 중 이 지갑이 고정한 키를 지정한 것이 없어 사용하지 않습니다.", @@ -433,9 +474,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "이 기기에서 증명 하나를 실행합니다 — 약 2분 걸립니다. 다른 화면을 볼 수 있지만, 지갑을 닫으면 중단됩니다.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "체인 {chain}의 해당 코인에는 {locked}만 잠겨 있습니다. 다른 담보를 선택하거나 금액을 줄이세요", "Open durian.market": "durian.market 열기", + "Open file": "파일 열기", "Open in explorer": "탐색기에서 열기", "Open in randscan": "randscan에서 열기", "Open in side panel": "사이드 패널에서 열기", + "Open in the side panel to choose files": "사이드 패널에서 열어 파일을 선택하세요", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "비밀번호를 입력하는 대신 {label}(으)로 Rand Wallet을 여세요. 비밀번호도 계속 사용할 수 있으며, 접근을 복원하는 수단으로 남습니다.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "비밀번호를 입력하는 대신 {label}(으)로 지갑을 여세요. 비밀번호도 계속 사용할 수 있으며, 접근을 복원하는 수단으로 남습니다.", "Open this window from a site that asked to connect.": "연결을 요청한 사이트에서 이 창을 여세요.", @@ -445,6 +488,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "또는 RandProtocol 프루버를 사용하세요 — 검증자가 운영하는 머신 {n}대이며, 각각 자체 키를 가집니다.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "또는 RandProtocol 프루버를 사용하세요 — 검증자가 운영하는 머신이며, 각각 자체 키를 가집니다.", "Outcome unknown": "결과 알 수 없음", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "출력 {slot}이(가) 이 계정의 것으로 열리지 않습니다: 볼 수 없는 값에는 서명을 거부합니다", "own={value}, expected 1 or absent": "own={value}, 1이거나 없어야 합니다", "Paid": "지불됨", "Pair your own prover": "자체 프루버 페어링", @@ -465,17 +509,20 @@ export default { "Paste your recovery key": "복구 키 붙여넣기", "path is not {n} levels": "경로가 {n}단계가 아닙니다", "Pay me on Rand": "Rand로 지불해 주세요", + "Payee address": "수취인 주소", "Payment link": "결제 링크", "Payment link · fingerprint {fingerprint}": "결제 링크 · 지문 {fingerprint}", "Payment link copied": "결제 링크를 복사했습니다", "Payment links cannot be read in this app; paste the address instead.": "이 앱에서는 결제 링크를 읽을 수 없습니다. 대신 주소를 붙여 넣으세요.", "Payment links cannot be read in this app.": "이 앱에서는 결제 링크를 읽을 수 없습니다.", + "Payments changed: check them again before signing.": "결제가 변경되었습니다. 서명하기 전에 다시 확인하세요.", "Peers": "피어", "Pending": "대기 중", "Permission to reach that host was not granted, so the node was left as it was.": "해당 호스트에 접근할 권한이 허용되지 않아 노드를 그대로 두었습니다.", "Permission to reach that host was not granted.": "해당 호스트에 접근할 권한이 허용되지 않았습니다.", "Permission to reach that prover was not granted, so nothing was saved.": "해당 프루버에 접근할 권한이 허용되지 않아 아무것도 저장하지 않았습니다.", "Point the camera at a Rand QR code.": "카메라를 Rand QR 코드에 맞추세요.", + "Policy file": "정책 파일", "Pool fee": "풀 수수료", "Price impact": "가격 영향", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "이 체인의 durian.market 풀 기준 가격입니다(풀 수수료 {pct}%). 스왑은 이 지갑이 직접 증명하고 보내는 거래입니다.", @@ -485,6 +532,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "증명은 이 기기에서만 만듭니다. 이 기기에서 만들 수 없는 경우, 프루버를 페어링하거나 RandProtocol 프루버를 다시 사용할 때까지 전송이 대기합니다.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "이 기기에서 만들 수 없는 증명은 {name}(으)로 보냅니다. 지문은 {fingerprint}입니다 — 프루버에 같은 지문이 표시되는지 확인하세요.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "이 기기에서 만들 수 없는 증명은 그중 하나로 보냅니다. 전송을 증명하는 프루버는 각각 해당 지갑의 조회 키를 봅니다.", + "Proposal (proposal.json)": "제안 (proposal.json)", "Prove and send": "증명 후 보내기", "Prove for my other devices": "내 다른 기기를 위해 증명", "Prover": "프루버", @@ -533,6 +581,7 @@ export default { "Relayer fee (optional)": "릴레이어 수수료(선택 사항)", "Remove": "삭제", "Remove {name}": "{name} 삭제", + "Remove this signer": "이 서명자 제거", "Request RAND": "RAND 요청", "Request RAND from the faucet to try sending, receiving and proving on this network.": "이 네트워크에서 보내기, 받기, 증명을 시험해 보려면 포셋에서 RAND를 요청하세요.", "Rescan": "재스캔", @@ -567,8 +616,12 @@ export default { "Running on": "실행 위치", "Save": "저장", "Save an address you pay often, and send to it by name.": "자주 보내는 주소를 저장하고 이름으로 보내세요.", + "Save as file": "파일로 저장", + "Save signer": "서명자 저장", "Save your recovery key": "복구 키 저장", "Saved": "저장됨", + "Saved to {path}": "{path}에 저장했습니다", + "Saved.": "저장했습니다.", "Scan": "스캔", "Scan a payment link": "결제 링크 스캔", "Scan a QR code": "QR 코드 스캔", @@ -589,6 +642,7 @@ export default { "Sent.": "보냈습니다.", "Set on this device when this wallet was created.": "이 지갑을 만들 때 이 기기에서 설정했습니다.", "Settings": "설정", + "Share": "서명 조각", "Share payment link": "결제 링크 공유", "shielded address decodes to {n} bytes, expected {expected}": "차폐 주소를 디코딩하면 {n}바이트입니다. {expected}바이트여야 합니다", "shielded address is not base58": "차폐 주소가 base58이 아닙니다", @@ -600,6 +654,12 @@ export default { "Show the full address": "전체 주소 표시", "Show viewing key": "조회 키 표시", "Show viewing key for 10 seconds": "조회 키를 10초 동안 표시", + "Sign": "서명", + "Sign a proposal": "제안에 서명", + "signer {index} is not in this policy (it has {count} signers)": "서명자 {index}은(는) 이 정책에 없습니다 (서명자는 {count}명입니다)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "서명자 {index}의 서명 조각이 유효합니다. {threshold}개 중 {have}개 확보: 서명자 {list}.", + "signer {index}'s signature does not verify over this proposal": "서명자 {index}의 서명이 이 제안에 대해 유효하지 않습니다", + "Signer key file, or its 64-hex seed": "서명자 키 파일 또는 64자리 16진수 시드", "So the address above is one you have actually read.": "위 주소를 실제로 읽었는지 확인하기 위함입니다.", "Something went wrong": "문제가 발생했습니다", "Something went wrong.": "문제가 발생했습니다.", @@ -632,6 +692,7 @@ export default { "That could not be copied.": "복사하지 못했습니다.", "That could not be saved.": "저장하지 못했습니다.", "That does not match — reveal the key again and check.": "일치하지 않습니다 — 키를 다시 표시하고 확인하세요.", + "That file is larger than a proposal, a policy or a key file can be.": "파일이 제안, 정책, 키 파일로 허용되는 크기보다 큽니다.", "That is more than your balance of {balance} {symbol}.": "잔액 {balance} {symbol}보다 많습니다.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "16진수 64자(해시)도 아니고 블록 높이(숫자만)도 아닙니다.", "That is not a pairing link.": "페어링 링크가 아닙니다.", @@ -667,6 +728,8 @@ export default { "The fee could not be worked out.": "수수료를 계산하지 못했습니다.", "the genesis hash is not hex of a plausible length": "제네시스 해시가 그럴듯한 길이의 16진수가 아닙니다", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "번들이 토큰을 소각하지 않을 때(inflow.amount == 0)에만 inflow가 `none`입니다", + "the key file's address is not the one its seed derives": "키 파일의 주소가 시드에서 도출된 주소와 다릅니다", + "the key file's public_key is not the one its seed derives": "키 파일의 public_key가 시드에서 도출된 값과 다릅니다", "The key you exported when this wallet was created.": "이 지갑을 만들 때 내보낸 키입니다.", "The language could not be saved.": "언어를 저장하지 못했습니다.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "링크에서 요청한 금액은 {linked} {symbol}인데, 입력한 금액은 {typed} {symbol}입니다.", @@ -707,6 +770,7 @@ export default { "The password that unlocks this wallet on this device.": "이 기기에서 이 지갑의 잠금을 해제하는 비밀번호입니다.", "The payer decides": "지불자가 결정", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "대기 중이던 증명이 지워져(지갑이 잠겼거나 다른 곳에서 취소됨) 아무것도 전송되지 않았습니다. 다시 보내세요.", + "the policy does not parse: {reason}": "정책을 해석할 수 없습니다: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "이 페이지가 풀을 읽은 뒤 풀이 변경되었습니다. 아무것도 전송되지 않았으며, 사이트에서 다시 견적을 받을 수 있습니다.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "증명하는 동안 풀이 변경되었습니다. 아무것도 전송되지 않았으며, 사이트에서 다시 견적을 받을 수 있습니다.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "풀에 이 금액을 지급할 만큼의 자산이 없습니다. 아무것도 전송되지 않았으며, 사이트에서 다시 견적을 받을 수 있습니다.", @@ -717,6 +781,12 @@ export default { "the proof does not verify: {reason}": "증명이 검증되지 않습니다: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "증명이 이 지갑이 만들지 않은 다이제스트를 공개했습니다({expected} 대신 {published}). 사용하지 않습니다", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "증명은 이 기기에서 실행됩니다. 다른 화면을 볼 수 있고 증명은 계속 진행되지만, 지갑을 닫으면 중단됩니다.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "제안 확인이 불완전한 응답을 반환했으므로 이 제안에는 서명하지 않습니다.", + "the proposal does not parse: {reason}": "제안을 해석할 수 없습니다: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "제안의 만료 {expires}이(가) 번들의 구간 ({from}, {to}] 밖에 있습니다", + "the proposal's auth_commit is not this policy's account": "제안의 auth_commit이 이 정책의 계정이 아닙니다", + "the proposal's expected digest is not its bundle's": "제안의 예상 다이제스트가 번들의 것과 다릅니다", + "the proposal's signing_message is not its binding's": "제안의 signing_message가 바인딩의 것과 다릅니다", "the prover": "프루버", "the prover accepted the job but named no job id": "프루버가 작업을 수락했지만 작업 ID를 알려 주지 않았습니다", "The prover answered with an unknown state ({state}).": "프루버가 알 수 없는 상태({state})로 응답했습니다.", @@ -778,6 +848,7 @@ export default { "The request was not approved in Rand Wallet.": "Rand Wallet에서 요청이 승인되지 않았습니다.", "The rescan could not be started.": "재스캔을 시작하지 못했습니다.", "The rescan did not finish": "재스캔이 완료되지 않았습니다", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "저장된 서명자 프로필을 열 수 없습니다. 변경된 것은 없습니다. 이 기기의 저장소를 복원하거나, 지갑을 지우고 키로 복원하세요", "The scan was cancelled.": "스캔이 취소되었습니다.", "The side panel could not be opened.": "사이드 패널을 열지 못했습니다.", "The site sent an invoke this wallet cannot read: {reason}.": "사이트가 이 지갑이 읽을 수 없는 호출을 보냈습니다: {reason}.", @@ -788,6 +859,10 @@ export default { "The swap did not go through.": "스왑이 처리되지 않았습니다.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "스왑은 계속 진행됩니다. 전송됨이 표시되기 전에 지갑을 닫으면 중단되지만, 잃는 것은 없습니다.", "The theme could not be saved.": "테마를 저장하지 못했습니다.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "트랜잭션에 액션이 포함되어 있습니다. 비공개 멀티시그 제안은 번들만으로 이루어진 결제입니다", + "the transaction carries no bundle": "트랜잭션에 번들이 없습니다", + "the transaction does not hash to the proposal's binding": "트랜잭션의 해시가 제안의 바인딩과 다릅니다", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "트랜잭션은 체인 {tx}용이지만 제안은 체인 {proposal}을(를) 가리킵니다", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "거래 키는 이 결제 하나 — 금액과 수신자 — 를 키를 받은 사람에게 공개합니다. 아무것도 지출할 수 없습니다.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "거래가 이미 체인에 있을 수 있습니다. 다시 출금하기 전에 활동을 확인하세요 — 두 번 출금하면 두 번 소각됩니다.", "The transfer could not be proved.": "송금을 증명하지 못했습니다.", @@ -805,6 +880,9 @@ export default { "The wallet was told: {message}": "지갑이 받은 메시지: {message}", "The withdrawal could not be proved.": "출금을 증명하지 못했습니다.", "The withdrawal was submitted, but the node did not return a usable hash.": "출금을 제출했지만, 노드가 사용할 수 있는 해시를 반환하지 않았습니다.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "증인(witness) 값을 확인할 수 없습니다: nk 또는 솔트가 이 제안의 것이 아닙니다", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "증인(witness) 값이 출력과 일치하지 않습니다: 자산 {asset}의 입력은 {held}, 수수료와 소각을 포함한 출력은 {spent}입니다", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "증인(witness) 값이 트랜잭션과 일치하지 않습니다: 입력 {input}이(가) 번들의 널리파이어로 이어지지 않습니다", "Theme": "테마", "There is no pool for that pair.": "해당 쌍에 대한 풀이 없습니다.", "There is no program {program}… on this chain. Nothing was sent.": "이 체인에는 프로그램 {program}…이(가) 없습니다. 아무것도 전송되지 않았습니다.", @@ -846,6 +924,7 @@ export default { "This device, or where it cannot · {name} provers": "이 기기, 불가능한 경우 · {name} 프루버", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "이전 체인에서 읽은 노트와 내역을 지우고 이 노드를 처음부터 읽습니다. 키와 비밀번호는 변경되지 않습니다.", "This key can spend your funds": "이 키로 자금을 지출할 수 있습니다", + "this key is not one of the policy's signers": "이 키는 정책의 서명자가 아닙니다", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "이 키가 곧 지갑입니다. 이 키를 가진 사람은 누구나 자금을 지출할 수 있으며, 키 없이 기기를 잃어버리면 자금도 잃게 됩니다. 설정에서 비밀번호로 다시 볼 수 있습니다.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "차폐 풀을 벗어납니다. 도착 주소와 금액이 다른 체인에 공개됩니다.", "This link is too long for a QR code — copy it instead.": "이 링크는 QR 코드로 만들기에 너무 깁니다 — 대신 복사하세요.", @@ -861,6 +940,10 @@ export default { "This note was not found.": "이 노트를 찾을 수 없습니다.", "This passkey does not support the PRF extension.": "이 패스키는 PRF 확장을 지원하지 않습니다.", "This proof was already submitted from another window. Check Activity before sending again.": "이 증명은 이미 다른 창에서 제출되었습니다. 다시 보내기 전에 활동을 확인하세요.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "이 제안은 높이 {expires}에서 만료되며 체인은 {head}에 있습니다: 서명하기에는 너무 늦었습니다. 새 제안을 요청하세요.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "이 제안은 이 지갑의 노드가 제공하는 것과 다른 제네시스에 묶여 있습니다.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "이 제안은 다른 정책용입니다: {theirs}을(를) 가리키며 이 정책은 {ours}입니다", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "이 제안은 체인 {theirs}용이며, 이 지갑은 체인 {ours}에 있습니다.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "이 프루버는 증명당 {fee} RAND를 받는데, 이 버전의 지갑은 프루버 수수료를 지불하지 않습니다. 수수료가 없는 프루버를 페어링하거나 이 기기에서 증명하세요", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "이 프루버는 수수료를 받는데, 이 버전의 지갑은 이를 지불하지 않습니다. 수수료가 없는 프루버를 페어링하거나 데스크톱 앱에서 보내세요.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "이 프루버는 이런 종류의 작업을 받지 않습니다({reason}). 설정에서 다른 프루버를 페어링하거나 데스크톱 앱에서 보내세요.", @@ -872,6 +955,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "이 기기에서 지갑을 삭제합니다. 복원하려면 복구 키가 필요합니다.", "This screen could not be shown.": "이 화면을 표시하지 못했습니다.", "This screen ships in a later task. Your wallet is unlocked and ready.": "이 화면은 이후 작업에서 제공됩니다. 지갑은 잠금 해제되어 사용할 준비가 되었습니다.", + "this signer is already saved as {label}": "이 서명자는 이미 {label}(으)로 저장되어 있습니다", + "This signer profile has no key on this device: remove it and add it again.": "이 서명자 프로필에는 이 기기에 키가 없습니다. 제거한 뒤 다시 추가하세요.", "This tab is showing what it has so far, and will refresh when that finishes.": "이 탭은 지금까지의 내용을 표시하며, 작업이 끝나면 새로 고칩니다.", "This takes a few minutes; keep this window open.": "몇 분 정도 걸립니다. 이 창을 열어 두세요.", "This transaction was not found.": "이 거래를 찾을 수 없습니다.", @@ -918,6 +1003,7 @@ export default { "Type {word} to confirm": "확인하려면 {word}을(를) 입력하세요", "Type it once more.": "한 번 더 입력하세요.", "Type the last four characters of that address": "해당 주소의 마지막 네 글자를 입력하세요", + "Type the last four characters of the account fingerprint": "계정 지문의 마지막 네 글자를 입력하세요", "unavailable in this app": "이 앱에서는 사용할 수 없음", "unknown": "알 수 없음", "unknown parameter {name}": "알 수 없는 매개변수 {name}", @@ -954,6 +1040,7 @@ export default { "Weak": "약함", "Welcome back": "다시 오신 것을 환영합니다", "What the pool pays you arrives as a note your wallet finds on its next sync.": "풀에서 지급하는 금액은 다음 동기화 때 지갑이 찾는 노트로 도착합니다.", + "What this proposal does, as checked against your policy": "이 제안이 하는 일 (정책과 대조해 확인됨)", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "이 기기에서 증명을 만들 수 없는 경우 프루버를 페어링하세요. 어느 경우든 지출 키는 이 기기에 남습니다. 직접 운영하는 프루버(데스크톱 앱 또는 내 머신의 rand-prover)를 쓰면 내역도 나만 볼 수 있습니다.", "Where to?": "어디로 보낼까요?", "Which address?": "어느 주소인가요?", @@ -1000,6 +1087,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "프루버가 {n}분 동안 증명했지만 아직 완료하지 못했습니다. 나중에 재개하거나 취소하세요.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "프루버가 대기열에서 {n}분이 지나도록 이 증명을 시작하지 않았습니다. 나중에 재개하거나 취소하세요.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "프루버 페어링 정보를 열 수 없습니다. 지갑을 잠갔다가 다시 잠금 해제하거나, 설정에서 프루버를 다시 페어링하세요.", + "Your share — send it to whoever proves the proposal": "내 서명 조각 — 제안을 증명하는 사람에게 보내세요", "Your wallet picks up what it pays you on its next sync.": "이 거래로 받는 금액은 다음 동기화 때 지갑에 반영됩니다.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "지갑 내역은 {chain}에서 읽었습니다 — 아무것도 변경되지 않았으며, 두 체인이 일치할 때까지 이 지갑은 자금을 보내거나 요청하지 않습니다. 설정에서 노드를 바꾸거나 새 체인으로 이 지갑을 재스캔하세요.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "이것은 {name} 프루버의 주소입니다. 페어링할 필요가 없습니다. 이 지갑은 기본적으로 이 프루버를 사용합니다.", diff --git a/ui/locales/ms.js b/ui/locales/ms.js index dc45a09..9b0890a 100644 --- a/ui/locales/ms.js +++ b/ui/locales/ms.js @@ -3,6 +3,7 @@ // chain names, rand1… addresses, randpay:/randprover: links, URLs and `rand send`. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Tinggal {blocks} blok lagi untuk membuktikannya.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} rakan setara", "{count} peers": "{count} rakan setara", @@ -31,6 +32,7 @@ export default { "{label} is off": "{label} dimatikan", "{label} is on": "{label} dihidupkan", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} tidak lagi membuka dompet ini (kata laluannya telah berubah). Buka kunci dengan kata laluan, kemudian hidupkan semula {label} dalam Tetapan.", + "{matched} of {total} payments matched.": "{matched} daripada {total} pembayaran sepadan.", "{member} answered with another key than the one this wallet pins": "{member} menjawab dengan kunci yang berlainan daripada kunci yang ditetapkan oleh dompet ini", "{member} did not answer": "{member} tidak menjawab", "{member} does not take this wallet's jobs": "{member} tidak menerima kerja daripada dompet ini", @@ -51,6 +53,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} tidak boleh dikeluarkan", "{symbol} has {decimals} decimal places — that is more.": "{symbol} mempunyai {decimals} tempat perpuluhan — amaun ini melebihinya.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} dilepaskan sebagai syiling yang anda pilih, di rantaian syiling itu sendiri.", + "{threshold} of {count} · signer {index}": "{threshold} daripada {count} · penandatangan {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} daripada {count} · kunci ini ialah penandatangan {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} di rantaian {chain} mempunyai {decimals} perpuluhan: amaun dan yuran penyampai mestilah gandaan {unit}", "{units} units of asset {asset}": "{units} unit aset {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} menjawab dahulu — rantaian {chain} · ketinggian {height}.", @@ -83,7 +87,12 @@ export default { "A prover address must be https://.": "Alamat pembukti mestilah https://.", "A prover is making the proof": "Pembukti sedang membuat bukti", "A prover sees your history": "Pembukti dapat melihat sejarah anda", + "a share is index:hex": "bahagian tandatangan ialah index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Dompet terlindung untuk rangkaian Rand. Baki dan sejarah kekal peribadi — setiap penghantaran dibuktikan pada peranti ini.", + "a signer key is a rand-node keygen file or 64 hex characters": "kunci penandatangan ialah fail keygen rand-node atau 64 aksara hex", + "a signer label is 1-64 characters": "label penandatangan ialah 1-64 aksara", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Profil penandatangan disimpan dalam peti besi disulitkan dompet ini. Setiap tandatangan meminta kata laluan anda lagi.", + "a signer seed is 64 hex characters": "seed penandatangan ialah 64 aksara hex", "A site": "Sebuah laman", "a spend key is 64 hex characters, or a wallet.key.json": "kunci perbelanjaan ialah 64 aksara heks, atau wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Cincangan transaksi dicuba dahulu; jika tiada apa-apa ditemui, cincangan yang sama dicuba sebagai blok. Input yang semuanya digit mencari blok mengikut ketinggian.", @@ -100,8 +109,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Pengeluaran ke yang tidak disenaraikannya mungkin ditolak. Tiada apa-apa dibelanjakan sehingga ia dibuktikan.", "About": "Perihal", "about {left} left": "kira-kira {left} lagi", + "Account fingerprint — compare it with every other signer before you save": "Cap jari akaun — bandingkan dengan setiap penandatangan lain sebelum menyimpan", "Activity": "Aktiviti", "Add a contact": "Tambah kenalan", + "Add a signer": "Tambah penandatangan", + "Add payment": "Tambah pembayaran", + "Add the policy file and your signer key to sign for a private multisig account.": "Tambah fail dasar dan kunci penandatangan anda untuk menandatangani bagi akaun multisig persendirian.", "Address": "Alamat", "Address copied": "Alamat disalin", "Address or payment link": "Alamat atau pautan pembayaran", @@ -110,14 +123,18 @@ export default { "almost done": "hampir selesai", "also tried {endpoints}": "juga mencuba {endpoints}", "Amount": "Amaun", + "Amount in RAND": "Amaun dalam RAND", "amount must be greater than zero": "amaun mestilah lebih daripada sifar", "amount overflow": "amaun melimpah", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Amaun tidak boleh disemak untuk pembayaran bukan RAND: biarkan amaun kosong bagi pembayaran itu.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Penerima ini menerima RAND dan juga token: pembayaran RAND dan token kepada satu penerima memerlukan cadangan berasingan.", "amounts overflow": "amaun-amaun melimpah", "an action is {n} characters, more than {max}": "satu tindakan ialah {n} aksara, melebihi {max}", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Alamat (rand1…), pautan pembayaran (randpay:…), atau nama kenalan yang disimpan.", "An address here is hexadecimal — 0-9 and a-f only.": "Alamat di sini ialah heksadesimal — 0-9 dan a-f sahaja.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Alamat di rantaian ini ialah 20 bait — 40 aksara heks. Yang ini {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Alamat di rantaian ini ialah 32 bait — 64 aksara heks. Yang ini {length}.", + "An amount is a number of RAND, such as 1.5.": "Amaun ialah bilangan RAND, seperti 1.5.", "an unknown chain": "rantaian yang tidak diketahui", "another tab changed this wallet while it was syncing; try again": "tab lain mengubah dompet ini semasa ia menyegerak; cuba lagi", "Another tab is syncing": "Tab lain sedang menyegerak", @@ -184,11 +201,16 @@ export default { "Chains known": "Rantaian yang diketahui", "Change back to you": "Baki dikembalikan kepada anda", "Change it": "Ubahnya", + "Check": "Semak", + "Check a share": "Semak bahagian tandatangan", "Check Activity": "Semak Aktiviti", "Check Activity first": "Semak Aktiviti dahulu", "Check before saving": "Semak sebelum menyimpan", "Check fingerprint": "Semak cap jari", + "Check payments": "Semak pembayaran", + "Check proposal": "Semak cadangan", "Check settings": "Semak tetapan", + "Check share": "Semak bahagian tandatangan", "Checking it against the chain before anything is proved.": "Menyemaknya dengan rantaian sebelum apa-apa dibuktikan.", "Checking the pool and your notes…": "Menyemak kolam dan nota anda…", "Checking the swap…": "Menyemak pertukaran…", @@ -215,6 +237,7 @@ export default { "Continue": "Teruskan", "Copied": "Disalin", "Copied as JSON": "Disalin sebagai JSON", + "Copy": "Salin", "Copy address": "Salin alamat", "Copy as JSON": "Salin sebagai JSON", "Copy link": "Salin pautan", @@ -263,9 +286,13 @@ export default { "Enter an amount to send.": "Masukkan amaun untuk dihantar.", "Enter an amount.": "Masukkan amaun.", "Enter at least 10 characters": "Masukkan sekurang-kurangnya 10 aksara", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Masukkan setiap pembayaran yang anda ingin buat: alamat dan amaun dalam RAND. Pembayaran ke alamat yang sama dua kali dimasukkan sekali, dengan jumlahnya. Output yang tidak sepadan dengan mana-mana tidak boleh ditandatangani.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Masukkan alamat setiap penerima bayaran: output yang tidak sepadan dengan alamat yang anda masukkan, atau yang alamatnya kosong, tidak boleh ditandatangani.", + "Enter the address of every payment.": "Masukkan alamat setiap pembayaran.", "Enter the address this should be released to.": "Masukkan alamat yang patut menerima pelepasan ini.", "Enter the address you are sending to.": "Masukkan alamat yang anda hantar kepadanya.", "Enter the amount as a number, for example 1.25.": "Masukkan amaun sebagai nombor, contohnya 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Masukkan amaun setiap pembayaran: ia menghalang pemecahan atau pembayaran yang dibengkakkan.", "Enter the first 4 characters of your recovery key.": "Masukkan 4 aksara pertama kunci pemulihan anda.", "Enter the relayer fee as a number, for example 1.25.": "Masukkan yuran penyampai sebagai nombor, contohnya 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Masukkan kata laluan dompet ini — pasangan ini dimeterai dengannya.", @@ -275,6 +302,8 @@ export default { "Enter your recovery key.": "Masukkan kunci pemulihan anda.", "Epoch": "Epok", "Exactly those four letters, in capitals.": "Tepat empat huruf itu, dalam huruf besar.", + "expected recipient {address}: {reason}": "penerima dijangka {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients mestilah senarai alamat", "Explore": "Teroka", "Export spend key": "Eksport kunci perbelanjaan", "Exporting the spend key is behind your password.": "Mengeksport kunci perbelanjaan memerlukan kata laluan anda.", @@ -283,6 +312,7 @@ export default { "Fee": "Yuran", "fee must be at least {fee} RAND (the bridge burn floor)": "yuran mestilah sekurang-kurangnya {fee} RAND (lantai pembakaran jambatan)", "fee must be at least {fee} RAND (the bundle floor)": "yuran mestilah sekurang-kurangnya {fee} RAND (lantai berkas)", + "Fee: {fee} RAND · expires at height {height}": "Yuran: {fee} RAND · tamat tempoh pada ketinggian {height}", "Filter activity by asset": "Tapis aktiviti mengikut aset", "Fingerprint": "Cap jari", "fingerprint {fp}": "cap jari {fp}", @@ -320,9 +350,11 @@ export default { "I understand anyone with this key can spend my funds": "Saya faham sesiapa yang memiliki kunci ini boleh membelanjakan dana saya", "Import wallet": "Import dompet", "Incorrect password.": "Kata laluan salah.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "input memegang {held}, tetapi amaun + yuran ialah {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "input memegang {held}, tetapi yuran + deposit RAND ialah {need}", "insufficient balance: have {have}, need {need}": "baki tidak mencukupi: ada {have}, perlu {need}", + "Intended payments": "Pembayaran yang dimaksudkan", "is on chain {chain}, not {expected}": "berada di rantaian {chain}, bukan {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Ia tidak dapat melihat baki atau aktiviti anda, dan ia tidak dapat memindahkan apa-apa.", "It cannot spend.": "Ia tidak boleh berbelanja.", @@ -345,6 +377,7 @@ export default { "key file has no spend_key": "fail kunci tiada spend_key", "key file must be version 2": "fail kunci mestilah versi 2", "Key hidden — enter your password to view it again.": "Kunci disembunyikan — masukkan kata laluan anda untuk melihatnya semula.", + "Label": "Label", "Language": "Bahasa", "Leave empty to let the payer choose.": "Biarkan kosong untuk membenarkan pembayar memilih.", "Light": "Cerah", @@ -355,11 +388,15 @@ export default { "machines": "mesin", "malformed parameter {parameter}": "parameter {parameter} tidak sah", "Manage contacts": "Urus kenalan", + "Manage multisig signers": "Urus penandatangan multisig", "Max": "Maks", "Memo": "Memo", "memo \"{memo}\"": "memo \"{memo}\"", "memo is {n} bytes, at most {max}": "memo ialah {n} bait, paling banyak {max}", "More is on the way": "Lebih banyak sedang dalam perjalanan", + "Multisig share": "Bahagian tandatangan multisig", + "Multisig signers": "Penandatangan multisig", + "Multisig signing is not available in this app.": "Penandatanganan multisig tidak tersedia dalam aplikasi ini.", "My own prover · {name}": "Pembukti saya sendiri · {name}", "Name": "Nama", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nama untuk alamat yang sering anda bayar, disimpan pada peranti ini sahaja. Hantar kepada kenalan mengikut nama; cap jarinya disemak apabila anda menyimpannya.", @@ -383,12 +420,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Belum ada kenalan. Simpan satu dari Tetapan, atau tampal alamat.", "no endpoint could answer {method}": "tiada titik akhir dapat menjawab {method}", "no endpoint could answer {method}; tried {endpoints}": "tiada titik akhir dapat menjawab {method}; telah mencuba {endpoints}", + "no expected recipient given: the payee was NOT checked": "tiada penerima dijangka diberikan: penerima bayaran TIDAK disemak", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Tiada nod menjawab di {url}, jadi tempoh tamat tidak dapat disemak dan cadangan ini tidak ditandatangani. Cuba lagi apabila nod boleh dicapai.", "No proof is pending.": "Tiada bukti yang belum selesai.", "No prover": "Tiada pembukti", "No prover is paired.": "Tiada pembukti dipasangkan.", "No RAND to send yet": "Belum ada RAND untuk dihantar", "No registry assets": "Tiada aset daftar", "no reply": "tiada balasan", + "No signer profiles yet": "Belum ada profil penandatangan", + "No such signer profile.": "Tiada profil penandatangan sedemikian.", "No transaction or block matches that.": "Tiada transaksi atau blok yang sepadan dengannya.", "no wallet on this device": "tiada dompet pada peranti ini", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Tiada satu pun pautan pembukti RandProtocol terbina dalam menamakan kunci yang ditetapkan oleh dompet ini untuknya; pautan itu tidak digunakan.", @@ -435,9 +476,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Satu bukti berjalan pada peranti ini — kira-kira dua minit. Anda boleh melihat skrin lain; menutup dompet akan menghentikannya.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "hanya {locked} dikunci dalam syiling itu di rantaian {chain}; pilih sandaran lain atau amaun yang lebih kecil", "Open durian.market": "Buka durian.market", + "Open file": "Buka fail", "Open in explorer": "Buka dalam penjelajah", "Open in randscan": "Buka dalam randscan", "Open in side panel": "Buka dalam panel sisi", + "Open in the side panel to choose files": "Buka dalam panel sisi untuk memilih fail", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Buka Rand Wallet dengan {label} dan bukannya menaip kata laluan anda. Kata laluan anda masih berfungsi, dan kekal sebagai cara untuk memulihkan akses.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Buka dompet dengan {label} dan bukannya menaip kata laluan anda. Kata laluan anda masih berfungsi, dan kekal sebagai cara untuk memulihkan akses.", "Open this window from a site that asked to connect.": "Buka tetingkap ini dari laman yang meminta untuk bersambung.", @@ -447,6 +490,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "Atau gunakan pembukti RandProtocol — {n} mesin dikendalikan oleh pengesah, setiap satu dengan kuncinya sendiri.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Atau gunakan pembukti RandProtocol — mesin yang dikendalikan oleh pengesah, setiap satu dengan kuncinya sendiri.", "Outcome unknown": "Keputusan tidak diketahui", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "output {slot} tidak terbuka sebagai milik akaun ini: enggan menandatangani nilai yang tidak dapat dilihatnya", "own={value}, expected 1 or absent": "own={value}, dijangka 1 atau tiada", "Paid": "Dibayar", "Pair your own prover": "Pasangkan pembukti anda sendiri", @@ -467,17 +511,20 @@ export default { "Paste your recovery key": "Tampal kunci pemulihan anda", "path is not {n} levels": "laluan bukan {n} aras", "Pay me on Rand": "Bayar saya di Rand", + "Payee address": "Alamat penerima bayaran", "Payment link": "Pautan pembayaran", "Payment link · fingerprint {fingerprint}": "Pautan pembayaran · cap jari {fingerprint}", "Payment link copied": "Pautan pembayaran disalin", "Payment links cannot be read in this app; paste the address instead.": "Pautan pembayaran tidak dapat dibaca dalam aplikasi ini; tampal alamat sebagai gantinya.", "Payment links cannot be read in this app.": "Pautan pembayaran tidak dapat dibaca dalam aplikasi ini.", + "Payments changed: check them again before signing.": "Pembayaran berubah: semak semula sebelum menandatangani.", "Peers": "Rakan setara", "Pending": "Belum selesai", "Permission to reach that host was not granted, so the node was left as it was.": "Kebenaran untuk mencapai hos itu tidak diberikan, jadi nod dibiarkan seperti sedia ada.", "Permission to reach that host was not granted.": "Kebenaran untuk mencapai hos itu tidak diberikan.", "Permission to reach that prover was not granted, so nothing was saved.": "Kebenaran untuk mencapai pembukti itu tidak diberikan, jadi tiada apa-apa disimpan.", "Point the camera at a Rand QR code.": "Halakan kamera ke kod QR Rand.", + "Policy file": "Fail dasar", "Pool fee": "Yuran kolam", "Price impact": "Kesan harga", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Harga ditentukan oleh kolam durian.market di rantaian ini (yuran kolam {pct}%). Pertukaran ini ialah transaksi yang dibuktikan dan dihantar oleh dompet ini sendiri.", @@ -487,6 +534,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Bukti dibuat pada peranti ini sahaja. Jika ia tidak dapat membuatnya, penghantaran menunggu sehingga anda memasangkan pembukti atau menggunakan pembukti RandProtocol semula.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Bukti yang tidak dapat dibuat oleh peranti ini dihantar kepada {name}. Cap jarinya ialah {fingerprint} — semak bahawa pembukti menunjukkan yang sama.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Bukti yang tidak dapat dibuat oleh peranti ini dihantar kepada salah satu daripadanya; setiap satu yang membuktikan penghantaran dapat melihat kunci tontonan dompet itu.", + "Proposal (proposal.json)": "Cadangan (proposal.json)", "Prove and send": "Buktikan dan hantar", "Prove for my other devices": "Buktikan untuk peranti saya yang lain", "Prover": "Pembukti", @@ -535,6 +583,7 @@ export default { "Relayer fee (optional)": "Yuran penyampai (pilihan)", "Remove": "Buang", "Remove {name}": "Buang {name}", + "Remove this signer": "Buang penandatangan ini", "Request RAND": "Minta RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Minta RAND daripada faucet untuk mencuba menghantar, menerima dan membuktikan di rangkaian ini.", "Rescan": "Imbas semula", @@ -569,8 +618,12 @@ export default { "Running on": "Berjalan pada", "Save": "Simpan", "Save an address you pay often, and send to it by name.": "Simpan alamat yang sering anda bayar, dan hantar kepadanya mengikut nama.", + "Save as file": "Simpan sebagai fail", + "Save signer": "Simpan penandatangan", "Save your recovery key": "Simpan kunci pemulihan anda", "Saved": "Disimpan", + "Saved to {path}": "Disimpan ke {path}", + "Saved.": "Disimpan.", "Scan": "Imbas", "Scan a payment link": "Imbas pautan pembayaran", "Scan a QR code": "Imbas kod QR", @@ -591,6 +644,7 @@ export default { "Sent.": "Dihantar.", "Set on this device when this wallet was created.": "Ditetapkan pada peranti ini semasa dompet ini dicipta.", "Settings": "Tetapan", + "Share": "Bahagian tandatangan", "Share payment link": "Kongsi pautan pembayaran", "shielded address decodes to {n} bytes, expected {expected}": "alamat terlindung dinyahkod kepada {n} bait, dijangka {expected}", "shielded address is not base58": "alamat terlindung bukan base58", @@ -602,6 +656,12 @@ export default { "Show the full address": "Tunjukkan alamat penuh", "Show viewing key": "Tunjukkan kunci tontonan", "Show viewing key for 10 seconds": "Tunjukkan kunci tontonan selama 10 saat", + "Sign": "Tandatangan", + "Sign a proposal": "Tandatangani cadangan", + "signer {index} is not in this policy (it has {count} signers)": "penandatangan {index} tiada dalam dasar ini (ia mempunyai {count} penandatangan)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Bahagian tandatangan penandatangan {index} sah. {have} daripada {threshold} sudah ada: penandatangan {list}.", + "signer {index}'s signature does not verify over this proposal": "tandatangan penandatangan {index} tidak sah untuk cadangan ini", + "Signer key file, or its 64-hex seed": "Fail kunci penandatangan, atau seed 64-hex-nya", "So the address above is one you have actually read.": "Supaya alamat di atas benar-benar telah anda baca.", "Something went wrong": "Berlaku ralat", "Something went wrong.": "Berlaku ralat.", @@ -634,6 +694,7 @@ export default { "That could not be copied.": "Itu tidak dapat disalin.", "That could not be saved.": "Itu tidak dapat disimpan.", "That does not match — reveal the key again and check.": "Itu tidak sepadan — dedahkan kunci sekali lagi dan semak.", + "That file is larger than a proposal, a policy or a key file can be.": "Fail itu lebih besar daripada yang boleh bagi sesebuah cadangan, dasar atau fail kunci.", "That is more than your balance of {balance} {symbol}.": "Itu melebihi baki anda sebanyak {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Itu bukan 64 aksara heks (cincangan) dan bukan ketinggian blok (digit sahaja).", "That is not a pairing link.": "Itu bukan pautan pasangan.", @@ -669,6 +730,8 @@ export default { "The fee could not be worked out.": "Yuran tidak dapat dikira.", "the genesis hash is not hex of a plausible length": "cincangan genesis bukan heks dengan panjang yang munasabah", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "aliran masuk ialah `none` tepat apabila berkas tidak membakar sebarang token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "alamat fail kunci bukan yang diterbitkan oleh seed-nya", + "the key file's public_key is not the one its seed derives": "public_key fail kunci bukan yang diterbitkan oleh seed-nya", "The key you exported when this wallet was created.": "Kunci yang anda eksport semasa dompet ini dicipta.", "The language could not be saved.": "Bahasa tidak dapat disimpan.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Pautan meminta {linked} {symbol}; anda menaip {typed} {symbol}.", @@ -709,6 +772,7 @@ export default { "The password that unlocks this wallet on this device.": "Kata laluan yang membuka kunci dompet ini pada peranti ini.", "The payer decides": "Pembayar yang menentukan", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "Bukti yang belum selesai telah dikosongkan (dompet dikunci, atau ia dibatalkan di tempat lain), jadi tiada apa-apa dihantar. Hantar semula.", + "the policy does not parse: {reason}": "dasar tidak dapat dihuraikan: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Kolam telah berubah sejak halaman ini membacanya. Tiada apa-apa dihantar; laman boleh memberikan sebut harga semula.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Kolam berubah semasa ini sedang dibuktikan. Tiada apa-apa dihantar; laman boleh memberikan sebut harga semula.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Kolam tidak memegang cukup untuk membayar ini. Tiada apa-apa dihantar; laman boleh memberikan sebut harga semula.", @@ -719,6 +783,12 @@ export default { "the proof does not verify: {reason}": "bukti tidak dapat disahkan: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "bukti menerbitkan ringkasan yang tidak dibina oleh dompet ini ({published} bagi {expected}); tidak menggunakannya", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "Bukti berjalan pada peranti ini. Anda boleh melihat skrin lain — ia terus berjalan — tetapi menutup dompet akan menghentikannya.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Semakan cadangan memulangkan jawapan yang tidak lengkap, jadi cadangan ini tidak ditandatangani.", + "the proposal does not parse: {reason}": "cadangan tidak dapat dihuraikan: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "cadangan tamat tempoh pada {expires}, di luar tetingkap bundle-nya ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "auth_commit cadangan bukan akaun dasar ini", + "the proposal's expected digest is not its bundle's": "digest dijangka cadangan bukan milik bundle-nya", + "the proposal's signing_message is not its binding's": "signing_message cadangan bukan milik binding-nya", "the prover": "pembukti", "the prover accepted the job but named no job id": "pembukti menerima kerja tetapi tidak menamakan id kerja", "The prover answered with an unknown state ({state}).": "Pembukti menjawab dengan keadaan yang tidak diketahui ({state}).", @@ -780,6 +850,7 @@ export default { "The request was not approved in Rand Wallet.": "Permintaan tidak diluluskan dalam Rand Wallet.", "The rescan could not be started.": "Imbasan semula tidak dapat dimulakan.", "The rescan did not finish": "Imbasan semula tidak selesai", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "profil penandatangan yang disimpan tidak dapat dibuka; tiada apa yang diubah. Pulihkan storan peranti ini, atau padam dompet dan pulihkannya daripada kuncinya", "The scan was cancelled.": "Imbasan telah dibatalkan.", "The side panel could not be opened.": "Panel sisi tidak dapat dibuka.", "The site sent an invoke this wallet cannot read: {reason}.": "Laman menghantar invoke yang tidak dapat dibaca oleh dompet ini: {reason}.", @@ -790,6 +861,10 @@ export default { "The swap did not go through.": "Pertukaran tidak berjaya.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Pertukaran terus berjalan. Menutup dompet sebelum ia menyatakan dihantar akan menghentikannya, dan tiada apa-apa hilang.", "The theme could not be saved.": "Tema tidak dapat disimpan.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transaksi membawa satu tindakan; cadangan multisig persendirian ialah pembayaran daripada bundle-nya sahaja", + "the transaction carries no bundle": "transaksi tidak membawa bundle", + "the transaction does not hash to the proposal's binding": "hash transaksi bukan binding cadangan", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "transaksi ini untuk rantaian {tx}, cadangan menyebut rantaian {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "Kunci transaksi mendedahkan pembayaran yang satu ini — amaun dan penerimanya — kepada sesiapa yang anda berikan. Ia tidak boleh membelanjakan apa-apa.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Transaksi mungkin sudah berada di rantaian. Semak Aktiviti sebelum mengeluarkan semula — mengeluarkan dua kali akan membakar dua kali.", "The transfer could not be proved.": "Pemindahan tidak dapat dibuktikan.", @@ -807,6 +882,9 @@ export default { "The wallet was told: {message}": "Dompet diberitahu: {message}", "The withdrawal could not be proved.": "Pengeluaran tidak dapat dibuktikan.", "The withdrawal was submitted, but the node did not return a usable hash.": "Pengeluaran telah diserahkan, tetapi nod tidak memulangkan cincangan yang boleh digunakan.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "nilai witness tidak dapat disemak: nk atau salt-nya bukan milik cadangan ini", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "nilai witness tidak sepadan dengan output: aset {asset} mempunyai {held} masuk dan {spent} keluar dengan yuran dan pembakaran", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "nilai witness tidak sepadan dengan transaksi: input {input} tidak menjadi nullifier bundle", "Theme": "Tema", "There is no pool for that pair.": "Tiada kolam untuk pasangan itu.", "There is no program {program}… on this chain. Nothing was sent.": "Tiada program {program}… di rantaian ini. Tiada apa-apa dihantar.", @@ -848,6 +926,7 @@ export default { "This device, or where it cannot · {name} provers": "Peranti ini, atau jika tidak dapat · pembukti {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Ini melupakan nota dan sejarah yang dibaca dari rantaian lama dan membaca nod ini dari awal. Kunci dan kata laluan anda tidak disentuh.", "This key can spend your funds": "Kunci ini boleh membelanjakan dana anda", + "this key is not one of the policy's signers": "kunci ini bukan salah satu penandatangan dasar", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Kunci ini ialah dompet anda. Sesiapa yang memilikinya boleh membelanjakan dana anda, dan tanpanya peranti yang hilang bermakna dana yang hilang. Anda boleh melihatnya semula dalam Tetapan dengan kata laluan anda.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Ini keluar dari kolam terlindung. Alamat destinasi dan amaun menjadi awam di rantaian yang satu lagi.", "This link is too long for a QR code — copy it instead.": "Pautan ini terlalu panjang untuk kod QR — salin sahaja.", @@ -863,6 +942,10 @@ export default { "This note was not found.": "Nota ini tidak ditemui.", "This passkey does not support the PRF extension.": "Kunci laluan ini tidak menyokong sambungan PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Bukti ini sudah diserahkan dari tetingkap lain. Semak Aktiviti sebelum menghantar semula.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Cadangan ini tamat tempoh pada ketinggian {expires} dan rantaian berada di {head}: terlambat untuk menandatangani. Minta cadangan baharu.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Cadangan ini terikat pada genesis yang berbeza daripada yang dilayani nod dompet ini.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "cadangan ini untuk dasar lain: ia menyebut {theirs}, dasar ini ialah {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Cadangan ini untuk rantaian {theirs}; dompet ini berada pada rantaian {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "pembukti ini mengenakan {fee} RAND bagi setiap bukti, dan versi dompet ini tidak membayar yuran pembukti: pasangkan pembukti yang tidak mengenakan bayaran, atau buat bukti pada peranti ini", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Pembukti ini mengenakan yuran, yang tidak dibayar oleh versi dompet ini. Pasangkan pembukti yang tidak mengenakan bayaran, atau hantar dari aplikasi desktop.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Pembukti ini tidak menerima kerja jenis ini ({reason}). Pasangkan pembukti lain dalam Tetapan, atau hantar dari aplikasi desktop.", @@ -874,6 +957,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Ini mengalih keluar dompet daripada peranti ini. Anda memerlukan kunci pemulihan untuk memulihkannya.", "This screen could not be shown.": "Skrin ini tidak dapat dipaparkan.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Skrin ini akan disediakan dalam tugasan kemudian. Dompet anda sudah dibuka kunci dan sedia.", + "this signer is already saved as {label}": "penandatangan ini sudah disimpan sebagai {label}", + "This signer profile has no key on this device: remove it and add it again.": "Profil penandatangan ini tiada kunci pada peranti ini: buangnya dan tambah semula.", "This tab is showing what it has so far, and will refresh when that finishes.": "Tab ini memaparkan apa yang ada setakat ini, dan akan dimuat semula apabila proses itu selesai.", "This takes a few minutes; keep this window open.": "Ini mengambil masa beberapa minit; biarkan tetingkap ini terbuka.", "This transaction was not found.": "Transaksi ini tidak ditemui.", @@ -920,6 +1005,7 @@ export default { "Type {word} to confirm": "Taip {word} untuk mengesahkan", "Type it once more.": "Taip sekali lagi.", "Type the last four characters of that address": "Taip empat aksara terakhir alamat itu", + "Type the last four characters of the account fingerprint": "Taip empat aksara terakhir cap jari akaun", "unavailable in this app": "tidak tersedia dalam aplikasi ini", "unknown": "tidak diketahui", "unknown parameter {name}": "parameter tidak diketahui {name}", @@ -956,6 +1042,7 @@ export default { "Weak": "Lemah", "Welcome back": "Selamat kembali", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Apa yang dibayar oleh kolam kepada anda tiba sebagai nota yang ditemui oleh dompet anda pada penyegerakan seterusnya.", + "What this proposal does, as checked against your policy": "Apa yang cadangan ini lakukan, disemak terhadap dasar anda", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Jika peranti ini tidak dapat membuat bukti, pasangkan pembukti. Kunci perbelanjaan anda tetap di sini dalam apa jua keadaan; pembukti yang anda jalankan sendiri — aplikasi desktop, atau rand-prover pada mesin anda sendiri — juga merahsiakan sejarah anda.", "Where to?": "Ke mana?", "Which address?": "Alamat yang mana?", @@ -1002,6 +1089,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "Pembukti anda belum selesai selepas {n} minit membuat bukti. Sambung semula kemudian, atau batalkan.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "Pembukti anda belum memulakan bukti ini selepas {n} minit dalam barisannya. Sambung semula kemudian, atau batalkan.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Pasangan pembukti anda tidak dapat dibuka. Kunci dan buka kunci dompet, atau pasangkan pembukti semula dalam Tetapan.", + "Your share — send it to whoever proves the proposal": "Bahagian tandatangan anda — hantar kepada yang membuktikan cadangan", "Your wallet picks up what it pays you on its next sync.": "Dompet anda menerima apa yang dibayar kepada anda pada penyegerakan seterusnya.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "Sejarah dompet anda dibaca dari {chain} — tiada apa-apa yang diubah, dan dompet ini tidak akan menghantar atau meminta dana sehingga kedua-duanya sepadan. Tukar nod dalam Tetapan, atau imbas semula dompet ini untuk rantaian baharu.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Itu alamat pembukti {name}. Ia tidak perlu dipasangkan: dompet ini menggunakannya secara lalai.", diff --git a/ui/locales/pl.js b/ui/locales/pl.js index c46dfd8..4be1fc1 100644 --- a/ui/locales/pl.js +++ b/ui/locales/pl.js @@ -4,6 +4,7 @@ // odzyskiwania, most, wypłata, wymiana, pula, kranik, odcisk. Plurals: one, few, many, other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Zostało {blocks} bloków, aby to udowodnić.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} peer", few: "{count} peery", many: "{count} peerów", other: "{count} peera" }, "{count} peers": { one: "{count} peer", few: "{count} peery", many: "{count} peerów", other: "{count} peera" }, @@ -32,6 +33,7 @@ export default { "{label} is off": "{label}: wyłączone", "{label} is on": "{label}: włączone", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} nie otwiera już tego portfela (zmieniono hasło). Odblokuj go hasłem, a potem ponownie włącz {label} w Ustawieniach.", + "{matched} of {total} payments matched.": "Zgodnych płatności: {matched} z {total}.", "{member} answered with another key than the one this wallet pins": "{member} odpowiedział innym kluczem niż ten, który przypisuje mu ten portfel", "{member} did not answer": "{member} nie odpowiedział", "{member} does not take this wallet's jobs": "{member} nie przyjmuje zadań od tego portfela", @@ -52,6 +54,8 @@ export default { "{symbol} cannot be withdrawn": "Nie można wypłacić {symbol}", "{symbol} has {decimals} decimal places — that is more.": "{symbol} ma {decimals} miejsc po przecinku — tu jest więcej.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} zostanie wydany jako wybrana przez ciebie moneta, w jej własnym łańcuchu.", + "{threshold} of {count} · signer {index}": "{threshold} z {count} · sygnatariusz {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} z {count} · ten klucz to sygnatariusz {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} w łańcuchu {chain} ma {decimals} miejsc po przecinku: kwota i opłata dla przekaźnika muszą być wielokrotnościami {unit}", "{units} units of asset {asset}": "{units} jednostek aktywa {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} odpowiedział pierwszy — łańcuch {chain} · wysokość {height}.", @@ -84,7 +88,12 @@ export default { "A prover address must be https://.": "Adres provera musi zaczynać się od https://.", "A prover is making the proof": "Prover generuje dowód", "A prover sees your history": "Prover widzi twoją historię", + "a share is index:hex": "udział podpisu ma postać index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Osłonięty portfel dla sieci Rand. Salda i historia pozostają prywatne — dowód każdej wysyłki powstaje na tym urządzeniu.", + "a signer key is a rand-node keygen file or 64 hex characters": "klucz sygnatariusza to plik keygen rand-node lub 64 znaki szesnastkowe", + "a signer label is 1-64 characters": "etykieta sygnatariusza ma 1-64 znaki", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Profil sygnatariusza jest przechowywany w zaszyfrowanym sejfie tego portfela. Każdy podpis ponownie pyta o hasło.", + "a signer seed is 64 hex characters": "seed sygnatariusza to 64 znaki szesnastkowe", "A site": "Strona", "a spend key is 64 hex characters, or a wallet.key.json": "klucz wydawania to 64 znaki szesnastkowe albo plik wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Najpierw sprawdzany jest hash transakcji; jeśli nic nie zostanie znalezione, ten sam hash jest sprawdzany jako blok. Same cyfry wyszukują blok według wysokości.", @@ -101,8 +110,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Wypłata do łańcucha, którego nie wymienia, może zostać odrzucona. Nic nie zostanie wydane, dopóki nie powstanie dowód.", "About": "Informacje", "about {left} left": "zostało około {left}", + "Account fingerprint — compare it with every other signer before you save": "Odcisk konta — porównaj go z odciskiem każdego innego sygnatariusza, zanim zapiszesz", "Activity": "Aktywność", "Add a contact": "Dodaj kontakt", + "Add a signer": "Dodaj sygnatariusza", + "Add payment": "Dodaj płatność", + "Add the policy file and your signer key to sign for a private multisig account.": "Dodaj plik polityki i swój klucz sygnatariusza, aby podpisywać dla prywatnego konta multisig.", "Address": "Adres", "Address copied": "Skopiowano adres", "Address or payment link": "Adres lub link płatności", @@ -111,14 +124,18 @@ export default { "almost done": "prawie gotowe", "also tried {endpoints}": "próbowano też {endpoints}", "Amount": "Kwota", + "Amount in RAND": "Kwota w RAND", "amount must be greater than zero": "kwota musi być większa od zera", "amount overflow": "przepełnienie kwoty", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Kwot nie można sprawdzić dla płatności innych niż w RAND: zostaw dla nich kwotę pustą.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Ten odbiorca otrzymuje zarówno RAND, jak i token: płatności w RAND i w tokenach do jednego odbiorcy wymagają osobnych propozycji.", "amounts overflow": "przepełnienie sumy kwot", "an action is {n} characters, more than {max}": { one: "akcja ma {n} znak, więcej niż {max}", few: "akcja ma {n} znaki, więcej niż {max}", many: "akcja ma {n} znaków, więcej niż {max}", other: "akcja ma {n} znaku, więcej niż {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Adres (rand1…), link płatności (randpay:…) lub nazwa zapisanego kontaktu.", "An address here is hexadecimal — 0-9 and a-f only.": "Adres jest tu szesnastkowy — tylko 0-9 i a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Adres w tym łańcuchu ma 20 bajtów — 40 znaków szesnastkowych. Ten ma {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Adres w tym łańcuchu ma 32 bajty — 64 znaki szesnastkowe. Ten ma {length}.", + "An amount is a number of RAND, such as 1.5.": "Kwota to liczba RAND, na przykład 1.5.", "an unknown chain": "nieznany łańcuch", "another tab changed this wallet while it was syncing; try again": "inna karta zmieniła ten portfel podczas synchronizacji; spróbuj ponownie", "Another tab is syncing": "Inna karta synchronizuje", @@ -185,11 +202,16 @@ export default { "Chains known": "Znane łańcuchy", "Change back to you": "Reszta wraca do ciebie", "Change it": "Zmień", + "Check": "Sprawdź", + "Check a share": "Sprawdź udział podpisu", "Check Activity": "Sprawdź Aktywność", "Check Activity first": "Najpierw sprawdź Aktywność", "Check before saving": "Sprawdź przed zapisaniem", "Check fingerprint": "Sprawdź odcisk", + "Check payments": "Sprawdź płatności", + "Check proposal": "Sprawdź propozycję", "Check settings": "Sprawdź ustawienia", + "Check share": "Sprawdź udział podpisu", "Checking it against the chain before anything is proved.": "Sprawdzanie z łańcuchem, zanim powstanie jakikolwiek dowód.", "Checking the pool and your notes…": "Sprawdzanie puli i twoich not…", "Checking the swap…": "Sprawdzanie wymiany…", @@ -216,6 +238,7 @@ export default { "Continue": "Dalej", "Copied": "Skopiowano", "Copied as JSON": "Skopiowano jako JSON", + "Copy": "Kopiuj", "Copy address": "Kopiuj adres", "Copy as JSON": "Kopiuj jako JSON", "Copy link": "Kopiuj link", @@ -264,9 +287,13 @@ export default { "Enter an amount to send.": "Wpisz kwotę do wysłania.", "Enter an amount.": "Wpisz kwotę.", "Enter at least 10 characters": "Wpisz co najmniej 10 znaków", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Wpisz każdą płatność, którą zamierzasz wykonać: adres i kwotę w RAND. Płatność dwa razy na ten sam adres wpisuje się raz, z sumą. Wyjścia niepasującego do żadnej z nich nie można podpisać.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Wpisz adres każdego odbiorcy: wyjścia niepasującego do wpisanego adresu lub mającego pusty adres nie można podpisać.", + "Enter the address of every payment.": "Wpisz adres każdej płatności.", "Enter the address this should be released to.": "Wpisz adres, na który ma zostać wypłacone.", "Enter the address you are sending to.": "Wpisz adres, na który wysyłasz.", "Enter the amount as a number, for example 1.25.": "Wpisz kwotę jako liczbę, na przykład 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Wpisz kwotę każdej płatności: to ona chroni przed podziałem lub zawyżoną płatnością.", "Enter the first 4 characters of your recovery key.": "Wpisz pierwsze 4 znaki klucza odzyskiwania.", "Enter the relayer fee as a number, for example 1.25.": "Wpisz opłatę dla przekaźnika jako liczbę, na przykład 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Wpisz hasło tego portfela — parowanie jest nim zabezpieczone.", @@ -276,6 +303,8 @@ export default { "Enter your recovery key.": "Wpisz klucz odzyskiwania.", "Epoch": "Epoka", "Exactly those four letters, in capitals.": "Dokładnie te cztery litery, wielkimi literami.", + "expected recipient {address}: {reason}": "oczekiwany odbiorca {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients musi być listą adresów", "Explore": "Eksploruj", "Export spend key": "Eksportuj klucz wydawania", "Exporting the spend key is behind your password.": "Eksport klucza wydawania wymaga hasła.", @@ -284,6 +313,7 @@ export default { "Fee": "Opłata", "fee must be at least {fee} RAND (the bridge burn floor)": "opłata musi wynosić co najmniej {fee} RAND (minimum spalenia przez most)", "fee must be at least {fee} RAND (the bundle floor)": "opłata musi wynosić co najmniej {fee} RAND (minimum pakietu)", + "Fee: {fee} RAND · expires at height {height}": "Opłata: {fee} RAND · wygasa na wysokości {height}", "Filter activity by asset": "Filtruj aktywność według aktywa", "Fingerprint": "Odcisk", "fingerprint {fp}": "odcisk {fp}", @@ -321,9 +351,11 @@ export default { "I understand anyone with this key can spend my funds": "Rozumiem, że każdy, kto ma ten klucz, może wydać moje środki", "Import wallet": "Importuj portfel", "Incorrect password.": "Nieprawidłowe hasło.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "wejścia zawierają {held}, a kwota + opłata to {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "wejścia zawierają {held}, a opłata + wpłata RAND to {need}", "insufficient balance: have {have}, need {need}": "niewystarczające saldo: jest {have}, potrzeba {need}", + "Intended payments": "Zamierzone płatności", "is on chain {chain}, not {expected}": "jest w łańcuchu {chain}, a nie {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Nie widzi twojego salda ani aktywności i nie może niczego przenieść.", "It cannot spend.": "Nie może wydawać.", @@ -346,6 +378,7 @@ export default { "key file has no spend_key": "plik klucza nie zawiera spend_key", "key file must be version 2": "plik klucza musi mieć wersję 2", "Key hidden — enter your password to view it again.": "Klucz ukryty — wpisz hasło, aby zobaczyć go ponownie.", + "Label": "Etykieta", "Language": "Język", "Leave empty to let the payer choose.": "Zostaw puste, aby płacący sam wybrał.", "Light": "Jasny", @@ -356,11 +389,15 @@ export default { "machines": "maszyny", "malformed parameter {parameter}": "nieprawidłowy parametr {parameter}", "Manage contacts": "Zarządzaj kontaktami", + "Manage multisig signers": "Zarządzaj sygnatariuszami multisig", "Max": "Maks.", "Memo": "Memo", "memo \"{memo}\"": "memo „{memo}”", "memo is {n} bytes, at most {max}": { one: "memo ma {n} bajt, dozwolone najwyżej {max}", few: "memo ma {n} bajty, dozwolone najwyżej {max}", many: "memo ma {n} bajtów, dozwolone najwyżej {max}", other: "memo ma {n} bajta, dozwolone najwyżej {max}" }, "More is on the way": "Więcej jest w drodze", + "Multisig share": "Udział podpisu multisig", + "Multisig signers": "Sygnatariusze multisig", + "Multisig signing is not available in this app.": "Podpisywanie multisig nie jest dostępne w tej aplikacji.", "My own prover · {name}": "Mój własny prover · {name}", "Name": "Nazwa", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nazwy adresów, na które często płacisz, przechowywane tylko na tym urządzeniu. Wysyłaj do kontaktu po nazwie; jego odcisk jest sprawdzany przy zapisywaniu.", @@ -384,12 +421,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Brak kontaktów. Zapisz kontakt w Ustawieniach lub wklej adres.", "no endpoint could answer {method}": "żaden punkt końcowy nie odpowiedział na {method}", "no endpoint could answer {method}; tried {endpoints}": "żaden punkt końcowy nie odpowiedział na {method}; próbowano {endpoints}", + "no expected recipient given: the payee was NOT checked": "nie podano oczekiwanego odbiorcy: odbiorca NIE został sprawdzony", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Żaden węzeł nie odpowiedział pod {url}, więc nie można sprawdzić wygaśnięcia i ta propozycja nie zostaje podpisana. Spróbuj ponownie, gdy węzeł będzie osiągalny.", "No proof is pending.": "Żaden dowód nie jest w toku.", "No prover": "Brak provera", "No prover is paired.": "Nie sparowano żadnego provera.", "No RAND to send yet": "Brak RAND do wysłania", "No registry assets": "Brak aktywów w rejestrze", "no reply": "brak odpowiedzi", + "No signer profiles yet": "Brak profili sygnatariuszy", + "No such signer profile.": "Nie ma takiego profilu sygnatariusza.", "No transaction or block matches that.": "Żadna transakcja ani blok nie pasuje.", "no wallet on this device": "brak portfela na tym urządzeniu", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Żaden z wbudowanych linków do proverów RandProtocol nie wskazuje klucza, który ten portfel do niego przypisuje; nie są używane.", @@ -436,9 +477,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Jeden dowód powstaje na tym urządzeniu — około dwóch minut. Możesz przeglądać inne ekrany; zamknięcie portfela go przerywa.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "w tej monecie w łańcuchu {chain} zablokowano tylko {locked}; wybierz inne zabezpieczenie lub mniejszą kwotę", "Open durian.market": "Otwórz durian.market", + "Open file": "Otwórz plik", "Open in explorer": "Otwórz w eksploratorze", "Open in randscan": "Otwórz w randscan", "Open in side panel": "Otwórz w panelu bocznym", + "Open in the side panel to choose files": "Otwórz w panelu bocznym, aby wybrać pliki", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Otwieraj Rand Wallet przez: {label} zamiast wpisywać hasło. Hasło nadal działa i pozostaje sposobem na odzyskanie dostępu.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Otwieraj portfel przez: {label} zamiast wpisywać hasło. Hasło nadal działa i pozostaje sposobem na odzyskanie dostępu.", "Open this window from a site that asked to connect.": "Otwórz to okno ze strony, która poprosiła o połączenie.", @@ -448,6 +491,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Albo użyj proverów RandProtocol — {n} maszyna prowadzona przez walidatorów, z własnym kluczem.", few: "Albo użyj proverów RandProtocol — {n} maszyny prowadzone przez walidatorów, każda z własnym kluczem.", many: "Albo użyj proverów RandProtocol — {n} maszyn prowadzonych przez walidatorów, każda z własnym kluczem.", other: "Albo użyj proverów RandProtocol — {n} maszyny prowadzonej przez walidatorów, z własnym kluczem." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Albo użyj proverów RandProtocol — maszyn prowadzonych przez walidatorów, każda z własnym kluczem.", "Outcome unknown": "Wynik nieznany", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "wyjście {slot} nie otwiera się jako należące do tego konta: odmowa podpisania wartości, której nie widzi", "own={value}, expected 1 or absent": "own={value}, oczekiwano 1 lub braku", "Paid": "Zapłacono", "Pair your own prover": "Sparuj własny prover", @@ -468,17 +512,20 @@ export default { "Paste your recovery key": "Wklej klucz odzyskiwania", "path is not {n} levels": { one: "ścieżka nie ma {n} poziomu", few: "ścieżka nie ma {n} poziomów", many: "ścieżka nie ma {n} poziomów", other: "ścieżka nie ma {n} poziomu" }, "Pay me on Rand": "Zapłać mi w Rand", + "Payee address": "Adres odbiorcy", "Payment link": "Link płatności", "Payment link · fingerprint {fingerprint}": "Link płatności · odcisk {fingerprint}", "Payment link copied": "Skopiowano link płatności", "Payment links cannot be read in this app; paste the address instead.": "Ta aplikacja nie odczytuje linków płatności; wklej zamiast tego adres.", "Payment links cannot be read in this app.": "Ta aplikacja nie odczytuje linków płatności.", + "Payments changed: check them again before signing.": "Płatności się zmieniły: sprawdź je ponownie przed podpisaniem.", "Peers": "Peery", "Pending": "W toku", "Permission to reach that host was not granted, so the node was left as it was.": "Nie udzielono zgody na połączenie z tym hostem, więc węzeł pozostał bez zmian.", "Permission to reach that host was not granted.": "Nie udzielono zgody na połączenie z tym hostem.", "Permission to reach that prover was not granted, so nothing was saved.": "Nie udzielono zgody na połączenie z tym proverem, więc nic nie zapisano.", "Point the camera at a Rand QR code.": "Skieruj aparat na kod QR Rand.", + "Policy file": "Plik polityki", "Pool fee": "Opłata puli", "Price impact": "Wpływ na cenę", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Wycena według pul durian.market w tym łańcuchu (opłata puli {pct}%). Wymiana to transakcja, której dowód ten portfel generuje i którą wysyła sam.", @@ -488,6 +535,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Dowody powstają tylko na tym urządzeniu. Gdy nie może go wygenerować, wysyłka czeka, aż sparujesz prover lub znów użyjesz provera RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Dowody, których to urządzenie nie może wygenerować, trafiają do {name}. Jego odcisk to {fingerprint} — sprawdź, czy prover pokazuje ten sam.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Dowody, których to urządzenie nie może wygenerować, trafiają do jednego z nich; każdy, który generuje dowód wysyłki, widzi klucz podglądu portfela.", + "Proposal (proposal.json)": "Propozycja (proposal.json)", "Prove and send": "Wygeneruj dowód i wyślij", "Prove for my other devices": "Generuj dowody dla moich innych urządzeń", "Prover": "Prover", @@ -536,6 +584,7 @@ export default { "Relayer fee (optional)": "Opłata dla przekaźnika (opcjonalna)", "Remove": "Usuń", "Remove {name}": "Usuń {name}", + "Remove this signer": "Usuń tego sygnatariusza", "Request RAND": "Poproś o RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Poproś kranik o RAND, aby wypróbować wysyłanie, odbieranie i generowanie dowodów w tej sieci.", "Rescan": "Skanuj ponownie", @@ -570,8 +619,12 @@ export default { "Running on": "Działa na", "Save": "Zapisz", "Save an address you pay often, and send to it by name.": "Zapisz adres, na który często płacisz, i wysyłaj na niego po nazwie.", + "Save as file": "Zapisz jako plik", + "Save signer": "Zapisz sygnatariusza", "Save your recovery key": "Zapisz klucz odzyskiwania", "Saved": "Zapisano", + "Saved to {path}": "Zapisano w {path}", + "Saved.": "Zapisano.", "Scan": "Skanuj", "Scan a payment link": "Zeskanuj link płatności", "Scan a QR code": "Zeskanuj kod QR", @@ -592,6 +645,7 @@ export default { "Sent.": "Wysłano.", "Set on this device when this wallet was created.": "Ustawione na tym urządzeniu przy tworzeniu portfela.", "Settings": "Ustawienia", + "Share": "Udział podpisu", "Share payment link": "Udostępnij link płatności", "shielded address decodes to {n} bytes, expected {expected}": { one: "osłonięty adres dekoduje się do {n} bajtu, oczekiwano {expected}", few: "osłonięty adres dekoduje się do {n} bajtów, oczekiwano {expected}", many: "osłonięty adres dekoduje się do {n} bajtów, oczekiwano {expected}", other: "osłonięty adres dekoduje się do {n} bajta, oczekiwano {expected}" }, "shielded address is not base58": "osłonięty adres nie jest w base58", @@ -603,6 +657,12 @@ export default { "Show the full address": "Pokaż pełny adres", "Show viewing key": "Pokaż klucz podglądu", "Show viewing key for 10 seconds": "Pokaż klucz podglądu na 10 sekund", + "Sign": "Podpisz", + "Sign a proposal": "Podpisz propozycję", + "signer {index} is not in this policy (it has {count} signers)": "sygnatariusza {index} nie ma w tej polityce (ma ona {count} sygnatariuszy)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Udział podpisu sygnatariusza {index} jest poprawny. Masz {have} z {threshold}: sygnatariusze {list}.", + "signer {index}'s signature does not verify over this proposal": "podpis sygnatariusza {index} nie jest poprawny dla tej propozycji", + "Signer key file, or its 64-hex seed": "Plik klucza sygnatariusza lub jego seed w 64 znakach hex", "So the address above is one you have actually read.": "Aby mieć pewność, że adres powyżej został naprawdę przeczytany.", "Something went wrong": "Coś poszło nie tak", "Something went wrong.": "Coś poszło nie tak.", @@ -635,6 +695,7 @@ export default { "That could not be copied.": "Nie udało się tego skopiować.", "That could not be saved.": "Nie udało się tego zapisać.", "That does not match — reveal the key again and check.": "To się nie zgadza — pokaż klucz ponownie i sprawdź.", + "That file is larger than a proposal, a policy or a key file can be.": "Ten plik jest większy, niż może być propozycja, polityka lub plik klucza.", "That is more than your balance of {balance} {symbol}.": "To więcej niż twoje saldo {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "To nie jest ani 64 znaki szesnastkowe (hash), ani wysokość bloku (same cyfry).", "That is not a pairing link.": "To nie jest link parowania.", @@ -670,6 +731,8 @@ export default { "The fee could not be worked out.": "Nie udało się obliczyć opłaty.", "the genesis hash is not hex of a plausible length": "hash genesis nie jest ciągiem szesnastkowym o wiarygodnej długości", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow ma wartość `none` dokładnie wtedy, gdy pakiet nie spala żadnego tokena (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "adres z pliku klucza nie jest tym, który wyprowadza jego seed", + "the key file's public_key is not the one its seed derives": "public_key z pliku klucza nie jest tym, który wyprowadza jego seed", "The key you exported when this wallet was created.": "Klucz wyeksportowany przy tworzeniu tego portfela.", "The language could not be saved.": "Nie udało się zapisać języka.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Link prosi o {linked} {symbol}; wpisano {typed} {symbol}.", @@ -710,6 +773,7 @@ export default { "The password that unlocks this wallet on this device.": "Hasło, które odblokowuje ten portfel na tym urządzeniu.", "The payer decides": "Decyduje płacący", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "Oczekujący dowód został wyczyszczony (portfel się zablokował lub dowód anulowano gdzie indziej), więc nic nie zostało wysłane. Wyślij ponownie.", + "the policy does not parse: {reason}": "nie można odczytać polityki: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Pula zmieniła się od czasu, gdy strona ją odczytała. Nic nie zostało wysłane; strona może podać nową wycenę.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Pula zmieniła się podczas generowania dowodu. Nic nie zostało wysłane; strona może podać nową wycenę.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "Pula nie ma dość środków, aby to wypłacić. Nic nie zostało wysłane; strona może podać nową wycenę.", @@ -720,6 +784,12 @@ export default { "the proof does not verify: {reason}": "dowód nie przechodzi weryfikacji: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "dowód opublikował skrót, którego ten portfel nie zbudował ({published} zamiast {expected}); nie jest używany", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "Dowód powstaje na tym urządzeniu. Możesz przeglądać inne ekrany — proces trwa dalej — ale zamknięcie portfela go przerywa.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Sprawdzenie propozycji zwróciło niepełną odpowiedź, więc ta propozycja nie zostaje podpisana.", + "the proposal does not parse: {reason}": "nie można odczytać propozycji: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "propozycja wygasa na {expires}, poza oknem swojego bundle ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "auth_commit propozycji nie jest kontem tej polityki", + "the proposal's expected digest is not its bundle's": "oczekiwany digest propozycji nie jest digestem jej bundle", + "the proposal's signing_message is not its binding's": "signing_message propozycji nie jest signing_message jej powiązania", "the prover": "prover", "the prover accepted the job but named no job id": "prover przyjął zadanie, ale nie podał jego identyfikatora", "The prover answered with an unknown state ({state}).": "Prover odpowiedział nieznanym stanem ({state}).", @@ -781,6 +851,7 @@ export default { "The request was not approved in Rand Wallet.": "Żądanie nie zostało zatwierdzone w Rand Wallet.", "The rescan could not be started.": "Nie udało się rozpocząć ponownego skanowania.", "The rescan did not finish": "Ponowne skanowanie nie zostało ukończone", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "zapisanych profili sygnatariuszy nie udało się otworzyć; nic nie zmieniono. Przywróć pamięć tego urządzenia albo wyczyść portfel i przywróć go z jego klucza", "The scan was cancelled.": "Skanowanie zostało anulowane.", "The side panel could not be opened.": "Nie udało się otworzyć panelu bocznego.", "The site sent an invoke this wallet cannot read: {reason}.": "Strona wysłała wywołanie, którego ten portfel nie umie odczytać: {reason}.", @@ -791,6 +862,10 @@ export default { "The swap did not go through.": "Wymiana nie powiodła się.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Wymiana trwa dalej. Zamknięcie portfela, zanim pojawi się „wysłano”, przerywa ją, ale nic nie zostanie utracone.", "The theme could not be saved.": "Nie udało się zapisać motywu.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "transakcja zawiera akcję; prywatna propozycja multisig to płatność wyłącznie z jej bundle", + "the transaction carries no bundle": "transakcja nie zawiera bundle", + "the transaction does not hash to the proposal's binding": "skrót transakcji nie jest powiązaniem propozycji", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "transakcja jest dla łańcucha {tx}, propozycja wskazuje łańcuch {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "Klucz transakcji ujawnia tę jedną płatność — jej kwotę i odbiorcę — każdemu, komu go przekażesz. Nie pozwala niczego wydać.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Transakcja może już być w łańcuchu. Sprawdź Aktywność przed ponowną wypłatą — dwukrotna wypłata oznacza dwukrotne spalenie.", "The transfer could not be proved.": "Nie udało się wygenerować dowodu przelewu.", @@ -808,6 +883,9 @@ export default { "The wallet was told: {message}": "Portfel otrzymał komunikat: {message}", "The withdrawal could not be proved.": "Nie udało się wygenerować dowodu wypłaty.", "The withdrawal was submitted, but the node did not return a usable hash.": "Wypłata została przesłana, ale węzeł nie zwrócił użytecznego hasha.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "wartości witness nie można sprawdzić: jego nk lub salt nie pochodzi z tej propozycji", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "wartość witness nie zgadza się z wyjściami: aktywo {asset} ma {held} na wejściu i {spent} na wyjściu z opłatą i spaleniami", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "wartość witness nie zgadza się z transakcją: wejście {input} nie daje nullifiera bundle", "Theme": "Motyw", "There is no pool for that pair.": "Nie ma puli dla tej pary.", "There is no program {program}… on this chain. Nothing was sent.": "Brak programu {program}… w tym łańcuchu. Nic nie zostało wysłane.", @@ -849,6 +927,7 @@ export default { "This device, or where it cannot · {name} provers": "To urządzenie, a gdy nie może · provery {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "To usuwa noty i historię odczytane ze starego łańcucha i odczytuje ten węzeł od początku. Klucze i hasło pozostają nienaruszone.", "This key can spend your funds": "Ten klucz pozwala wydać twoje środki", + "this key is not one of the policy's signers": "ten klucz nie należy do sygnatariuszy polityki", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Ten klucz to twój portfel. Każdy, kto go ma, może wydać twoje środki, a bez niego utrata urządzenia oznacza utratę środków. Możesz go ponownie zobaczyć w Ustawieniach po podaniu hasła.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "To opuszcza osłoniętą pulę. Adres docelowy i kwota staną się publiczne w drugim łańcuchu.", "This link is too long for a QR code — copy it instead.": "Ten link jest za długi na kod QR — skopiuj go.", @@ -864,6 +943,10 @@ export default { "This note was not found.": "Nie znaleziono tej noty.", "This passkey does not support the PRF extension.": "Ten klucz dostępu nie obsługuje rozszerzenia PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Ten dowód został już przesłany z innego okna. Sprawdź Aktywność przed ponownym wysłaniem.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Ta propozycja wygasa na wysokości {expires}, a łańcuch jest na {head}: za późno na podpis. Poproś o nową propozycję.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Ta propozycja jest związana z innym genesis niż ten, który obsługuje węzeł tego portfela.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "ta propozycja dotyczy innej polityki: wskazuje {theirs}, ta polityka to {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Ta propozycja jest dla łańcucha {theirs}; ten portfel jest w łańcuchu {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "ten prover pobiera {fee} RAND za dowód, a ta wersja portfela nie płaci opłat proverom: sparuj prover, który nic nie pobiera, lub generuj dowód na tym urządzeniu", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Ten prover pobiera opłatę, której ta wersja portfela nie płaci. Sparuj prover, który nic nie pobiera, lub wyślij z aplikacji desktopowej.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Ten prover nie przyjmuje tego rodzaju zadań ({reason}). Sparuj inny prover w Ustawieniach lub wyślij z aplikacji desktopowej.", @@ -875,6 +958,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "To usuwa portfel z tego urządzenia. Do jego przywrócenia potrzebny będzie klucz odzyskiwania.", "This screen could not be shown.": "Nie udało się wyświetlić tego ekranu.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Ten ekran pojawi się w późniejszej wersji. Twój portfel jest odblokowany i gotowy.", + "this signer is already saved as {label}": "ten sygnatariusz jest już zapisany jako {label}", + "This signer profile has no key on this device: remove it and add it again.": "Ten profil sygnatariusza nie ma klucza na tym urządzeniu: usuń go i dodaj ponownie.", "This tab is showing what it has so far, and will refresh when that finishes.": "Ta karta pokazuje to, co ma do tej pory, i odświeży się po zakończeniu.", "This takes a few minutes; keep this window open.": "To zajmuje kilka minut; nie zamykaj tego okna.", "This transaction was not found.": "Nie znaleziono tej transakcji.", @@ -921,6 +1006,7 @@ export default { "Type {word} to confirm": "Wpisz {word}, aby potwierdzić", "Type it once more.": "Wpisz je jeszcze raz.", "Type the last four characters of that address": "Wpisz ostatnie cztery znaki tego adresu", + "Type the last four characters of the account fingerprint": "Wpisz ostatnie cztery znaki odcisku konta", "unavailable in this app": "niedostępne w tej aplikacji", "unknown": "nieznany", "unknown parameter {name}": "nieznany parametr {name}", @@ -957,6 +1043,7 @@ export default { "Weak": "Słabe", "Welcome back": "Witaj ponownie", "What the pool pays you arrives as a note your wallet finds on its next sync.": "To, co wypłaci ci pula, przychodzi jako nota, którą portfel znajdzie przy następnej synchronizacji.", + "What this proposal does, as checked against your policy": "Co robi ta propozycja, sprawdzone względem Twojej polityki", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Tam, gdzie to urządzenie nie może wygenerować dowodu, sparuj prover. Twój klucz wydawania i tak zostaje tutaj; prover uruchomiony przez ciebie — aplikacja desktopowa lub rand-prover na twojej maszynie — zachowuje też historię tylko dla ciebie.", "Where to?": "Dokąd?", "Which address?": "Który adres?", @@ -1003,6 +1090,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Twój prover nie skończył po {n} minucie generowania dowodu. Wznów później lub anuluj.", few: "Twój prover nie skończył po {n} minutach generowania dowodu. Wznów później lub anuluj.", many: "Twój prover nie skończył po {n} minutach generowania dowodu. Wznów później lub anuluj.", other: "Twój prover nie skończył po {n} minuty generowania dowodu. Wznów później lub anuluj." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Twój prover nie rozpoczął tego dowodu po {n} minucie w kolejce. Wznów później lub anuluj.", few: "Twój prover nie rozpoczął tego dowodu po {n} minutach w kolejce. Wznów później lub anuluj.", many: "Twój prover nie rozpoczął tego dowodu po {n} minutach w kolejce. Wznów później lub anuluj.", other: "Twój prover nie rozpoczął tego dowodu po {n} minuty w kolejce. Wznów później lub anuluj." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Nie udało się otworzyć parowania twojego provera. Zablokuj i odblokuj portfel lub sparuj prover ponownie w Ustawieniach.", + "Your share — send it to whoever proves the proposal": "Twój udział podpisu — wyślij go temu, kto dowodzi propozycji", "Your wallet picks up what it pays you on its next sync.": "Twój portfel odbierze to, co ci wypłaci, przy następnej synchronizacji.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "Historia twojego portfela została odczytana z {chain} — nic nie zostało zmienione, a ten portfel nie będzie wysyłał ani żądał środków, dopóki oba się nie zgodzą. Zmień węzeł w Ustawieniach lub przeskanuj ten portfel ponownie dla nowego łańcucha.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "To adres proverów {name}. Nie trzeba ich parować: ten portfel używa ich domyślnie.", diff --git a/ui/locales/ps.js b/ui/locales/ps.js index 04c6cba..da02653 100644 --- a/ui/locales/ps.js +++ b/ui/locales/ps.js @@ -8,6 +8,7 @@ // Every {n}/{count} key is a plain string: the English keys already split one from other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "د ثبوت لپاره {blocks} بلاکونه پاتې دي.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} همځولی", "{count} peers": "{count} همځولي", @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} بند دی", "{label} is on": "{label} روښانه دی", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} نور دا والټ نه خلاصوي (پټنوم یې بدل شوی). په پټنوم یې خلاص کړئ، بیا په امستنو کې {label} بیا روښانه کړئ.", + "{matched} of {total} payments matched.": "د {total} تادیاتو څخه {matched} سره برابر شول.", "{member} answered with another key than the one this wallet pins": "{member} په داسې کیلي ځواب ورکړ چې دا والټ یې نه پېژني", "{member} did not answer": "{member} ځواب ونه کړ", "{member} does not take this wallet's jobs": "{member} د دې والټ دندې نه مني", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} نه شي ایستل کېدای", "{symbol} has {decimals} decimal places — that is more.": "{symbol} {decimals} اعشاري ځایونه لري — دا تر هغه ډېر دي.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} د هغې سکې په بڼه خوشې کېږي چې تاسو یې غوره کوئ، د هماغې سکې په خپل زنځیر.", + "{threshold} of {count} · signer {index}": "د {count} څخه {threshold} · لاسلیک کوونکی {index}", + "{threshold} of {count} · this key is signer {index}": "د {count} څخه {threshold} · دا کیلي لاسلیک کوونکی {index} دی", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} د {chain} په زنځیر {decimals} اعشاري ځایونه لري: اندازه او د ریلېر فیس باید د {unit} مضربونه وي", "{units} units of asset {asset}": "د {asset} شتمنۍ {units} واحدونه", "{url} answered first — chain {chain} · height {height}.": "{url} لومړی ځواب ورکړ — زنځیر {chain} · لوړوالی {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "د ثبوت جوړوونکي پته باید https:// وي.", "A prover is making the proof": "یو ثبوت جوړوونکی ثبوت جوړوي", "A prover sees your history": "ثبوت جوړوونکی ستاسو تاریخچه ویني", + "a share is index:hex": "د لاسلیک برخه index:hex ده", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "د Rand شبکې لپاره یو خوندي والټ. بیلانس او تاریخچه پټ پاتې کېږي — هر لېږل په همدې وسیله ثابتېږي.", + "a signer key is a rand-node keygen file or 64 hex characters": "د لاسلیک کوونکي کیلي د rand-node keygen فایل یا ۶۴ هېکس توري دي", + "a signer label is 1-64 characters": "د لاسلیک کوونکي نښه ۱-۶۴ توري ده", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "د لاسلیک کوونکي پروفایل د دې والټ په کوډ شوي خونديځای کې ساتل کېږي. هر لاسلیک بیا ستاسو پټنوم غواړي.", + "a signer seed is 64 hex characters": "د لاسلیک کوونکي seed ۶۴ هېکس توري دي", "A site": "یوه وېبپاڼه", "a spend key is 64 hex characters, or a wallet.key.json": "د لګښت کیلي 64 هېکس توري ده، یا یو wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "لومړی د راکړې ورکړې هش ازمویل کېږي؛ که هېڅ پیدا نه شي، هماغه هش د بلاک په توګه ازمویل کېږي. یوازې عددونه بلاک د لوړوالي له مخې لټوي.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "هغه ته ایستل چې دا یې په لېست کې نه لري ښايي رد شي. تر ثابتېدو پورې هېڅ نه لګېږي.", "About": "په اړه", "about {left} left": "شاوخوا {left} پاتې", + "Account fingerprint — compare it with every other signer before you save": "د حساب ګوتنښه — مخکې له خوندي کولو یې له هر بل لاسلیک کوونکي سره پرتله کړئ", "Activity": "فعالیت", "Add a contact": "اړیکه زیاته کړئ", + "Add a signer": "لاسلیک کوونکی زیات کړئ", + "Add payment": "تادیه زیاته کړئ", + "Add the policy file and your signer key to sign for a private multisig account.": "د خصوصي ملټي‌سېګ حساب لپاره د لاسلیک کولو لپاره د پالیسۍ فایل او خپله د لاسلیک کوونکي کیلي زیاته کړئ.", "Address": "پته", "Address copied": "پته کاپي شوه", "Address or payment link": "پته یا د ورکړې لینک", @@ -115,14 +128,18 @@ export default { "almost done": "نږدې بشپړ", "also tried {endpoints}": "{endpoints} هم وازمویل شول", "Amount": "اندازه", + "Amount in RAND": "په RAND کې اندازه", "amount must be greater than zero": "اندازه باید له صفر څخه لویه وي", "amount overflow": "اندازه له حد څخه واوښته", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "د غیر RAND تادیاتو اندازه نشي څېړل کېدای: د هغوی لپاره اندازه خالي پرېږدئ.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "دا ترلاسه کوونکی RAND او یو ټوکن دواړه ترلاسه کوي: یو ترلاسه کوونکي ته د RAND او ټوکن تادیات جلا وړاندیزونو ته اړتیا لري.", "amounts overflow": "اندازې له حد څخه واوښتې", "an action is {n} characters, more than {max}": "یوه کړنه {n} توري ده، له {max} څخه ډېره", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "یوه پته (rand1…)، د ورکړې لینک (randpay:…)، یا د خوندي شوې اړیکې نوم.", "An address here is hexadecimal — 0-9 and a-f only.": "دلته پته هېکساډېسیمل ده — یوازې 0-9 او a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "په دې زنځیر کې پته 20 بایټه ده — 40 هېکس توري. دا {length} دی.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "په دې زنځیر کې پته 32 بایټه ده — 64 هېکس توري. دا {length} دی.", + "An amount is a number of RAND, such as 1.5.": "اندازه د RAND شمېره ده، لکه 1.5.", "an unknown chain": "یو نامعلوم زنځیر", "another tab changed this wallet while it was syncing; try again": "بلې ټب دا والټ د همغږۍ پر مهال بدل کړ؛ بیا هڅه وکړئ", "Another tab is syncing": "بله ټب همغږي کوي", @@ -189,11 +206,16 @@ export default { "Chains known": "پېژندل شوي زنځیرونه", "Change back to you": "تاسو ته پاتې بېرته", "Change it": "بدل یې کړئ", + "Check": "وڅېړئ", + "Check a share": "د لاسلیک برخه وڅېړئ", "Check Activity": "فعالیت وګورئ", "Check Activity first": "لومړی فعالیت وګورئ", "Check before saving": "د خوندي کولو مخکې یې وګورئ", "Check fingerprint": "ګوتنښه وګورئ", + "Check payments": "تادیات وڅېړئ", + "Check proposal": "وړاندیز وڅېړئ", "Check settings": "امستنې وګورئ", + "Check share": "د لاسلیک برخه وڅېړئ", "Checking it against the chain before anything is proved.": "مخکې له دې چې څه ثابت شي، له زنځیر سره پرتله کېږي.", "Checking the pool and your notes…": "پول او ستاسو نوټونه کتل کېږي…", "Checking the swap…": "تبادله کتل کېږي…", @@ -220,6 +242,7 @@ export default { "Continue": "دوام ورکړئ", "Copied": "کاپي شو", "Copied as JSON": "د JSON په بڼه کاپي شو", + "Copy": "کاپي کړئ", "Copy address": "پته کاپي کړئ", "Copy as JSON": "د JSON په بڼه کاپي کړئ", "Copy link": "لینک کاپي کړئ", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "د لېږلو لپاره اندازه ولیکئ.", "Enter an amount.": "اندازه ولیکئ.", "Enter at least 10 characters": "لږ تر لږه 10 توري ولیکئ", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "هره تادیه چې کول یې غواړئ ولیکئ: پته او په RAND کې اندازه. هماغې پتې ته دوه ځله تادیه یو ځل د ټولیز مجموعې سره لیکل کېږي. هغه محصول چې له هیڅ یوې سره نه برابرېږي لاسلیک نشي کېدای.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "د هر تادیه ترلاسه کوونکي پته ولیکئ: هغه محصول چې له لیکل شوې پتې سره نه برابرېږي، یا پته یې خالي ده، لاسلیک نشي کېدای.", + "Enter the address of every payment.": "د هرې تادیې پته ولیکئ.", "Enter the address this should be released to.": "هغه پته ولیکئ چې دا باید ورته خوشې شي.", "Enter the address you are sending to.": "هغه پته ولیکئ چې ورته لېږئ.", "Enter the amount as a number, for example 1.25.": "اندازه د عدد په توګه ولیکئ، د بېلګې په توګه 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "د هرې تادیې اندازه ولیکئ: همدا هغه څه دي چې ویش یا پړسول شوې تادیه ځنډوي.", "Enter the first 4 characters of your recovery key.": "د خپلې د بیا رغونې کیلي لومړي 4 توري ولیکئ.", "Enter the relayer fee as a number, for example 1.25.": "د ریلېر فیس د عدد په توګه ولیکئ، د بېلګې په توګه 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "د دې والټ پټنوم ولیکئ — جوړه کول د هغه لاندې مهر شوي دي.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "خپله د بیا رغونې کیلي ولیکئ.", "Epoch": "دوره", "Exactly those four letters, in capitals.": "بالکل هماغه څلور توري، په لویو تورو.", + "expected recipient {address}: {reason}": "تمه شوی ترلاسه کوونکی {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients باید د پتو لیست وي", "Explore": "وپلټئ", "Export spend key": "د لګښت کیلي صادره کړئ", "Exporting the spend key is behind your password.": "د لګښت کیلي صادرول ستاسو د پټنوم تر شا دي.", @@ -288,6 +317,7 @@ export default { "Fee": "فیس", "fee must be at least {fee} RAND (the bridge burn floor)": "فیس باید لږ تر لږه {fee} RAND وي (د پل د سوځولو لږترلږه)", "fee must be at least {fee} RAND (the bundle floor)": "فیس باید لږ تر لږه {fee} RAND وي (د بنډل لږترلږه)", + "Fee: {fee} RAND · expires at height {height}": "فیس: {fee} RAND · په {height} لوړوالي کې پای ته رسېږي", "Filter activity by asset": "فعالیت د شتمنۍ له مخې چاڼ کړئ", "Fingerprint": "ګوتنښه", "fingerprint {fp}": "ګوتنښه {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "پوهېږم چې هر څوک دا کیلي ولري زما پیسې لګولی شي", "Import wallet": "والټ وارد کړئ", "Incorrect password.": "ناسم پټنوم.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "ننوتې {held} لري، خو اندازه + فیس {need} دی", "inputs hold {held}, but fee + RAND deposit is {need}": "ننوتې {held} لري، خو فیس + د RAND زېرمه {need} ده", "insufficient balance: have {have}, need {need}": "بیلانس بس نه دی: {have} شته، {need} اړین دي", + "Intended payments": "موخه شوې تادیات", "is on chain {chain}, not {expected}": "په {chain} زنځیر دی، نه په {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "دا ستاسو بیلانس یا فعالیت نه شي لیدلی، او هېڅ شی نه شي خوځولی.", "It cannot spend.": "لګښت نه شي کولی.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "د کیلي فایل spend_key نه لري", "key file must be version 2": "د کیلي فایل باید 2 نسخه وي", "Key hidden — enter your password to view it again.": "کیلي پټه شوه — د بیا لیدو لپاره خپل پټنوم ولیکئ.", + "Label": "نښه", "Language": "ژبه", "Leave empty to let the payer choose.": "تش یې پرېږدئ چې ورکوونکی یې غوره کړي.", "Light": "روښانه", @@ -360,11 +393,15 @@ export default { "machines": "ماشینونه", "malformed parameter {parameter}": "ناسم پارامېټر {parameter}", "Manage contacts": "اړیکې اداره کړئ", + "Manage multisig signers": "د ملټي‌سېګ لاسلیک کوونکي اداره کړئ", "Max": "اعظمي", "Memo": "یادښت", "memo \"{memo}\"": "یادښت \"{memo}\"", "memo is {n} bytes, at most {max}": "یادښت {n} بایټه دی، تر ډېره {max}", "More is on the way": "نور په لاره دي", + "Multisig share": "د ملټي‌سېګ د لاسلیک برخه", + "Multisig signers": "د ملټي‌سېګ لاسلیک کوونکي", + "Multisig signing is not available in this app.": "په دې اپ کې ملټي‌سېګ لاسلیک شتون نه لري.", "My own prover · {name}": "زما خپل ثبوت جوړوونکی · {name}", "Name": "نوم", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "د هغو پتو لپاره نومونه چې ډېر ځله ورته ورکړه کوئ، یوازې په دې وسیله کې ساتل کېږي. اړیکې ته د نوم له لارې ولېږئ؛ د خوندي کولو پر مهال یې ګوتنښه کتل کېږي.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "لا اړیکې نشته. له امستنو څخه یوه خوندي کړئ، یا پته ولګوئ.", "no endpoint could answer {method}": "هېڅ پای ټکي {method} ته ځواب ونه شو ورکولی", "no endpoint could answer {method}; tried {endpoints}": "هېڅ پای ټکي {method} ته ځواب ونه شو ورکولی؛ {endpoints} وازمویل شول", + "no expected recipient given: the payee was NOT checked": "هیڅ تمه شوی ترلاسه کوونکی نه دی ورکړل شوی: ترلاسه کوونکی ونه څېړل شو (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "په {url} کې هیڅ نوډ ځواب نه کړ، نو پای ته رسېدل نشي څېړل کېدای او دا وړاندیز نه لاسلیک کېږي. کله چې نوډ ترلاسه وي بیا هڅه وکړئ.", "No proof is pending.": "هېڅ ثبوت په تمه نه دی.", "No prover": "ثبوت جوړوونکی نشته", "No prover is paired.": "هېڅ ثبوت جوړوونکی جوړه شوی نه دی.", "No RAND to send yet": "لا د لېږلو لپاره RAND نشته", "No registry assets": "د راجستر شتمنۍ نشته", "no reply": "ځواب نشته", + "No signer profiles yet": "تر اوسه د لاسلیک کوونکي پروفایلونه نشته", + "No such signer profile.": "داسې د لاسلیک کوونکي پروفایل نشته.", "No transaction or block matches that.": "هېڅ راکړه ورکړه یا بلاک له دې سره سمون نه لري.", "no wallet on this device": "په دې وسیله کې والټ نشته", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "د RandProtocol ثبوت جوړوونکو جوړ شوي لینکونو کې هېڅ یو هغه کیلي نه یادوي چې دا والټ یې ورته ټاکلې؛ نه کارول کېږي.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "یو ثبوت په دې وسیله کې چلېږي — شاوخوا دوه دقیقې. تاسو کولی شئ نورې پردې وګورئ؛ د والټ بندول یې دروي.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "د {chain} په زنځیر کې په هغه سکه کې یوازې {locked} قلف دي؛ بل ملاتړ یا کوچنۍ اندازه غوره کړئ", "Open durian.market": "durian.market خلاص کړئ", + "Open file": "فایل خلاص کړئ", "Open in explorer": "په اکسپلورر کې یې خلاص کړئ", "Open in randscan": "په randscan کې یې خلاص کړئ", "Open in side panel": "په غاړې پینل کې یې خلاص کړئ", + "Open in the side panel to choose files": "د فایلونو د ټاکلو لپاره یې په ضمیمه پینل کې خلاص کړئ", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Rand Wallet د پټنوم لیکلو پر ځای په {label} خلاص کړئ. ستاسو پټنوم لا هم کار کوي، او د لاسرسي د بېرته راګرځولو لاره پاتې کېږي.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "والټ د پټنوم لیکلو پر ځای په {label} خلاص کړئ. ستاسو پټنوم لا هم کار کوي، او د لاسرسي د بېرته راګرځولو لاره پاتې کېږي.", "Open this window from a site that asked to connect.": "دا کړکۍ له هغې وېبپاڼې خلاصه کړئ چې د وصل کېدو غوښتنه یې کړې.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "یا د RandProtocol ثبوت جوړوونکي وکاروئ — {n} ماشینونه چې تصدیقوونکي یې چلوي، هر یو له خپلې کیلي سره.", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "یا د RandProtocol ثبوت جوړوونکي وکاروئ — ماشینونه چې تصدیقوونکي یې چلوي، هر یو له خپلې کیلي سره.", "Outcome unknown": "پایله نامعلومه", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "محصول {slot} د دې حساب په توګه نه خلاصېږي: د هغه ارزښت لاسلیک ردېږي چې نشي لیدل کېدای", "own={value}, expected 1 or absent": "own={value}، تمه وه 1 یا نشتوالی", "Paid": "ورکړل شو", "Pair your own prover": "خپل ثبوت جوړوونکی جوړه کړئ", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "خپله د بیا رغونې کیلي ولګوئ", "path is not {n} levels": "لاره {n} کچې نه ده", "Pay me on Rand": "په Rand ماته ورکړه وکړئ", + "Payee address": "د ترلاسه کوونکي پته", "Payment link": "د ورکړې لینک", "Payment link · fingerprint {fingerprint}": "د ورکړې لینک · ګوتنښه {fingerprint}", "Payment link copied": "د ورکړې لینک کاپي شو", "Payment links cannot be read in this app; paste the address instead.": "په دې اپ کې د ورکړې لینکونه نه شي لوستل کېدای؛ پر ځای یې پته ولګوئ.", "Payment links cannot be read in this app.": "په دې اپ کې د ورکړې لینکونه نه شي لوستل کېدای.", + "Payments changed: check them again before signing.": "تادیات بدل شول: مخکې له لاسلیک بیا یې وڅېړئ.", "Peers": "همځولي", "Pending": "په تمه", "Permission to reach that host was not granted, so the node was left as it was.": "هغه کوربه ته د رسېدو اجازه ورنه کړل شوه، نو نوډ څنګه چې و هماغسې پاتې شو.", "Permission to reach that host was not granted.": "هغه کوربه ته د رسېدو اجازه ورنه کړل شوه.", "Permission to reach that prover was not granted, so nothing was saved.": "هغه ثبوت جوړوونکي ته د رسېدو اجازه ورنه کړل شوه، نو هېڅ خوندي نه شو.", "Point the camera at a Rand QR code.": "کامره د Rand QR کوډ ته ونیسئ.", + "Policy file": "د پالیسۍ فایل", "Pool fee": "د پول فیس", "Price impact": "پر بیه اغېز", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "په دې زنځیر کې د durian.market پولونو له مخې بیه ټاکل شوې ({pct}% د پول فیس). تبادله یوه راکړه ورکړه ده چې دا والټ یې پخپله ثابتوي او لېږي.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "ثبوتونه یوازې په دې وسیله کې جوړېږي. چېرې چې یې جوړولی نه شي، لېږل تر هغه انتظار باسي چې تاسو ثبوت جوړوونکی جوړه کړئ یا بیا د RandProtocol ثبوت جوړوونکی وکاروئ.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "هغه ثبوتونه چې دا وسیله یې نه شي جوړولی {name} ته ځي. د هغه ګوتنښه {fingerprint} ده — وګورئ چې ثبوت جوړوونکی هم هماغه ښيي.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "هغه ثبوتونه چې دا وسیله یې نه شي جوړولی له دوی څخه یوه ته ځي؛ هر هغه چې یو لېږل ثابتوي د هغه والټ د لیدو کیلي ویني.", + "Proposal (proposal.json)": "وړاندیز (proposal.json)", "Prove and send": "ثابت یې کړئ او ولېږئ", "Prove for my other devices": "زما د نورو وسیلو لپاره ثبوت جوړ کړئ", "Prover": "ثبوت جوړوونکی", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "د ریلېر فیس (اختیاري)", "Remove": "لرې یې کړئ", "Remove {name}": "{name} لرې کړئ", + "Remove this signer": "دا لاسلیک کوونکی لرې کړئ", "Request RAND": "RAND وغواړئ", "Request RAND from the faucet to try sending, receiving and proving on this network.": "له فاسېټ څخه RAND وغواړئ چې په دې شبکه کې لېږل، ترلاسه کول او ثابتول وازمویئ.", "Rescan": "بیا سکین", @@ -574,8 +623,12 @@ export default { "Running on": "چلېږي په", "Save": "خوندي یې کړئ", "Save an address you pay often, and send to it by name.": "هغه پته خوندي کړئ چې ډېر ځله ورته ورکړه کوئ، او د نوم له لارې ورته ولېږئ.", + "Save as file": "د فایل په توګه خوندي کړئ", + "Save signer": "لاسلیک کوونکی خوندي کړئ", "Save your recovery key": "خپله د بیا رغونې کیلي خوندي کړئ", "Saved": "خوندي شو", + "Saved to {path}": "په {path} کې خوندي شو", + "Saved.": "خوندي شو.", "Scan": "سکین", "Scan a payment link": "د ورکړې لینک سکین کړئ", "Scan a QR code": "QR کوډ سکین کړئ", @@ -596,6 +649,7 @@ export default { "Sent.": "ولېږل شو.", "Set on this device when this wallet was created.": "په دې وسیله کې هغه وخت ټاکل شوی کله چې دا والټ جوړ شو.", "Settings": "امستنې", + "Share": "د لاسلیک برخه", "Share payment link": "د ورکړې لینک شریک کړئ", "shielded address decodes to {n} bytes, expected {expected}": "خوندي پته {n} بایټو ته ډیکوډ کېږي، تمه {expected} وه", "shielded address is not base58": "خوندي پته base58 نه ده", @@ -607,6 +661,12 @@ export default { "Show the full address": "بشپړه پته وښیئ", "Show viewing key": "د لیدو کیلي وښیئ", "Show viewing key for 10 seconds": "د لیدو کیلي د 10 ثانیو لپاره وښیئ", + "Sign": "لاسلیک", + "Sign a proposal": "وړاندیز لاسلیک کړئ", + "signer {index} is not in this policy (it has {count} signers)": "لاسلیک کوونکی {index} په دې پالیسۍ کې نشته (دا {count} لاسلیک کوونکي لري)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "د لاسلیک کوونکي {index} د لاسلیک برخه سمه ده. د {threshold} څخه {have} په لاس کې دي: لاسلیک کوونکي {list}.", + "signer {index}'s signature does not verify over this proposal": "د لاسلیک کوونکي {index} لاسلیک د دې وړاندیز لپاره سم نه دی", + "Signer key file, or its 64-hex seed": "د لاسلیک کوونکي د کیلي فایل، یا د هغه ۶۴ هېکس seed", "So the address above is one you have actually read.": "تر څو پورتنۍ پته هغه وي چې تاسو په رښتیا لوستې ده.", "Something went wrong": "یو څه خراب شول", "Something went wrong.": "یو څه خراب شول.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "دا کاپي نه شو.", "That could not be saved.": "دا خوندي نه شو.", "That does not match — reveal the key again and check.": "دا سمون نه لري — کیلي بیا ښکاره کړئ او وګورئ.", + "That file is larger than a proposal, a policy or a key file can be.": "دا فایل له هغه څخه لوی دی چې یو وړاندیز، پالیسي یا د کیلي فایل کېدای شي.", "That is more than your balance of {balance} {symbol}.": "دا ستاسو له {balance} {symbol} بیلانس څخه ډېر دی.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "دا نه 64 هېکس توري (هش) دي او نه د بلاک لوړوالی (یوازې عددونه).", "That is not a pairing link.": "دا د جوړه کولو لینک نه دی.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "فیس ونه شو محاسبه کېدای.", "the genesis hash is not hex of a plausible length": "جېنېسس هش د منلو وړ اوږدوالي هېکس نه دی", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "ننوتل بالکل هغه وخت `none` وي چې بنډل هېڅ ټوکن نه سوځوي (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "د کیلي فایل پته هغه نه ده چې seed یې راباسي", + "the key file's public_key is not the one its seed derives": "د کیلي فایل public_key هغه نه دی چې seed یې راباسي", "The key you exported when this wallet was created.": "هغه کیلي چې تاسو د دې والټ د جوړېدو پر مهال صادره کړې.", "The language could not be saved.": "ژبه خوندي نه شوه.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "لینک {linked} {symbol} غواړي؛ تاسو {typed} {symbol} ولیکل.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "هغه پټنوم چې دا والټ په دې وسیله کې خلاصوي.", "The payer decides": "ورکوونکی یې ټاکي", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "په تمه ثبوت پاک شو (والټ قلف شو، یا بل ځای لغوه شو)، نو هېڅ ونه لېږل شول. بیا یې ولېږئ.", + "the policy does not parse: {reason}": "پالیسي نشي تحلیل کېدای: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "پول له هغه وخته بدل شوی چې دې پاڼې لوستی و. هېڅ ونه لېږل شول؛ وېبپاڼه کولی شي بیا بیه ورکړي.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "پول د ثابتولو پر مهال بدل شو. هېڅ ونه لېږل شول؛ وېبپاڼه کولی شي بیا بیه ورکړي.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "پول دومره نه لري چې دا ورکړي. هېڅ ونه لېږل شول؛ وېبپاڼه کولی شي بیا بیه ورکړي.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "ثبوت نه تصدیقېږي: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "ثبوت داسې ډایجسټ خپور کړ چې دې والټ نه و جوړ کړی ({expected} پر ځای {published})؛ نه کارول کېږي", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "ثبوت په دې وسیله کې چلېږي. تاسو کولی شئ نورې پردې وګورئ — دا روان پاتې کېږي — خو د والټ بندول یې دروي.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "د وړاندیز څېړنې ناقص ځواب راستون کړ، نو دا وړاندیز نه لاسلیک کېږي.", + "the proposal does not parse: {reason}": "وړاندیز نشي تحلیل کېدای: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "وړاندیز په {expires} کې پای ته رسېږي، د خپل bundle له کړکۍ ({from}, {to}] بهر", + "the proposal's auth_commit is not this policy's account": "د وړاندیز auth_commit د دې پالیسۍ حساب نه دی", + "the proposal's expected digest is not its bundle's": "د وړاندیز تمه شوی digest د هغه د bundle نه دی", + "the proposal's signing_message is not its binding's": "د وړاندیز signing_message د هغه د binding نه دی", "the prover": "ثبوت جوړوونکی", "the prover accepted the job but named no job id": "ثبوت جوړوونکي دنده ومنله خو د دندې پېژند یې ونه ښود", "The prover answered with an unknown state ({state}).": "ثبوت جوړوونکي په نامعلوم حالت ({state}) ځواب ورکړ.", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "غوښتنه په Rand Wallet کې منظور نه شوه.", "The rescan could not be started.": "بیا سکین پیل نه شو.", "The rescan did not finish": "بیا سکین بشپړ نه شو", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "خوندي شوي د لاسلیک کوونکي پروفایلونه خلاص نشول؛ هیڅ نه دی بدل شوی. د دې وسیلې ذخیره بیا جوړه کړئ، یا والټ پاک کړئ او د خپلې کیلي څخه یې بیرته راولئ", "The scan was cancelled.": "سکین لغوه شو.", "The side panel could not be opened.": "غاړې پینل خلاص نه شو.", "The site sent an invoke this wallet cannot read: {reason}.": "وېبپاڼې داسې invoke ولېږه چې دا والټ یې نه شي لوستلی: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "تبادله ونه شوه.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "تبادله روانه ده. که والټ مخکې له دې بند کړئ چې ووایي لېږل شو، دا درېږي، او هېڅ نه ورکېږي.", "The theme could not be saved.": "بڼه خوندي نه شوه.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "راکړه ورکړه یو عمل لري؛ خصوصي ملټي‌سېګ وړاندیز یوازې د خپل bundle څخه تادیه ده", + "the transaction carries no bundle": "راکړه ورکړه هیڅ bundle نه لري", + "the transaction does not hash to the proposal's binding": "د راکړې ورکړې hash د وړاندیز binding نه دی", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "راکړه ورکړه د زنځیر {tx} لپاره ده، وړاندیز زنځیر {proposal} وايي", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "د راکړې ورکړې کیلي یوازې دا یوه ورکړه — اندازه او ترلاسه کوونکی یې — هغه چا ته ښکاره کوي چې تاسو یې ورکوئ. هېڅ شی نه شي لګولی.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "ښايي راکړه ورکړه دمخه په زنځیر کې وي. د بیا ایستلو مخکې فعالیت وګورئ — دوه ځله ایستل به دوه ځله سوځول وي.", "The transfer could not be proved.": "لېږدونه ثابته نه شوه.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "والټ ته وویل شول: {message}", "The withdrawal could not be proved.": "ایستنه ثابته نه شوه.", "The withdrawal was submitted, but the node did not return a usable hash.": "ایستنه وسپارل شوه، خو نوډ د کارولو وړ هش بېرته ورنه کړ.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "د witness ارزښت نشي څېړل کېدای: د هغه nk یا salt د دې وړاندیز نه دی", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "د witness ارزښت له محصولاتو سره نه برابرېږي: شتمنۍ {asset} {held} راتلونکی او {spent} وتلونکی لري، له فیس او سوځولو سره", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "د witness ارزښت له راکړې ورکړې سره نه برابرېږي: ننوتنه {input} د bundle nullifier ته نه رسېږي", "Theme": "بڼه", "There is no pool for that pair.": "د هغې جوړې لپاره پول نشته.", "There is no program {program}… on this chain. Nothing was sent.": "په دې زنځیر کې {program}… پروګرام نشته. هېڅ ونه لېږل شول.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "دا وسیله، یا چېرې چې نه شي · {name} ثبوت جوړوونکي", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "دا له زاړه زنځیر څخه لوستل شوي نوټونه او تاریخچه هېروي او دا نوډ له پیله لولي. ستاسو کیلي او پټنوم ته لاس نه وروړل کېږي.", "This key can spend your funds": "دا کیلي ستاسو پیسې لګولی شي", + "this key is not one of the policy's signers": "دا کیلي د پالیسۍ له لاسلیک کوونکو څخه نه ده", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "دا کیلي ستاسو والټ دی. هر څوک چې یې ولري ستاسو پیسې لګولی شي، او له دې پرته ورکه وسیله یعنې ورکې پیسې. تاسو کولی شئ په امستنو کې یې د خپل پټنوم سره بیا وګورئ.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "دا له خوندي پول څخه وځي. د منزل پته او اندازه به په بل زنځیر کې عامه شي.", "This link is too long for a QR code — copy it instead.": "دا لینک د QR کوډ لپاره ډېر اوږد دی — پر ځای یې کاپي کړئ.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "دا نوټ پیدا نه شو.", "This passkey does not support the PRF extension.": "دا پاسکي د PRF توسعې ملاتړ نه کوي.", "This proof was already submitted from another window. Check Activity before sending again.": "دا ثبوت دمخه له بلې کړکۍ څخه سپارل شوی. د بیا لېږلو مخکې فعالیت وګورئ.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "دا وړاندیز په {expires} لوړوالي کې پای ته رسېږي او زنځیر په {head} کې دی: د لاسلیک لپاره ډېر ناوخته دی. نوی وړاندیز وغواړئ.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "دا وړاندیز له هغه genesis پرته بل ته تړل شوی چې د دې والټ نوډ یې وړاندې کوي.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "دا وړاندیز د بلې پالیسۍ لپاره دی: {theirs} یادوي، دا پالیسي {ours} ده", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "دا وړاندیز د زنځیر {theirs} لپاره دی؛ دا والټ په زنځیر {ours} کې دی.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "دا ثبوت جوړوونکی په هر ثبوت {fee} RAND اخلي، او د والټ دا نسخه د ثبوت جوړوونکي فیس نه ورکوي: داسې ثبوت جوړوونکی جوړه کړئ چې هېڅ نه اخلي، یا په دې وسیله کې ثبوت جوړ کړئ", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "دا ثبوت جوړوونکی فیس اخلي، چې د والټ دا نسخه یې نه ورکوي. داسې ثبوت جوړوونکی جوړه کړئ چې هېڅ نه اخلي، یا د ډېسکټاپ له اپ څخه ولېږئ.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "دا ثبوت جوړوونکی دا ډول دنده نه مني ({reason}). په امستنو کې بل ثبوت جوړوونکی جوړه کړئ، یا د ډېسکټاپ له اپ څخه ولېږئ.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "دا والټ له دې وسیلې څخه لرې کوي. د بېرته راګرځولو لپاره به مو د بیا رغونې کیلي ته اړتیا وي.", "This screen could not be shown.": "دا پرده ونه ښودل شوه.", "This screen ships in a later task. Your wallet is unlocked and ready.": "دا پرده په یوه وروستۍ دنده کې راځي. ستاسو والټ خلاص او چمتو دی.", + "this signer is already saved as {label}": "دا لاسلیک کوونکی لا دمخه د {label} په نامه خوندي دی", + "This signer profile has no key on this device: remove it and add it again.": "دا د لاسلیک کوونکي پروفایل په دې وسیله کې کیلي نه لري: لرې یې کړئ او بیا یې زیات کړئ.", "This tab is showing what it has so far, and will refresh when that finishes.": "دا ټب هغه څه ښيي چې تر اوسه یې لري، او کله چې هغه بشپړ شي تازه به شي.", "This takes a few minutes; keep this window open.": "دا څو دقیقې وخت نیسي؛ دا کړکۍ خلاصه وساتئ.", "This transaction was not found.": "دا راکړه ورکړه پیدا نه شوه.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "د تایید لپاره {word} ولیکئ", "Type it once more.": "یو ځل بیا یې ولیکئ.", "Type the last four characters of that address": "د هغې پتې وروستي څلور توري ولیکئ", + "Type the last four characters of the account fingerprint": "د حساب د ګوتنښې وروستي څلور توري ولیکئ", "unavailable in this app": "په دې اپ کې نشته", "unknown": "نامعلوم", "unknown parameter {name}": "نامعلوم پارامېټر {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "کمزوری", "Welcome back": "ښه راغلاست بېرته", "What the pool pays you arrives as a note your wallet finds on its next sync.": "هغه څه چې پول تاسو ته درکوي د نوټ په توګه رسېږي چې ستاسو والټ یې په راتلونکې همغږۍ کې پیدا کوي.", + "What this proposal does, as checked against your policy": "دا وړاندیز څه کوي، ستاسو د پالیسۍ پر وړاندې څېړل شوی", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "چېرې چې دا وسیله ثبوت نه شي جوړولی، ثبوت جوړوونکی جوړه کړئ. ستاسو د لګښت کیلي په هر حال دلته پاتې کېږي؛ هغه ثبوت جوړوونکی چې تاسو یې پخپله چلوئ — د ډېسکټاپ اپ، یا ستاسو په خپل ماشین کې rand-prover — ستاسو تاریخچه هم تاسو ته ساتي.", "Where to?": "چېرته؟", "Which address?": "کومه پته؟", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "ستاسو ثبوت جوړوونکی د {n} دقیقو ثابتولو وروسته لا بشپړ شوی نه دی. وروسته بیا پیل کړئ، یا لغوه کړئ.", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "ستاسو ثبوت جوړوونکي په خپل کتار کې د {n} دقیقو وروسته لا دا ثبوت نه دی پیل کړی. وروسته بیا پیل کړئ، یا لغوه کړئ.", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "ستاسو د ثبوت جوړوونکي جوړه کول خلاص نه شول. والټ قلف او بیا خلاص کړئ، یا په امستنو کې ثبوت جوړوونکی بیا جوړه کړئ.", + "Your share — send it to whoever proves the proposal": "ستاسو د لاسلیک برخه — هغه چا ته یې ولېږئ چې وړاندیز ثابتوي", "Your wallet picks up what it pays you on its next sync.": "ستاسو والټ هغه څه چې تاسو ته ورکول کېږي په راتلونکې همغږۍ کې ترلاسه کوي.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "ستاسو د والټ تاریخچه له {chain} څخه لوستل شوې — هېڅ نه دي بدل شوي، او دا والټ به تر هغه پیسې ونه لېږي او ونه غواړي چې دواړه سره سمون ونه لري. په امستنو کې نوډ بدل کړئ، یا دا والټ د نوي زنځیر لپاره بیا سکین کړئ.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "دا د {name} ثبوت جوړوونکو پته ده. جوړه کولو ته اړتیا نه لري: دا والټ یې په ډیفالټ ډول کاروي.", diff --git a/ui/locales/pt.js b/ui/locales/pt.js index fd115c1..ec781f2 100644 --- a/ui/locales/pt.js +++ b/ui/locales/pt.js @@ -8,6 +8,7 @@ // Register: você. Plural categories (Intl.PluralRules('pt')): one, many, other. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Faltam {blocks} blocos para prová-la.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} par", many: "{count} pares", other: "{count} pares" }, "{count} peers": { one: "{count} par", many: "{count} pares", other: "{count} pares" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} está desativado", "{label} is on": "{label} está ativado", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} não abre mais esta carteira (a senha dela mudou). Desbloqueie com a senha e depois ative {label} de novo em Configurações.", + "{matched} of {total} payments matched.": "{matched} de {total} pagamentos conferem.", "{member} answered with another key than the one this wallet pins": "{member} respondeu com uma chave diferente da que esta carteira fixou", "{member} did not answer": "{member} não respondeu", "{member} does not take this wallet's jobs": "{member} não aceita os trabalhos desta carteira", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} não pode ser sacado", "{symbol} has {decimals} decimal places — that is more.": "{symbol} tem {decimals} casas decimais — isso é mais.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} é liberado como a moeda que você escolher, na própria cadeia dessa moeda.", + "{threshold} of {count} · signer {index}": "{threshold} de {count} · signatário {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} de {count} · esta chave é o signatário {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "{token} na cadeia {chain} tem {decimals} casas decimais: o valor e a taxa do retransmissor devem ser múltiplos de {unit}", "{units} units of asset {asset}": "{units} unidades do ativo {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} respondeu primeiro — cadeia {chain} · altura {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "O endereço de um provador deve ser https://.", "A prover is making the proof": "Um provador está gerando a prova", "A prover sees your history": "Um provador vê seu histórico", + "a share is index:hex": "uma parte da assinatura é index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Uma carteira blindada para a rede Rand. Saldos e histórico ficam privados — cada envio é provado neste dispositivo.", + "a signer key is a rand-node keygen file or 64 hex characters": "uma chave de signatário é um arquivo keygen do rand-node ou 64 caracteres hexadecimais", + "a signer label is 1-64 characters": "o rótulo de um signatário tem 1-64 caracteres", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Um perfil de signatário fica guardado no cofre criptografado desta carteira. Cada assinatura pede sua senha de novo.", + "a signer seed is 64 hex characters": "uma seed de signatário tem 64 caracteres hexadecimais", "A site": "Um site", "a spend key is 64 hex characters, or a wallet.key.json": "uma chave de gasto tem 64 caracteres hexadecimais, ou é um wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Primeiro tenta-se um hash de transação; se nada for encontrado, o mesmo hash é tentado como bloco. Só dígitos busca um bloco pela altura.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Um saque para uma que não esteja na lista pode ser recusado. Nada é gasto até que seja provado.", "About": "Sobre", "about {left} left": "faltam cerca de {left}", + "Account fingerprint — compare it with every other signer before you save": "Impressão digital da conta — compare-a com a de cada outro signatário antes de salvar", "Activity": "Atividade", "Add a contact": "Adicionar um contato", + "Add a signer": "Adicionar um signatário", + "Add payment": "Adicionar pagamento", + "Add the policy file and your signer key to sign for a private multisig account.": "Adicione o arquivo de política e sua chave de signatário para assinar por uma conta multiassinatura privada.", "Address": "Endereço", "Address copied": "Endereço copiado", "Address or payment link": "Endereço ou link de pagamento", @@ -115,14 +128,18 @@ export default { "almost done": "quase pronto", "also tried {endpoints}": "também tentou {endpoints}", "Amount": "Valor", + "Amount in RAND": "Valor em RAND", "amount must be greater than zero": "o valor deve ser maior que zero", "amount overflow": "estouro de valor", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Os valores não podem ser verificados em pagamentos que não são em RAND: deixe o valor vazio nesses.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Este beneficiário recebe RAND e também um token: pagamentos em RAND e em tokens a um mesmo beneficiário precisam de propostas separadas.", "amounts overflow": "estouro na soma dos valores", "an action is {n} characters, more than {max}": { one: "uma ação tem {n} caractere, mais que {max}", many: "uma ação tem {n} caracteres, mais que {max}", other: "uma ação tem {n} caracteres, mais que {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Um endereço (rand1…), um link de pagamento (randpay:…) ou o nome de um contato salvo.", "An address here is hexadecimal — 0-9 and a-f only.": "Um endereço aqui é hexadecimal — apenas 0-9 e a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Um endereço nesta cadeia tem 20 bytes — 40 caracteres hexadecimais. Este tem {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Um endereço nesta cadeia tem 32 bytes — 64 caracteres hexadecimais. Este tem {length}.", + "An amount is a number of RAND, such as 1.5.": "Um valor é um número de RAND, como 1.5.", "an unknown chain": "uma cadeia desconhecida", "another tab changed this wallet while it was syncing; try again": "outra aba alterou esta carteira durante a sincronização; tente de novo", "Another tab is syncing": "Outra aba está sincronizando", @@ -189,11 +206,16 @@ export default { "Chains known": "Cadeias conhecidas", "Change back to you": "Troco de volta para você", "Change it": "Alterar", + "Check": "Verificar", + "Check a share": "Verificar uma parte da assinatura", "Check Activity": "Ver Atividade", "Check Activity first": "Veja a Atividade primeiro", "Check before saving": "Verificar antes de salvar", "Check fingerprint": "Verificar impressão digital", + "Check payments": "Verificar pagamentos", + "Check proposal": "Verificar proposta", "Check settings": "Verificar configurações", + "Check share": "Verificar parte da assinatura", "Checking it against the chain before anything is proved.": "Conferindo com a cadeia antes de provar qualquer coisa.", "Checking the pool and your notes…": "Verificando o pool e suas notas…", "Checking the swap…": "Verificando a troca…", @@ -220,6 +242,7 @@ export default { "Continue": "Continuar", "Copied": "Copiado", "Copied as JSON": "Copiado como JSON", + "Copy": "Copiar", "Copy address": "Copiar endereço", "Copy as JSON": "Copiar como JSON", "Copy link": "Copiar link", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "Digite um valor para enviar.", "Enter an amount.": "Digite um valor.", "Enter at least 10 characters": "Digite pelo menos 10 caracteres", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Informe cada pagamento que você pretende fazer: o endereço e o valor em RAND. Um pagamento ao mesmo endereço duas vezes é informado uma vez só, com o total. Uma saída que não corresponde a nenhum não pode ser assinada.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Informe o endereço de cada beneficiário: uma saída que não corresponde ao endereço informado, ou cujo endereço está vazio, não pode ser assinada.", + "Enter the address of every payment.": "Informe o endereço de cada pagamento.", "Enter the address this should be released to.": "Digite o endereço para onde isto deve ser liberado.", "Enter the address you are sending to.": "Digite o endereço para onde você está enviando.", "Enter the amount as a number, for example 1.25.": "Digite o valor como número, por exemplo 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Informe o valor de cada pagamento: é o que impede uma divisão ou um pagamento inflado.", "Enter the first 4 characters of your recovery key.": "Digite os 4 primeiros caracteres da sua chave de recuperação.", "Enter the relayer fee as a number, for example 1.25.": "Digite a taxa do retransmissor como número, por exemplo 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Digite a senha desta carteira — o pareamento fica protegido por ela.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "Digite sua chave de recuperação.", "Epoch": "Época", "Exactly those four letters, in capitals.": "Exatamente essas quatro letras, em maiúsculas.", + "expected recipient {address}: {reason}": "destinatário esperado {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients deve ser uma lista de endereços", "Explore": "Explorar", "Export spend key": "Exportar chave de gasto", "Exporting the spend key is behind your password.": "Exportar a chave de gasto exige sua senha.", @@ -288,6 +317,7 @@ export default { "Fee": "Taxa", "fee must be at least {fee} RAND (the bridge burn floor)": "a taxa deve ser de pelo menos {fee} RAND (o mínimo da queima na ponte)", "fee must be at least {fee} RAND (the bundle floor)": "a taxa deve ser de pelo menos {fee} RAND (o mínimo do pacote)", + "Fee: {fee} RAND · expires at height {height}": "Taxa: {fee} RAND · expira na altura {height}", "Filter activity by asset": "Filtrar atividade por ativo", "Fingerprint": "Impressão digital", "fingerprint {fp}": "impressão digital {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "Entendo que qualquer pessoa com esta chave pode gastar meus fundos", "Import wallet": "Importar carteira", "Incorrect password.": "Senha incorreta.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "as entradas somam {held}, mas valor + taxa é {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "as entradas somam {held}, mas taxa + depósito de RAND é {need}", "insufficient balance: have {have}, need {need}": "saldo insuficiente: tem {have}, precisa de {need}", + "Intended payments": "Pagamentos pretendidos", "is on chain {chain}, not {expected}": "está na cadeia {chain}, não na {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Ele não pode ver seu saldo nem sua atividade, e não pode mover nada.", "It cannot spend.": "Ela não pode gastar.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "o arquivo de chave não tem spend_key", "key file must be version 2": "o arquivo de chave deve ser da versão 2", "Key hidden — enter your password to view it again.": "Chave oculta — digite sua senha para vê-la de novo.", + "Label": "Rótulo", "Language": "Idioma", "Leave empty to let the payer choose.": "Deixe vazio para quem paga escolher.", "Light": "Claro", @@ -360,11 +393,15 @@ export default { "machines": "máquinas", "malformed parameter {parameter}": "parâmetro malformado {parameter}", "Manage contacts": "Gerenciar contatos", + "Manage multisig signers": "Gerenciar signatários multiassinatura", "Max": "Máx.", "Memo": "Memo", "memo \"{memo}\"": "memo \"{memo}\"", "memo is {n} bytes, at most {max}": { one: "o memo tem {n} byte, no máximo {max}", many: "o memo tem {n} bytes, no máximo {max}", other: "o memo tem {n} bytes, no máximo {max}" }, "More is on the way": "Há mais a caminho", + "Multisig share": "Parte da assinatura multiassinatura", + "Multisig signers": "Signatários multiassinatura", + "Multisig signing is not available in this app.": "A assinatura multiassinatura não está disponível neste app.", "My own prover · {name}": "Meu próprio provador · {name}", "Name": "Nome", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Nomes para os endereços que você paga com frequência, guardados apenas neste dispositivo. Envie a um contato pelo nome; a impressão digital dele é verificada quando você o salva.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Nenhum contato ainda. Salve um em Configurações ou cole um endereço.", "no endpoint could answer {method}": "nenhum endpoint conseguiu responder a {method}", "no endpoint could answer {method}; tried {endpoints}": "nenhum endpoint conseguiu responder a {method}; tentados: {endpoints}", + "no expected recipient given: the payee was NOT checked": "nenhum destinatário esperado informado: o beneficiário NÃO foi verificado", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Nenhum nó respondeu em {url}, então a expiração não pode ser verificada e esta proposta não é assinada. Tente de novo quando o nó estiver acessível.", "No proof is pending.": "Nenhuma prova pendente.", "No prover": "Nenhum provador", "No prover is paired.": "Nenhum provador pareado.", "No RAND to send yet": "Ainda não há RAND para enviar", "No registry assets": "Nenhum ativo no registro", "no reply": "sem resposta", + "No signer profiles yet": "Ainda não há perfis de signatário", + "No such signer profile.": "Não existe esse perfil de signatário.", "No transaction or block matches that.": "Nenhuma transação ou bloco corresponde a isso.", "no wallet on this device": "nenhuma carteira neste dispositivo", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Nenhum dos links de provador da RandProtocol embutidos indica a chave que esta carteira fixou para ele; eles não serão usados.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Uma prova roda neste dispositivo — cerca de dois minutos. Você pode ver outras telas; fechar a carteira a interrompe.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "só há {locked} bloqueado nessa moeda na cadeia {chain}; escolha outro lastro ou um valor menor", "Open durian.market": "Abrir durian.market", + "Open file": "Abrir arquivo", "Open in explorer": "Abrir no explorador", "Open in randscan": "Abrir no randscan", "Open in side panel": "Abrir no painel lateral", + "Open in the side panel to choose files": "Abra no painel lateral para escolher arquivos", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Abra a Rand Wallet com {label} em vez de digitar sua senha. Sua senha continua funcionando e segue sendo o meio de recuperar o acesso.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Abra a carteira com {label} em vez de digitar sua senha. Sua senha continua funcionando e segue sendo o meio de recuperar o acesso.", "Open this window from a site that asked to connect.": "Abra esta janela a partir de um site que pediu para se conectar.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Ou use os provadores da RandProtocol — {n} máquina operada pelos validadores, com sua própria chave.", many: "Ou use os provadores da RandProtocol — {n} máquinas operadas pelos validadores, cada uma com sua própria chave.", other: "Ou use os provadores da RandProtocol — {n} máquinas operadas pelos validadores, cada uma com sua própria chave." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Ou use os provadores da RandProtocol — máquinas operadas pelos validadores, cada uma com sua própria chave.", "Outcome unknown": "Resultado desconhecido", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "a saída {slot} não abre como desta conta: recusa-se a assinar um valor que não consegue ver", "own={value}, expected 1 or absent": "own={value}, esperado 1 ou ausente", "Paid": "Pago", "Pair your own prover": "Parear seu próprio provador", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "Cole sua chave de recuperação", "path is not {n} levels": { one: "o caminho não tem {n} nível", many: "o caminho não tem {n} níveis", other: "o caminho não tem {n} níveis" }, "Pay me on Rand": "Pague-me na Rand", + "Payee address": "Endereço do beneficiário", "Payment link": "Link de pagamento", "Payment link · fingerprint {fingerprint}": "Link de pagamento · impressão digital {fingerprint}", "Payment link copied": "Link de pagamento copiado", "Payment links cannot be read in this app; paste the address instead.": "Links de pagamento não podem ser lidos neste app; cole o endereço.", "Payment links cannot be read in this app.": "Links de pagamento não podem ser lidos neste app.", + "Payments changed: check them again before signing.": "Os pagamentos mudaram: verifique-os de novo antes de assinar.", "Peers": "Pares", "Pending": "Pendente", "Permission to reach that host was not granted, so the node was left as it was.": "A permissão para acessar esse host não foi concedida, então o nó ficou como estava.", "Permission to reach that host was not granted.": "A permissão para acessar esse host não foi concedida.", "Permission to reach that prover was not granted, so nothing was saved.": "A permissão para acessar esse provador não foi concedida, então nada foi salvo.", "Point the camera at a Rand QR code.": "Aponte a câmera para um QR code Rand.", + "Policy file": "Arquivo de política", "Pool fee": "Taxa do pool", "Price impact": "Impacto no preço", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Preço dos pools durian.market nesta cadeia (taxa do pool de {pct}%). A troca é uma transação que esta carteira prova e envia ela mesma.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "As provas são geradas apenas neste dispositivo. Quando ele não conseguir gerar uma, o envio espera até você parear um provador ou usar de novo o provador da RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "As provas que este dispositivo não consegue gerar vão para {name}. A impressão digital dele é {fingerprint} — confira se o provador mostra a mesma.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "As provas que este dispositivo não consegue gerar vão para um deles; cada um que prova um envio vê a chave de visualização dessa carteira.", + "Proposal (proposal.json)": "Proposta (proposal.json)", "Prove and send": "Provar e enviar", "Prove for my other devices": "Provar para meus outros dispositivos", "Prover": "Provador", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "Taxa do retransmissor (opcional)", "Remove": "Remover", "Remove {name}": "Remover {name}", + "Remove this signer": "Remover este signatário", "Request RAND": "Pedir RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Peça RAND ao faucet para experimentar enviar, receber e provar nesta rede.", "Rescan": "Reescanear", @@ -574,8 +623,12 @@ export default { "Running on": "Rodando em", "Save": "Salvar", "Save an address you pay often, and send to it by name.": "Salve um endereço que você paga com frequência e envie para ele pelo nome.", + "Save as file": "Salvar como arquivo", + "Save signer": "Salvar signatário", "Save your recovery key": "Salve sua chave de recuperação", "Saved": "Salvo", + "Saved to {path}": "Salvo em {path}", + "Saved.": "Salvo.", "Scan": "Escanear", "Scan a payment link": "Escanear um link de pagamento", "Scan a QR code": "Escanear um QR code", @@ -596,6 +649,7 @@ export default { "Sent.": "Enviado.", "Set on this device when this wallet was created.": "Definida neste dispositivo quando esta carteira foi criada.", "Settings": "Configurações", + "Share": "Parte da assinatura", "Share payment link": "Compartilhar link de pagamento", "shielded address decodes to {n} bytes, expected {expected}": { one: "o endereço blindado decodifica para {n} byte, esperado {expected}", many: "o endereço blindado decodifica para {n} bytes, esperado {expected}", other: "o endereço blindado decodifica para {n} bytes, esperado {expected}" }, "shielded address is not base58": "o endereço blindado não é base58", @@ -607,6 +661,12 @@ export default { "Show the full address": "Mostrar o endereço completo", "Show viewing key": "Mostrar chave de visualização", "Show viewing key for 10 seconds": "Mostrar chave de visualização por 10 segundos", + "Sign": "Assinar", + "Sign a proposal": "Assinar uma proposta", + "signer {index} is not in this policy (it has {count} signers)": "o signatário {index} não está nesta política (ela tem {count} signatários)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "A parte da assinatura do signatário {index} é válida. {have} de {threshold} em mãos: signatários {list}.", + "signer {index}'s signature does not verify over this proposal": "a assinatura do signatário {index} não é válida para esta proposta", + "Signer key file, or its 64-hex seed": "Arquivo de chave do signatário, ou sua seed de 64 hex", "So the address above is one you have actually read.": "Para que o endereço acima seja um que você realmente leu.", "Something went wrong": "Algo deu errado", "Something went wrong.": "Algo deu errado.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "Não foi possível copiar.", "That could not be saved.": "Não foi possível salvar.", "That does not match — reveal the key again and check.": "Não corresponde — revele a chave de novo e confira.", + "That file is larger than a proposal, a policy or a key file can be.": "Esse arquivo é maior do que uma proposta, uma política ou um arquivo de chave pode ser.", "That is more than your balance of {balance} {symbol}.": "Isso é mais que seu saldo de {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Isso não é nem 64 caracteres hexadecimais (um hash) nem uma altura de bloco (só dígitos).", "That is not a pairing link.": "Isso não é um link de pareamento.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "Não foi possível calcular a taxa.", "the genesis hash is not hex of a plausible length": "o hash de gênese não é hexadecimal de um tamanho plausível", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "a entrada é `none` exatamente quando o pacote não queima nenhum token (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "o endereço do arquivo de chave não é o que sua seed deriva", + "the key file's public_key is not the one its seed derives": "o public_key do arquivo de chave não é o que sua seed deriva", "The key you exported when this wallet was created.": "A chave que você exportou quando esta carteira foi criada.", "The language could not be saved.": "Não foi possível salvar o idioma.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "O link pede {linked} {symbol}; você digitou {typed} {symbol}.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "A senha que desbloqueia esta carteira neste dispositivo.", "The payer decides": "Quem paga decide", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "A prova pendente foi descartada (a carteira foi bloqueada ou ela foi cancelada em outro lugar), então nada foi enviado. Envie de novo.", + "the policy does not parse: {reason}": "a política não pôde ser lida: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "O pool mudou desde que esta página o leu. Nada foi enviado; o site pode cotar de novo.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "O pool mudou enquanto isto era provado. Nada foi enviado; o site pode cotar de novo.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "O pool não tem o suficiente para pagar isto. Nada foi enviado; o site pode cotar de novo.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "a prova não é válida: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "a prova publicou um digest que esta carteira não montou ({published} em vez de {expected}); ela não será usada", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "A prova roda neste dispositivo. Você pode ver outras telas — ela continua —, mas fechar a carteira a interrompe.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "A verificação da proposta devolveu uma resposta incompleta, então esta proposta não é assinada.", + "the proposal does not parse: {reason}": "a proposta não pôde ser lida: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "a proposta expira em {expires}, fora da janela do seu bundle ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "o auth_commit da proposta não é a conta desta política", + "the proposal's expected digest is not its bundle's": "o digest esperado da proposta não é o do seu bundle", + "the proposal's signing_message is not its binding's": "o signing_message da proposta não é o do seu vínculo", "the prover": "o provador", "the prover accepted the job but named no job id": "o provador aceitou o trabalho, mas não informou um id de trabalho", "The prover answered with an unknown state ({state}).": "O provador respondeu com um estado desconhecido ({state}).", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "A solicitação não foi aprovada na Rand Wallet.", "The rescan could not be started.": "Não foi possível iniciar o reescaneamento.", "The rescan did not finish": "O reescaneamento não terminou", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "os perfis de signatário salvos não puderam ser abertos; nada foi alterado. Restaure o armazenamento deste dispositivo ou apague a carteira e restaure-a a partir da sua chave", "The scan was cancelled.": "A leitura foi cancelada.", "The side panel could not be opened.": "Não foi possível abrir o painel lateral.", "The site sent an invoke this wallet cannot read: {reason}.": "O site enviou uma invocação que esta carteira não consegue ler: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "A troca não foi concluída.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "A troca continua. Fechar a carteira antes de aparecer enviado a interrompe, e nada se perde.", "The theme could not be saved.": "Não foi possível salvar o tema.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "a transação carrega uma ação; uma proposta multiassinatura privada é um pagamento apenas do seu bundle", + "the transaction carries no bundle": "a transação não carrega nenhum bundle", + "the transaction does not hash to the proposal's binding": "o hash da transação não é o vínculo da proposta", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "a transação é para a cadeia {tx}, a proposta diz a cadeia {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "A chave da transação revela este único pagamento — o valor e o destinatário — a quem você a der. Ela não pode gastar nada.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "A transação talvez já esteja na cadeia. Veja a Atividade antes de sacar de novo — sacar duas vezes queimaria duas vezes.", "The transfer could not be proved.": "Não foi possível provar a transferência.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "A carteira recebeu: {message}", "The withdrawal could not be proved.": "Não foi possível provar o saque.", "The withdrawal was submitted, but the node did not return a usable hash.": "O saque foi enviado, mas o nó não devolveu um hash utilizável.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "o valor do witness não pode ser verificado: seu nk ou salt não é o desta proposta", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "o valor do witness não confere com as saídas: o ativo {asset} tem {held} de entrada e {spent} de saída com a taxa e as queimas", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "o valor do witness não confere com a transação: a entrada {input} não gera o nullifier do bundle", "Theme": "Tema", "There is no pool for that pair.": "Não há pool para esse par.", "There is no program {program}… on this chain. Nothing was sent.": "Não há programa {program}… nesta cadeia. Nada foi enviado.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "Este dispositivo ou, quando ele não puder · provadores {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Isto esquece as notas e o histórico lidos da cadeia antiga e lê este nó desde o início. Suas chaves e sua senha não são alteradas.", "This key can spend your funds": "Esta chave pode gastar seus fundos", + "this key is not one of the policy's signers": "esta chave não é de nenhum dos signatários da política", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Esta chave é a sua carteira. Qualquer pessoa que a tenha pode gastar seus fundos, e sem ela um dispositivo perdido significa fundos perdidos. Você pode vê-la de novo em Configurações com sua senha.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Isto sai do pool blindado. O endereço de destino e o valor se tornam públicos na outra cadeia.", "This link is too long for a QR code — copy it instead.": "Este link é longo demais para um QR code — copie-o.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "Esta nota não foi encontrada.", "This passkey does not support the PRF extension.": "Esta chave de acesso não suporta a extensão PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Esta prova já foi enviada de outra janela. Veja a Atividade antes de enviar de novo.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Esta proposta expira na altura {expires} e a cadeia está em {head}: tarde demais para assinar. Peça uma nova proposta.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Esta proposta está vinculada a um genesis diferente do que o nó desta carteira serve.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "esta proposta é de outra política: nomeia {theirs}, esta política é {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Esta proposta é para a cadeia {theirs}; esta carteira está na cadeia {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "este provador cobra {fee} RAND por prova, e esta versão da carteira não paga taxa de provador: pareie um provador que não cobre nada ou prove neste dispositivo", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Este provador cobra uma taxa, que esta versão da carteira não paga. Pareie um provador que não cobre nada ou envie pelo app para computador.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Este provador não aceita este tipo de trabalho ({reason}). Pareie outro provador em Configurações ou envie pelo app para computador.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Isto remove a carteira deste dispositivo. Você vai precisar da sua chave de recuperação para restaurá-la.", "This screen could not be shown.": "Não foi possível mostrar esta tela.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Esta tela chega em uma versão futura. Sua carteira está desbloqueada e pronta.", + "this signer is already saved as {label}": "este signatário já está salvo como {label}", + "This signer profile has no key on this device: remove it and add it again.": "Este perfil de signatário não tem chave neste dispositivo: remova-o e adicione-o de novo.", "This tab is showing what it has so far, and will refresh when that finishes.": "Esta aba mostra o que tem até agora e vai se atualizar quando isso terminar.", "This takes a few minutes; keep this window open.": "Isto leva alguns minutos; mantenha esta janela aberta.", "This transaction was not found.": "Esta transação não foi encontrada.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "Digite {word} para confirmar", "Type it once more.": "Digite mais uma vez.", "Type the last four characters of that address": "Digite os quatro últimos caracteres desse endereço", + "Type the last four characters of the account fingerprint": "Digite os últimos quatro caracteres da impressão digital da conta", "unavailable in this app": "indisponível neste app", "unknown": "desconhecido", "unknown parameter {name}": "parâmetro desconhecido {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "Fraca", "Welcome back": "Boas-vindas de volta", "What the pool pays you arrives as a note your wallet finds on its next sync.": "O que o pool paga a você chega como uma nota que sua carteira encontra na próxima sincronização.", + "What this proposal does, as checked against your policy": "O que esta proposta faz, verificado contra a sua política", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Quando este dispositivo não conseguir gerar uma prova, pareie um provador. Sua chave de gasto fica aqui de qualquer forma; um provador que você mesmo roda — o app para computador ou o rand-prover na sua própria máquina — também mantém seu histórico só para você.", "Where to?": "Para onde?", "Which address?": "Qual endereço?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Seu provador não terminou após {n} minuto de prova. Retome mais tarde ou cancele.", many: "Seu provador não terminou após {n} minutos de prova. Retome mais tarde ou cancele.", other: "Seu provador não terminou após {n} minutos de prova. Retome mais tarde ou cancele." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Seu provador não começou esta prova após {n} minuto na fila. Retome mais tarde ou cancele.", many: "Seu provador não começou esta prova após {n} minutos na fila. Retome mais tarde ou cancele.", other: "Seu provador não começou esta prova após {n} minutos na fila. Retome mais tarde ou cancele." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Não foi possível abrir o pareamento do seu provador. Bloqueie e desbloqueie a carteira ou pareie o provador de novo em Configurações.", + "Your share — send it to whoever proves the proposal": "Sua parte da assinatura — envie-a a quem prova a proposta", "Your wallet picks up what it pays you on its next sync.": "Sua carteira recebe o que ele paga a você na próxima sincronização.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "O histórico da sua carteira foi lido de {chain} — nada foi alterado, e esta carteira não vai enviar nem pedir fundos até que as duas coincidam. Troque de nó em Configurações ou reescaneie esta carteira para a nova cadeia.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Esse é o endereço dos provadores {name}. Eles não precisam de pareamento: esta carteira os usa por padrão.", diff --git a/ui/locales/ru.js b/ui/locales/ru.js index 73bf48c..43e26cb 100644 --- a/ui/locales/ru.js +++ b/ui/locales/ru.js @@ -6,6 +6,7 @@ // shielded — экранированный, fingerprint — отпечаток, memo — мемо, pair — подключить (сопряжение). export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "Осталось {blocks} блоков, чтобы доказать это.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} пир", few: "{count} пира", many: "{count} пиров", other: "{count} пира" }, "{count} peers": { one: "{count} пир", few: "{count} пира", many: "{count} пиров", other: "{count} пира" }, @@ -34,6 +35,7 @@ export default { "{label} is off": "{label}: выключено", "{label} is on": "{label}: включено", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} больше не открывает этот кошелёк (его пароль изменился). Разблокируйте его паролем, затем снова включите {label} в настройках.", + "{matched} of {total} payments matched.": "Совпало платежей: {matched} из {total}.", "{member} answered with another key than the one this wallet pins": "{member} ответил другим ключом, а не тем, который закреплён в этом кошельке", "{member} did not answer": "{member} не ответил", "{member} does not take this wallet's jobs": "{member} не принимает задания этого кошелька", @@ -54,6 +56,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} нельзя вывести", "{symbol} has {decimals} decimal places — that is more.": "У {symbol} знаков после запятой: {decimals} — здесь больше.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} выплачивается в выбранной вами монете, в собственной сети этой монеты.", + "{threshold} of {count} · signer {index}": "{threshold} из {count} · подписант {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold} из {count} · этот ключ — подписант {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "У {token} в сети {chain} знаков после запятой: {decimals}; сумма и комиссия релейера должны быть кратны {unit}", "{units} units of asset {asset}": "{units} ед. актива {asset}", "{url} answered first — chain {chain} · height {height}.": "{url} ответил первым — сеть {chain} · высота {height}.", @@ -86,7 +90,12 @@ export default { "A prover address must be https://.": "Адрес прувера должен начинаться с https://.", "A prover is making the proof": "Доказательство строит прувер", "A prover sees your history": "Прувер видит вашу историю", + "a share is index:hex": "доля подписи имеет вид index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Экранированный кошелёк для сети Rand. Балансы и история остаются приватными — доказательство для каждой отправки строится на этом устройстве.", + "a signer key is a rand-node keygen file or 64 hex characters": "ключ подписанта — это файл keygen от rand-node или 64 шестнадцатеричных символа", + "a signer label is 1-64 characters": "метка подписанта — 1-64 символа", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "Профиль подписанта хранится в зашифрованном хранилище этого кошелька. Каждая подпись снова запрашивает ваш пароль.", + "a signer seed is 64 hex characters": "seed подписанта — 64 шестнадцатеричных символа", "A site": "Сайт", "a spend key is 64 hex characters, or a wallet.key.json": "ключ расходования — это 64 шестнадцатеричных символа или файл wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "Сначала ищется транзакция с этим хешем; если ничего не найдено — блок с тем же хешем. Строка из одних цифр ищет блок по высоте.", @@ -103,8 +112,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "Вывод в сеть, которой нет в списке, может быть отклонён. Ничего не тратится, пока доказательство не построено.", "About": "О приложении", "about {left} left": "осталось около {left}", + "Account fingerprint — compare it with every other signer before you save": "Отпечаток аккаунта — сверьте его с каждым другим подписантом, прежде чем сохранять", "Activity": "Активность", "Add a contact": "Добавить контакт", + "Add a signer": "Добавить подписанта", + "Add payment": "Добавить платёж", + "Add the policy file and your signer key to sign for a private multisig account.": "Добавьте файл политики и ключ подписанта, чтобы подписывать за приватный мультиподписной аккаунт.", "Address": "Адрес", "Address copied": "Адрес скопирован", "Address or payment link": "Адрес или платёжная ссылка", @@ -113,14 +126,18 @@ export default { "almost done": "почти готово", "also tried {endpoints}": "также пробовали {endpoints}", "Amount": "Сумма", + "Amount in RAND": "Сумма в RAND", "amount must be greater than zero": "сумма должна быть больше нуля", "amount overflow": "переполнение суммы", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "Суммы нельзя проверить для платежей не в RAND: оставьте для них сумму пустой.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "Этот получатель получает и RAND, и токен: платежи в RAND и в токенах одному получателю требуют отдельных предложений.", "amounts overflow": "переполнение сумм", "an action is {n} characters, more than {max}": { one: "действие длиной {n} символ, больше чем {max}", few: "действие длиной {n} символа, больше чем {max}", many: "действие длиной {n} символов, больше чем {max}", other: "действие длиной {n} символа, больше чем {max}" }, "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "Адрес (rand1…), платёжная ссылка (randpay:…) или имя сохранённого контакта.", "An address here is hexadecimal — 0-9 and a-f only.": "Адрес здесь шестнадцатеричный — только 0-9 и a-f.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "Адрес в этой сети — 20 байт, то есть 40 шестнадцатеричных символов. Здесь {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "Адрес в этой сети — 32 байта, то есть 64 шестнадцатеричных символа. Здесь {length}.", + "An amount is a number of RAND, such as 1.5.": "Сумма — это число RAND, например 1.5.", "an unknown chain": "неизвестная сеть", "another tab changed this wallet while it was syncing; try again": "другая вкладка изменила кошелёк во время синхронизации; попробуйте снова", "Another tab is syncing": "Синхронизация идёт в другой вкладке", @@ -187,11 +204,16 @@ export default { "Chains known": "Известные сети", "Change back to you": "Сдача вам", "Change it": "Изменить", + "Check": "Проверить", + "Check a share": "Проверить долю подписи", "Check Activity": "Проверить активность", "Check Activity first": "Сначала проверьте активность", "Check before saving": "Проверьте перед сохранением", "Check fingerprint": "Проверить отпечаток", + "Check payments": "Проверить платежи", + "Check proposal": "Проверить предложение", "Check settings": "Проверить настройки", + "Check share": "Проверить долю подписи", "Checking it against the chain before anything is proved.": "Проверяем по блокчейну, прежде чем что-либо доказывать.", "Checking the pool and your notes…": "Проверяем пул и ваши ноты…", "Checking the swap…": "Проверяем обмен…", @@ -218,6 +240,7 @@ export default { "Continue": "Продолжить", "Copied": "Скопировано", "Copied as JSON": "Скопировано как JSON", + "Copy": "Копировать", "Copy address": "Копировать адрес", "Copy as JSON": "Копировать как JSON", "Copy link": "Копировать ссылку", @@ -266,9 +289,13 @@ export default { "Enter an amount to send.": "Введите сумму для отправки.", "Enter an amount.": "Введите сумму.", "Enter at least 10 characters": "Введите не менее 10 символов", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "Введите каждый платёж, который вы собираетесь сделать: адрес и сумму в RAND. Платёж на один и тот же адрес дважды вводится один раз, с итоговой суммой. Выход, не совпадающий ни с одним из них, подписать нельзя.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "Введите адрес каждого получателя: выход, не совпадающий с введённым адресом или с пустым адресом, подписать нельзя.", + "Enter the address of every payment.": "Введите адрес каждого платежа.", "Enter the address this should be released to.": "Введите адрес, на который нужно выплатить средства.", "Enter the address you are sending to.": "Введите адрес получателя.", "Enter the amount as a number, for example 1.25.": "Введите сумму числом, например 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "Введите сумму каждого платежа: именно она не даёт разбить платёж или завысить его.", "Enter the first 4 characters of your recovery key.": "Введите первые 4 символа ключа восстановления.", "Enter the relayer fee as a number, for example 1.25.": "Введите комиссию релейера числом, например 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "Введите пароль этого кошелька — сопряжение зашифровано им.", @@ -278,6 +305,8 @@ export default { "Enter your recovery key.": "Введите ключ восстановления.", "Epoch": "Эпоха", "Exactly those four letters, in capitals.": "Именно эти четыре буквы, заглавными.", + "expected recipient {address}: {reason}": "ожидаемый получатель {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients должен быть списком адресов", "Explore": "Обзор", "Export spend key": "Экспорт ключа расходования", "Exporting the spend key is behind your password.": "Для экспорта ключа расходования нужен пароль.", @@ -286,6 +315,7 @@ export default { "Fee": "Комиссия", "fee must be at least {fee} RAND (the bridge burn floor)": "комиссия должна быть не меньше {fee} RAND (минимум для сжигания через мост)", "fee must be at least {fee} RAND (the bundle floor)": "комиссия должна быть не меньше {fee} RAND (минимум для пакета)", + "Fee: {fee} RAND · expires at height {height}": "Комиссия: {fee} RAND · истекает на высоте {height}", "Filter activity by asset": "Фильтр активности по активу", "Fingerprint": "Отпечаток", "fingerprint {fp}": "отпечаток {fp}", @@ -323,9 +353,11 @@ export default { "I understand anyone with this key can spend my funds": "Я понимаю, что любой, у кого есть этот ключ, может потратить мои средства", "Import wallet": "Импортировать кошелёк", "Incorrect password.": "Неверный пароль.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "во входах {held}, а сумма + комиссия составляет {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "во входах {held}, а комиссия + депозит RAND составляет {need}", "insufficient balance: have {have}, need {need}": "недостаточно средств: есть {have}, нужно {need}", + "Intended payments": "Предполагаемые платежи", "is on chain {chain}, not {expected}": "находится в сети {chain}, а не {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "Он не видит ваш баланс и активность и ничего не может переместить.", "It cannot spend.": "Тратить им нельзя.", @@ -348,6 +380,7 @@ export default { "key file has no spend_key": "в файле ключа нет spend_key", "key file must be version 2": "файл ключа должен быть версии 2", "Key hidden — enter your password to view it again.": "Ключ скрыт — введите пароль, чтобы снова его увидеть.", + "Label": "Метка", "Language": "Язык", "Leave empty to let the payer choose.": "Оставьте пустым, чтобы выбрал плательщик.", "Light": "Светлая", @@ -358,11 +391,15 @@ export default { "machines": "машины", "malformed parameter {parameter}": "некорректный параметр {parameter}", "Manage contacts": "Управление контактами", + "Manage multisig signers": "Управление подписантами мультиподписи", "Max": "Макс.", "Memo": "Мемо", "memo \"{memo}\"": "мемо «{memo}»", "memo is {n} bytes, at most {max}": { one: "мемо занимает {n} байт, допустимо не более {max}", few: "мемо занимает {n} байта, допустимо не более {max}", many: "мемо занимает {n} байт, допустимо не более {max}", other: "мемо занимает {n} байта, допустимо не более {max}" }, "More is on the way": "Ожидаются ещё поступления", + "Multisig share": "Доля мультиподписи", + "Multisig signers": "Подписанты мультиподписи", + "Multisig signing is not available in this app.": "Мультиподпись недоступна в этом приложении.", "My own prover · {name}": "Мой прувер · {name}", "Name": "Имя", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "Имена для адресов, на которые вы часто платите; хранятся только на этом устройстве. Отправляйте контакту по имени — его отпечаток проверяется при сохранении.", @@ -386,12 +423,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "Контактов пока нет. Сохраните контакт в настройках или вставьте адрес.", "no endpoint could answer {method}": "ни один адрес не смог ответить на {method}", "no endpoint could answer {method}; tried {endpoints}": "ни один адрес не смог ответить на {method}; пробовали {endpoints}", + "no expected recipient given: the payee was NOT checked": "ожидаемый получатель не указан: получатель НЕ проверялся", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "Ни один узел не ответил по адресу {url}, поэтому срок действия проверить нельзя, и это предложение не подписано. Повторите, когда узел станет доступен.", "No proof is pending.": "Ожидающих доказательств нет.", "No prover": "Нет прувера", "No prover is paired.": "Прувер не подключён.", "No RAND to send yet": "Пока нет RAND для отправки", "No registry assets": "Нет активов в реестре", "no reply": "нет ответа", + "No signer profiles yet": "Профилей подписантов пока нет", + "No such signer profile.": "Такого профиля подписанта нет.", "No transaction or block matches that.": "Ни одна транзакция или блок не соответствует запросу.", "no wallet on this device": "на этом устройстве нет кошелька", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "Ни одна из встроенных ссылок на пруверы RandProtocol не указывает ключ, закреплённый в этом кошельке; они не используются.", @@ -438,9 +479,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "Одно доказательство строится на этом устройстве — около двух минут. Можно переходить на другие экраны; закрытие кошелька его остановит.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "в этой монете в сети {chain} заблокировано только {locked}; выберите другое обеспечение или меньшую сумму", "Open durian.market": "Открыть durian.market", + "Open file": "Открыть файл", "Open in explorer": "Открыть в обозревателе", "Open in randscan": "Открыть в randscan", "Open in side panel": "Открыть в боковой панели", + "Open in the side panel to choose files": "Откройте в боковой панели, чтобы выбрать файлы", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Открывайте Rand Wallet через {label} вместо ввода пароля. Пароль по-прежнему работает и остаётся способом восстановить доступ.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "Открывайте кошелёк через {label} вместо ввода пароля. Пароль по-прежнему работает и остаётся способом восстановить доступ.", "Open this window from a site that asked to connect.": "Откройте это окно с сайта, который запросил подключение.", @@ -450,6 +493,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "Или используйте пруверы RandProtocol — {n} машина под управлением валидаторов, со своим ключом.", few: "Или используйте пруверы RandProtocol — {n} машины под управлением валидаторов, у каждой свой ключ.", many: "Или используйте пруверы RandProtocol — {n} машин под управлением валидаторов, у каждой свой ключ.", other: "Или используйте пруверы RandProtocol — {n} машины под управлением валидаторов, у каждой свой ключ." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "Или используйте пруверы RandProtocol — машины под управлением валидаторов, у каждой свой ключ.", "Outcome unknown": "Результат неизвестен", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "выход {slot} не открывается как принадлежащий этому аккаунту: отказ подписывать значение, которое нельзя увидеть", "own={value}, expected 1 or absent": "own={value}, ожидается 1 или отсутствие", "Paid": "Оплачено", "Pair your own prover": "Подключите свой прувер", @@ -470,17 +514,20 @@ export default { "Paste your recovery key": "Вставьте ключ восстановления", "path is not {n} levels": { one: "путь не из {n} уровня", few: "путь не из {n} уровней", many: "путь не из {n} уровней", other: "путь не из {n} уровня" }, "Pay me on Rand": "Заплатите мне в Rand", + "Payee address": "Адрес получателя", "Payment link": "Платёжная ссылка", "Payment link · fingerprint {fingerprint}": "Платёжная ссылка · отпечаток {fingerprint}", "Payment link copied": "Платёжная ссылка скопирована", "Payment links cannot be read in this app; paste the address instead.": "Это приложение не читает платёжные ссылки; вставьте адрес.", "Payment links cannot be read in this app.": "Это приложение не читает платёжные ссылки.", + "Payments changed: check them again before signing.": "Платежи изменились: проверьте их снова перед подписью.", "Peers": "Пиры", "Pending": "В ожидании", "Permission to reach that host was not granted, so the node was left as it was.": "Разрешение на доступ к этому хосту не выдано, поэтому узел не изменён.", "Permission to reach that host was not granted.": "Разрешение на доступ к этому хосту не выдано.", "Permission to reach that prover was not granted, so nothing was saved.": "Разрешение на доступ к этому пруверу не выдано, поэтому ничего не сохранено.", "Point the camera at a Rand QR code.": "Наведите камеру на QR-код Rand.", + "Policy file": "Файл политики", "Pool fee": "Комиссия пула", "Price impact": "Влияние на цену", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "Цена по пулам durian.market в этой сети (комиссия пула {pct}%). Обмен — это транзакция, которую кошелёк сам доказывает и отправляет.", @@ -490,6 +537,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "Доказательства строятся только на этом устройстве. Если оно не может построить доказательство, отправка ждёт, пока вы не подключите прувер или снова не выберете прувер RandProtocol.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "Доказательства, которые это устройство построить не может, отправляются на {name}. Его отпечаток — {fingerprint}; проверьте, что прувер показывает такой же.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "Доказательства, которые это устройство построить не может, отправляются одному из них; каждый, кто строит доказательство для отправки, видит ключ просмотра этого кошелька.", + "Proposal (proposal.json)": "Предложение (proposal.json)", "Prove and send": "Доказать и отправить", "Prove for my other devices": "Строить доказательства для моих других устройств", "Prover": "Прувер", @@ -538,6 +586,7 @@ export default { "Relayer fee (optional)": "Комиссия релейера (необязательно)", "Remove": "Удалить", "Remove {name}": "Удалить {name}", + "Remove this signer": "Удалить этого подписанта", "Request RAND": "Запросить RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "Запросите RAND из крана, чтобы попробовать отправку, получение и построение доказательств в этой сети.", "Rescan": "Пересканировать", @@ -572,8 +621,12 @@ export default { "Running on": "Работает на", "Save": "Сохранить", "Save an address you pay often, and send to it by name.": "Сохраните адрес, на который часто платите, и отправляйте на него по имени.", + "Save as file": "Сохранить как файл", + "Save signer": "Сохранить подписанта", "Save your recovery key": "Сохраните ключ восстановления", "Saved": "Сохранено", + "Saved to {path}": "Сохранено в {path}", + "Saved.": "Сохранено.", "Scan": "Сканировать", "Scan a payment link": "Сканировать платёжную ссылку", "Scan a QR code": "Сканировать QR-код", @@ -594,6 +647,7 @@ export default { "Sent.": "Отправлено.", "Set on this device when this wallet was created.": "Задан на этом устройстве при создании кошелька.", "Settings": "Настройки", + "Share": "Доля подписи", "Share payment link": "Поделиться платёжной ссылкой", "shielded address decodes to {n} bytes, expected {expected}": { one: "экранированный адрес декодируется в {n} байт, ожидается {expected}", few: "экранированный адрес декодируется в {n} байта, ожидается {expected}", many: "экранированный адрес декодируется в {n} байт, ожидается {expected}", other: "экранированный адрес декодируется в {n} байта, ожидается {expected}" }, "shielded address is not base58": "экранированный адрес не в base58", @@ -605,6 +659,12 @@ export default { "Show the full address": "Показать полный адрес", "Show viewing key": "Показать ключ просмотра", "Show viewing key for 10 seconds": "Показать ключ просмотра на 10 секунд", + "Sign": "Подписать", + "Sign a proposal": "Подписать предложение", + "signer {index} is not in this policy (it has {count} signers)": "подписанта {index} нет в этой политике (в ней {count} подписантов)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "Доля подписи подписанта {index} верна. Собрано {have} из {threshold}: подписанты {list}.", + "signer {index}'s signature does not verify over this proposal": "подпись подписанта {index} не верна для этого предложения", + "Signer key file, or its 64-hex seed": "Файл ключа подписанта или его seed из 64 hex-символов", "So the address above is one you have actually read.": "Чтобы адрес выше был действительно прочитан вами.", "Something went wrong": "Что-то пошло не так", "Something went wrong.": "Что-то пошло не так.", @@ -637,6 +697,7 @@ export default { "That could not be copied.": "Не удалось скопировать.", "That could not be saved.": "Не удалось сохранить.", "That does not match — reveal the key again and check.": "Не совпадает — покажите ключ снова и проверьте.", + "That file is larger than a proposal, a policy or a key file can be.": "Этот файл больше, чем может быть предложение, политика или файл ключа.", "That is more than your balance of {balance} {symbol}.": "Это больше вашего баланса: {balance} {symbol}.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "Это не 64 шестнадцатеричных символа (хеш) и не высота блока (только цифры).", "That is not a pairing link.": "Это не ссылка для сопряжения.", @@ -672,6 +733,8 @@ export default { "The fee could not be worked out.": "Не удалось рассчитать комиссию.", "the genesis hash is not hex of a plausible length": "хеш генезиса — не шестнадцатеричная строка правдоподобной длины", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow равен `none` ровно тогда, когда пакет не сжигает токен (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "адрес в файле ключа не тот, что выводится из его seed", + "the key file's public_key is not the one its seed derives": "public_key в файле ключа не тот, что выводится из его seed", "The key you exported when this wallet was created.": "Ключ, который вы экспортировали при создании кошелька.", "The language could not be saved.": "Не удалось сохранить язык.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "Ссылка запрашивает {linked} {symbol}; вы ввели {typed} {symbol}.", @@ -712,6 +775,7 @@ export default { "The password that unlocks this wallet on this device.": "Пароль, который разблокирует кошелёк на этом устройстве.", "The payer decides": "Решает плательщик", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "Ожидающее доказательство сброшено (кошелёк заблокировался или его отменили в другом месте), поэтому ничего не отправлено. Отправьте снова.", + "the policy does not parse: {reason}": "политику не удалось разобрать: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "Пул изменился с тех пор, как страница его прочитала. Ничего не отправлено; сайт может выдать новую котировку.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "Пул изменился, пока строилось доказательство. Ничего не отправлено; сайт может выдать новую котировку.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "В пуле недостаточно средств для этой выплаты. Ничего не отправлено; сайт может выдать новую котировку.", @@ -722,6 +786,12 @@ export default { "the proof does not verify: {reason}": "доказательство не проходит проверку: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "доказательство опубликовало дайджест, который кошелёк не строил ({published} вместо {expected}); оно не используется", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "Доказательство строится на этом устройстве. Можно переходить на другие экраны — оно продолжится, — но закрытие кошелька его остановит.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "Проверка предложения вернула неполный ответ, поэтому это предложение не подписано.", + "the proposal does not parse: {reason}": "предложение не удалось разобрать: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "предложение истекает на {expires}, вне окна его bundle ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "auth_commit предложения — не аккаунт этой политики", + "the proposal's expected digest is not its bundle's": "ожидаемый digest предложения — не digest его bundle", + "the proposal's signing_message is not its binding's": "signing_message предложения — не signing_message его привязки", "the prover": "прувер", "the prover accepted the job but named no job id": "прувер принял задание, но не назвал его ID", "The prover answered with an unknown state ({state}).": "Прувер ответил неизвестным состоянием ({state}).", @@ -783,6 +853,7 @@ export default { "The request was not approved in Rand Wallet.": "Запрос не был одобрен в Rand Wallet.", "The rescan could not be started.": "Не удалось запустить пересканирование.", "The rescan did not finish": "Пересканирование не завершилось", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "сохранённые профили подписантов не удалось открыть; ничего не изменено. Восстановите хранилище этого устройства или сотрите кошелёк и восстановите его из ключа", "The scan was cancelled.": "Сканирование отменено.", "The side panel could not be opened.": "Не удалось открыть боковую панель.", "The site sent an invoke this wallet cannot read: {reason}.": "Сайт прислал вызов, который кошелёк не может прочитать: {reason}.", @@ -793,6 +864,10 @@ export default { "The swap did not go through.": "Обмен не прошёл.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "Обмен продолжается. Если закрыть кошелёк до появления «Отправлено», обмен остановится, но ничего не будет потеряно.", "The theme could not be saved.": "Не удалось сохранить тему.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "транзакция несёт действие; приватное мультиподписное предложение — это платёж только из его bundle", + "the transaction carries no bundle": "транзакция не несёт bundle", + "the transaction does not hash to the proposal's binding": "хеш транзакции — не привязка предложения", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "транзакция предназначена для сети {tx}, а предложение указывает сеть {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "Ключ транзакции раскрывает этот один платёж — сумму и получателя — тому, кому вы его передадите. Тратить им ничего нельзя.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "Транзакция, возможно, уже в блокчейне. Проверьте активность, прежде чем выводить снова, — повторный вывод сожжёт средства дважды.", "The transfer could not be proved.": "Не удалось построить доказательство перевода.", @@ -810,6 +885,9 @@ export default { "The wallet was told: {message}": "Кошелёк получил ответ: {message}", "The withdrawal could not be proved.": "Не удалось построить доказательство вывода.", "The withdrawal was submitted, but the node did not return a usable hash.": "Вывод отправлен, но узел не вернул пригодный хеш.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "значение witness проверить нельзя: его nk или salt — не от этого предложения", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "значение witness не совпадает с выходами: у актива {asset} на входе {held}, на выходе {spent} с комиссией и сжиганиями", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "значение witness не совпадает с транзакцией: вход {input} не даёт nullifier bundle", "Theme": "Тема", "There is no pool for that pair.": "Для этой пары нет пула.", "There is no program {program}… on this chain. Nothing was sent.": "В этой сети нет программы {program}… Ничего не отправлено.", @@ -851,6 +929,7 @@ export default { "This device, or where it cannot · {name} provers": "Это устройство, а где оно не может · пруверы {name}", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "Ноты и история, прочитанные из старой сети, будут забыты, а этот узел прочитан с начала. Ключи и пароль не затрагиваются.", "This key can spend your funds": "Этим ключом можно потратить ваши средства", + "this key is not one of the policy's signers": "этот ключ не входит в число подписантов политики", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "Этот ключ и есть ваш кошелёк. Любой, у кого он есть, может потратить ваши средства, а без него потеря устройства означает потерю средств. Его можно снова посмотреть в настройках, введя пароль.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "Средства покидают экранированный пул. Адрес назначения и сумма станут публичными в другой сети.", "This link is too long for a QR code — copy it instead.": "Ссылка слишком длинная для QR-кода — скопируйте её.", @@ -866,6 +945,10 @@ export default { "This note was not found.": "Эта нота не найдена.", "This passkey does not support the PRF extension.": "Этот ключ доступа не поддерживает расширение PRF.", "This proof was already submitted from another window. Check Activity before sending again.": "Это доказательство уже отправлено из другого окна. Проверьте активность, прежде чем отправлять снова.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "Это предложение истекает на высоте {expires}, а сеть на {head}: подписывать поздно. Запросите новое предложение.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "Это предложение привязано к другому genesis, чем тот, который обслуживает узел этого кошелька.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "это предложение для другой политики: в нём указано {theirs}, эта политика — {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "Это предложение для сети {theirs}; этот кошелёк в сети {ours}.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "этот прувер берёт {fee} RAND за доказательство, а эта версия кошелька не платит комиссию прувера: подключите бесплатный прувер или стройте доказательства на этом устройстве", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "Этот прувер берёт комиссию, а эта версия кошелька её не платит. Подключите бесплатный прувер или отправьте из настольного приложения.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "Этот прувер не принимает такие задания ({reason}). Подключите другой прувер в настройках или отправьте из настольного приложения.", @@ -877,6 +960,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "Кошелёк будет удалён с этого устройства. Для восстановления понадобится ключ восстановления.", "This screen could not be shown.": "Не удалось показать этот экран.", "This screen ships in a later task. Your wallet is unlocked and ready.": "Этот экран появится в следующей версии. Кошелёк разблокирован и готов к работе.", + "this signer is already saved as {label}": "этот подписант уже сохранён как {label}", + "This signer profile has no key on this device: remove it and add it again.": "У этого профиля подписанта нет ключа на этом устройстве: удалите его и добавьте снова.", "This tab is showing what it has so far, and will refresh when that finishes.": "Вкладка показывает то, что уже есть, и обновится, когда процесс завершится.", "This takes a few minutes; keep this window open.": "Это занимает несколько минут; не закрывайте окно.", "This transaction was not found.": "Эта транзакция не найдена.", @@ -923,6 +1008,7 @@ export default { "Type {word} to confirm": "Введите {word} для подтверждения", "Type it once more.": "Введите ещё раз.", "Type the last four characters of that address": "Введите последние четыре символа этого адреса", + "Type the last four characters of the account fingerprint": "Введите последние четыре символа отпечатка аккаунта", "unavailable in this app": "недоступно в этом приложении", "unknown": "неизвестно", "unknown parameter {name}": "неизвестный параметр {name}", @@ -959,6 +1045,7 @@ export default { "Weak": "Слабый", "Welcome back": "С возвращением", "What the pool pays you arrives as a note your wallet finds on its next sync.": "Выплата из пула поступит в виде ноты, которую кошелёк найдёт при следующей синхронизации.", + "What this proposal does, as checked against your policy": "Что делает это предложение — проверено по вашей политике", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "Если это устройство не может построить доказательство, подключите прувер. Ключ расходования в любом случае остаётся здесь; прувер, который вы запускаете сами — настольное приложение или rand-prover на вашей машине, — к тому же сохраняет вашу историю только у вас.", "Where to?": "Куда?", "Which address?": "Какой адрес?", @@ -1005,6 +1092,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "Ваш прувер не закончил доказательство за {n} минуту. Продолжите позже или отмените.", few: "Ваш прувер не закончил доказательство за {n} минуты. Продолжите позже или отмените.", many: "Ваш прувер не закончил доказательство за {n} минут. Продолжите позже или отмените.", other: "Ваш прувер не закончил доказательство за {n} минуты. Продолжите позже или отмените." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "Ваш прувер не начал это доказательство после {n} минуты в очереди. Продолжите позже или отмените.", few: "Ваш прувер не начал это доказательство после {n} минут в очереди. Продолжите позже или отмените.", many: "Ваш прувер не начал это доказательство после {n} минут в очереди. Продолжите позже или отмените.", other: "Ваш прувер не начал это доказательство после {n} минуты в очереди. Продолжите позже или отмените." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "Не удалось открыть сопряжение с вашим прувером. Заблокируйте и разблокируйте кошелёк или подключите прувер заново в настройках.", + "Your share — send it to whoever proves the proposal": "Ваша доля подписи — отправьте её тому, кто доказывает предложение", "Your wallet picks up what it pays you on its next sync.": "Кошелёк получит выплату при следующей синхронизации.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "История кошелька прочитана из сети {chain} — ничего не изменено, и кошелёк не будет отправлять или запрашивать средства, пока сети не совпадут. Смените узел в настройках или пересканируйте кошелёк для новой сети.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "Это адрес пруверов {name}. Подключать их не нужно: кошелёк использует их по умолчанию.", diff --git a/ui/locales/ta.js b/ui/locales/ta.js index af24d3d..82112f9 100644 --- a/ui/locales/ta.js +++ b/ui/locales/ta.js @@ -8,6 +8,7 @@ // pair — இணை, transfer — பரிமாற்றம், transaction — பரிவர்த்தனை, sync — ஒத்திசை, burn — எரிப்பு. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "இதை நிரூபிக்க இன்னும் {blocks} பிளாக்குகள் உள்ளன.", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": { one: "{count} பியர்", other: "{count} பியர்கள்" }, "{count} peers": { one: "{count} பியர்", other: "{count} பியர்கள்" }, @@ -36,6 +37,7 @@ export default { "{label} is off": "{label} அணைக்கப்பட்டுள்ளது", "{label} is on": "{label} இயக்கத்தில் உள்ளது", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} இனி இந்த வாலட்டைத் திறக்காது (அதன் கடவுச்சொல் மாறிவிட்டது). கடவுச்சொல்லால் திறந்து, பிறகு அமைப்புகளில் {label} ஐ மீண்டும் இயக்கவும்.", + "{matched} of {total} payments matched.": "{total} கட்டணங்களில் {matched} பொருந்துகின்றன.", "{member} answered with another key than the one this wallet pins": "இந்த வாலட் பதிவு செய்து வைத்துள்ள விசைக்குப் பதிலாக வேறு ஒரு விசையுடன் {member} பதிலளித்தது", "{member} did not answer": "{member} பதிலளிக்கவில்லை", "{member} does not take this wallet's jobs": "{member} இந்த வாலட்டின் பணிகளை ஏற்பதில்லை", @@ -56,6 +58,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} ஐத் திரும்பப் பெற முடியாது", "{symbol} has {decimals} decimal places — that is more.": "{symbol} க்கு {decimals} தசம இடங்கள் மட்டுமே உள்ளன — இது அதைவிட அதிகம்.", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} நீங்கள் தேர்ந்தெடுக்கும் நாணயமாக, அந்த நாணயத்தின் சொந்த செயினில் விடுவிக்கப்படும்.", + "{threshold} of {count} · signer {index}": "{count}-இல் {threshold} · கையொப்பமிடுபவர் {index}", + "{threshold} of {count} · this key is signer {index}": "{count}-இல் {threshold} · இந்த விசை கையொப்பமிடுபவர் {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "செயின் {chain} இல் உள்ள {token} க்கு {decimals} தசமங்கள் உள்ளன: தொகையும் ரிலேயர் கட்டணமும் {unit} இன் மடங்குகளாக இருக்க வேண்டும்", "{units} units of asset {asset}": "சொத்து {asset} இன் {units} அலகுகள்", "{url} answered first — chain {chain} · height {height}.": "{url} முதலில் பதிலளித்தது — செயின் {chain} · உயரம் {height}.", @@ -88,7 +92,12 @@ export default { "A prover address must be https://.": "நிரூபிப்பான் முகவரி https:// ஆக இருக்க வேண்டும்.", "A prover is making the proof": "ஒரு நிரூபிப்பான் நிரூபணத்தை உருவாக்குகிறது", "A prover sees your history": "நிரூபிப்பான் உங்கள் வரலாற்றைப் பார்க்கும்", + "a share is index:hex": "கையொப்பப் பங்கு index:hex வடிவில் இருக்கும்", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand நெட்வொர்க்குக்கான பாதுகாக்கப்பட்ட வாலட். இருப்புகளும் வரலாறும் தனிப்பட்டவையாகவே இருக்கும் — ஒவ்வொரு அனுப்புதலும் இந்தச் சாதனத்திலேயே நிரூபிக்கப்படுகிறது.", + "a signer key is a rand-node keygen file or 64 hex characters": "கையொப்பமிடுபவர் விசை என்பது rand-node keygen கோப்பு அல்லது 64 ஹெக்ஸ் எழுத்துகள்", + "a signer label is 1-64 characters": "கையொப்பமிடுபவர் லேபிள் 1-64 எழுத்துகள்", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "கையொப்பமிடுபவர் சுயவிவரம் இந்த வாலட்டின் என்கிரிப்ட் செய்யப்பட்ட பெட்டகத்தில் வைக்கப்படும். ஒவ்வொரு கையொப்பமும் உங்கள் கடவுச்சொல்லை மீண்டும் கேட்கும்.", + "a signer seed is 64 hex characters": "கையொப்பமிடுபவர் சீட் 64 ஹெக்ஸ் எழுத்துகள்", "A site": "ஒரு தளம்", "a spend key is 64 hex characters, or a wallet.key.json": "செலவு விசை என்பது 64 hex எழுத்துகள், அல்லது ஒரு wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "முதலில் பரிவர்த்தனை ஹாஷாக முயலப்படும்; எதுவும் கிடைக்காவிட்டால், அதே ஹாஷ் ஒரு பிளாக்காக முயலப்படும். எல்லாம் இலக்கங்களாக இருந்தால் உயரத்தின் மூலம் பிளாக் தேடப்படும்.", @@ -105,8 +114,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "அது பட்டியலிடாத ஒன்றுக்குத் திரும்பப் பெறுதல் மறுக்கப்படலாம். நிரூபிக்கப்படும் வரை எதுவும் செலவாகாது.", "About": "பற்றி", "about {left} left": "சுமார் {left} மீதம்", + "Account fingerprint — compare it with every other signer before you save": "கணக்கு கைரேகை — சேமிப்பதற்கு முன் மற்ற ஒவ்வொரு கையொப்பமிடுபவருடனும் ஒப்பிடுங்கள்", "Activity": "செயல்பாடு", "Add a contact": "தொடர்பைச் சேர்", + "Add a signer": "கையொப்பமிடுபவரைச் சேர்", + "Add payment": "கட்டணத்தைச் சேர்", + "Add the policy file and your signer key to sign for a private multisig account.": "தனிப்பட்ட மல்டிசிக் கணக்கிற்குக் கையொப்பமிட, கொள்கைக் கோப்பையும் உங்கள் கையொப்பமிடுபவர் விசையையும் சேருங்கள்.", "Address": "முகவரி", "Address copied": "முகவரி நகலெடுக்கப்பட்டது", "Address or payment link": "முகவரி அல்லது பணம்செலுத்தல் இணைப்பு", @@ -115,14 +128,18 @@ export default { "almost done": "கிட்டத்தட்ட முடிந்தது", "also tried {endpoints}": "{endpoints} ஆகியவையும் முயலப்பட்டன", "Amount": "தொகை", + "Amount in RAND": "RAND-இல் தொகை", "amount must be greater than zero": "தொகை பூஜ்ஜியத்தை விட அதிகமாக இருக்க வேண்டும்", "amount overflow": "தொகை வரம்பை மீறியது", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "RAND அல்லாத கட்டணங்களின் தொகையைச் சரிபார்க்க முடியாது: அவற்றுக்குத் தொகையை வெறுமையாக விடுங்கள்.", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "இந்தப் பெறுநர் RAND மற்றும் ஒரு டோக்கன் இரண்டையும் பெறுகிறார்: ஒரே பெறுநருக்கான RAND மற்றும் டோக்கன் கொடுப்பனவுகளுக்குத் தனித்தனி முன்மொழிவுகள் தேவை.", "amounts overflow": "தொகைகள் வரம்பை மீறின", "an action is {n} characters, more than {max}": "ஒரு செயல் {n} எழுத்துகள் கொண்டது, இது {max} ஐ விட அதிகம்", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "ஒரு முகவரி (rand1…), ஒரு பணம்செலுத்தல் இணைப்பு (randpay:…), அல்லது சேமித்த தொடர்பின் பெயர்.", "An address here is hexadecimal — 0-9 and a-f only.": "இங்கே முகவரி hexadecimal ஆகும் — 0-9 மற்றும் a-f மட்டும்.", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "இந்த செயினில் ஒரு முகவரி 20 பைட்டுகள் — 40 hex எழுத்துகள். இது {length}.", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "இந்த செயினில் ஒரு முகவரி 32 பைட்டுகள் — 64 hex எழுத்துகள். இது {length}.", + "An amount is a number of RAND, such as 1.5.": "தொகை என்பது RAND-இன் எண், எடுத்துக்காட்டாக 1.5.", "an unknown chain": "அறியப்படாத செயின்", "another tab changed this wallet while it was syncing; try again": "ஒத்திசைக்கும்போது வேறொரு தாவல் இந்த வாலட்டை மாற்றியது; மீண்டும் முயலவும்", "Another tab is syncing": "வேறொரு தாவல் ஒத்திசைக்கிறது", @@ -189,11 +206,16 @@ export default { "Chains known": "அறியப்பட்ட செயின்கள்", "Change back to you": "உங்களுக்குத் திரும்பும் மீதம்", "Change it": "அதை மாற்று", + "Check": "சரிபார்", + "Check a share": "கையொப்பப் பங்கைச் சரிபார்", "Check Activity": "செயல்பாட்டைச் சரிபார்", "Check Activity first": "முதலில் செயல்பாட்டைச் சரிபார்க்கவும்", "Check before saving": "சேமிக்கும் முன் சரிபார்க்கவும்", "Check fingerprint": "கைரேகையைச் சரிபார்", + "Check payments": "கட்டணங்களைச் சரிபார்", + "Check proposal": "முன்மொழிவைச் சரிபார்", "Check settings": "அமைப்புகளைச் சரிபார்", + "Check share": "கையொப்பப் பங்கைச் சரிபார்", "Checking it against the chain before anything is proved.": "எதையும் நிரூபிக்கும் முன் செயினுடன் ஒப்பிட்டுச் சரிபார்க்கிறது.", "Checking the pool and your notes…": "பூலையும் உங்கள் நோட்டுகளையும் சரிபார்க்கிறது…", "Checking the swap…": "ஸ்வாப்பைச் சரிபார்க்கிறது…", @@ -220,6 +242,7 @@ export default { "Continue": "தொடர்", "Copied": "நகலெடுக்கப்பட்டது", "Copied as JSON": "JSON ஆக நகலெடுக்கப்பட்டது", + "Copy": "நகலெடு", "Copy address": "முகவரியை நகலெடு", "Copy as JSON": "JSON ஆக நகலெடு", "Copy link": "இணைப்பை நகலெடு", @@ -268,9 +291,13 @@ export default { "Enter an amount to send.": "அனுப்ப வேண்டிய தொகையை உள்ளிடவும்.", "Enter an amount.": "ஒரு தொகையை உள்ளிடவும்.", "Enter at least 10 characters": "குறைந்தது 10 எழுத்துகளை உள்ளிடவும்", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "நீங்கள் செய்யவிருக்கும் ஒவ்வொரு கட்டணத்தையும் உள்ளிடுங்கள்: முகவரியும் RAND-இல் தொகையும். ஒரே முகவரிக்கு இருமுறை செலுத்தும் கட்டணம் மொத்தத்துடன் ஒருமுறை உள்ளிடப்படும். இவற்றில் எதனுடனும் பொருந்தாத வெளியீட்டில் கையொப்பமிட முடியாது.", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "பெறுபவர் ஒவ்வொருவரின் முகவரியையும் உள்ளிடுங்கள்: நீங்கள் உள்ளிட்ட முகவரியுடன் பொருந்தாத அல்லது முகவரி வெறுமையாக உள்ள வெளியீட்டில் கையொப்பமிட முடியாது.", + "Enter the address of every payment.": "ஒவ்வொரு கட்டணத்தின் முகவரியையும் உள்ளிடுங்கள்.", "Enter the address this should be released to.": "இது விடுவிக்கப்பட வேண்டிய முகவரியை உள்ளிடவும்.", "Enter the address you are sending to.": "நீங்கள் அனுப்பும் முகவரியை உள்ளிடவும்.", "Enter the amount as a number, for example 1.25.": "தொகையை எண்ணாக உள்ளிடவும், எடுத்துக்காட்டாக 1.25.", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "ஒவ்வொரு கட்டணத்தின் தொகையையும் உள்ளிடுங்கள்: பிரித்தலையோ பெருக்கிய கட்டணத்தையோ தடுப்பது இதுவே.", "Enter the first 4 characters of your recovery key.": "உங்கள் மீட்பு விசையின் முதல் 4 எழுத்துகளை உள்ளிடவும்.", "Enter the relayer fee as a number, for example 1.25.": "ரிலேயர் கட்டணத்தை எண்ணாக உள்ளிடவும், எடுத்துக்காட்டாக 1.25.", "Enter this wallet's password — the pairing is sealed under it.": "இந்த வாலட்டின் கடவுச்சொல்லை உள்ளிடவும் — இணைப்பு அதன் கீழ் பூட்டி வைக்கப்படுகிறது.", @@ -280,6 +307,8 @@ export default { "Enter your recovery key.": "உங்கள் மீட்பு விசையை உள்ளிடவும்.", "Epoch": "எபோக்", "Exactly those four letters, in capitals.": "அதே நான்கு எழுத்துகள், பெரிய எழுத்துகளில்.", + "expected recipient {address}: {reason}": "எதிர்பார்க்கப்படும் பெறுநர் {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients முகவரிகளின் பட்டியலாக இருக்க வேண்டும்", "Explore": "ஆராய்", "Export spend key": "செலவு விசையை ஏற்றுமதி செய்", "Exporting the spend key is behind your password.": "செலவு விசையை ஏற்றுமதி செய்ய உங்கள் கடவுச்சொல் தேவை.", @@ -288,6 +317,7 @@ export default { "Fee": "கட்டணம்", "fee must be at least {fee} RAND (the bridge burn floor)": "கட்டணம் குறைந்தது {fee} RAND ஆக இருக்க வேண்டும் (பால எரிப்பின் குறைந்தபட்சம்)", "fee must be at least {fee} RAND (the bundle floor)": "கட்டணம் குறைந்தது {fee} RAND ஆக இருக்க வேண்டும் (தொகுப்பின் குறைந்தபட்சம்)", + "Fee: {fee} RAND · expires at height {height}": "கட்டணம்: {fee} RAND · உயரம் {height}-இல் காலாவதியாகும்", "Filter activity by asset": "சொத்தின்படி செயல்பாட்டை வடிகட்டு", "Fingerprint": "கைரேகை", "fingerprint {fp}": "கைரேகை {fp}", @@ -325,9 +355,11 @@ export default { "I understand anyone with this key can spend my funds": "இந்த விசை உள்ள எவரும் என் நிதியைச் செலவிட முடியும் என்பதைப் புரிந்துகொண்டேன்", "Import wallet": "வாலட்டை இறக்குமதி செய்", "Incorrect password.": "தவறான கடவுச்சொல்.", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "உள்ளீடுகளில் {held} உள்ளது, ஆனால் தொகை + கட்டணம் {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "உள்ளீடுகளில் {held} உள்ளது, ஆனால் கட்டணம் + RAND வைப்பு {need}", "insufficient balance: have {have}, need {need}": "போதிய இருப்பு இல்லை: உள்ளது {have}, தேவை {need}", + "Intended payments": "நோக்கிய கட்டணங்கள்", "is on chain {chain}, not {expected}": "செயின் {chain} இல் உள்ளது, {expected} இல் அல்ல", "It cannot see your balance or your activity, and it cannot move anything.": "அதனால் உங்கள் இருப்பையோ செயல்பாட்டையோ பார்க்க முடியாது, எதையும் நகர்த்தவும் முடியாது.", "It cannot spend.": "அதனால் செலவிட முடியாது.", @@ -350,6 +382,7 @@ export default { "key file has no spend_key": "விசைக் கோப்பில் spend_key இல்லை", "key file must be version 2": "விசைக் கோப்பு பதிப்பு 2 ஆக இருக்க வேண்டும்", "Key hidden — enter your password to view it again.": "விசை மறைக்கப்பட்டது — மீண்டும் பார்க்க உங்கள் கடவுச்சொல்லை உள்ளிடவும்.", + "Label": "லேபிள்", "Language": "மொழி", "Leave empty to let the payer choose.": "பணம் செலுத்துபவர் தேர்ந்தெடுக்கக் காலியாக விடுங்கள்.", "Light": "வெளிர்", @@ -360,11 +393,15 @@ export default { "machines": "கணினிகள்", "malformed parameter {parameter}": "தவறான வடிவிலான அளவுரு {parameter}", "Manage contacts": "தொடர்புகளை நிர்வகி", + "Manage multisig signers": "மல்டிசிக் கையொப்பமிடுபவர்களை நிர்வகி", "Max": "அதிகபட்சம்", "Memo": "குறிப்பு", "memo \"{memo}\"": "குறிப்பு \"{memo}\"", "memo is {n} bytes, at most {max}": "குறிப்பு {n} பைட்டுகள், அதிகபட்சம் {max}", "More is on the way": "மேலும் வந்துகொண்டிருக்கிறது", + "Multisig share": "மல்டிசிக் கையொப்பப் பங்கு", + "Multisig signers": "மல்டிசிக் கையொப்பமிடுபவர்கள்", + "Multisig signing is not available in this app.": "இந்த ஆப்பில் மல்டிசிக் கையொப்பம் கிடைக்காது.", "My own prover · {name}": "என் சொந்த நிரூபிப்பான் · {name}", "Name": "பெயர்", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "நீங்கள் அடிக்கடி பணம் செலுத்தும் முகவரிகளுக்கான பெயர்கள், இந்தச் சாதனத்தில் மட்டுமே வைக்கப்படும். ஒரு தொடர்புக்குப் பெயரால் அனுப்பலாம்; சேமிக்கும்போது அதன் கைரேகை சரிபார்க்கப்படும்.", @@ -388,12 +425,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "இன்னும் தொடர்புகள் இல்லை. அமைப்புகளிலிருந்து ஒன்றைச் சேமிக்கவும், அல்லது ஒரு முகவரியை ஒட்டவும்.", "no endpoint could answer {method}": "எந்த முனையத்தாலும் {method} க்குப் பதிலளிக்க முடியவில்லை", "no endpoint could answer {method}; tried {endpoints}": "எந்த முனையத்தாலும் {method} க்குப் பதிலளிக்க முடியவில்லை; முயன்றவை {endpoints}", + "no expected recipient given: the payee was NOT checked": "எதிர்பார்க்கப்படும் பெறுநர் கொடுக்கப்படவில்லை: பெறுநர் சரிபார்க்கப்படவில்லை (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url}-இல் எந்த நோட்டும் பதிலளிக்கவில்லை, எனவே காலாவதியைச் சரிபார்க்க முடியாது; இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை. நோட் எட்டக்கூடியதாகும்போது மீண்டும் முயலுங்கள்.", "No proof is pending.": "எந்த நிரூபணமும் நிலுவையில் இல்லை.", "No prover": "நிரூபிப்பான் இல்லை", "No prover is paired.": "எந்த நிரூபிப்பானும் இணைக்கப்படவில்லை.", "No RAND to send yet": "அனுப்ப இன்னும் RAND இல்லை", "No registry assets": "பதிவேட்டுச் சொத்துகள் இல்லை", "no reply": "பதில் இல்லை", + "No signer profiles yet": "கையொப்பமிடுபவர் சுயவிவரங்கள் இன்னும் இல்லை", + "No such signer profile.": "அப்படி ஒரு கையொப்பமிடுபவர் சுயவிவரம் இல்லை.", "No transaction or block matches that.": "அதனுடன் பொருந்தும் பரிவர்த்தனையோ பிளாக்கோ இல்லை.", "no wallet on this device": "இந்தச் சாதனத்தில் வாலட் இல்லை", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "உள்ளமைந்த RandProtocol நிரூபிப்பான் இணைப்புகள் எதுவும் இந்த வாலட் பதிவு செய்து வைத்துள்ள விசையைக் குறிப்பிடவில்லை; அவை பயன்படுத்தப்படவில்லை.", @@ -440,9 +481,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "ஒரு நிரூபணம் இந்தச் சாதனத்தில் இயங்கும் — சுமார் இரண்டு நிமிடங்கள். நீங்கள் மற்ற திரைகளைப் பார்க்கலாம்; வாலட்டை மூடினால் அது நின்றுவிடும்.", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "செயின் {chain} இல் அந்த நாணயத்தில் {locked} மட்டுமே பூட்டப்பட்டுள்ளது; வேறொரு ஆதாரத்தையோ சிறிய தொகையையோ தேர்ந்தெடுக்கவும்", "Open durian.market": "durian.market ஐத் திற", + "Open file": "கோப்பைத் திற", "Open in explorer": "எக்ஸ்ப்ளோரரில் திற", "Open in randscan": "randscan இல் திற", "Open in side panel": "பக்கப் பலகத்தில் திற", + "Open in the side panel to choose files": "கோப்புகளைத் தேர்ந்தெடுக்க பக்கப் பலகையில் திறக்கவும்", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "உங்கள் கடவுச்சொல்லைத் தட்டச்சு செய்வதற்குப் பதிலாக {label} மூலம் Rand Wallet ஐத் திறக்கவும். உங்கள் கடவுச்சொல் இன்னும் வேலை செய்யும், அணுகலை மீட்டெடுக்கும் வழியாகவும் அதுவே இருக்கும்.", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "உங்கள் கடவுச்சொல்லைத் தட்டச்சு செய்வதற்குப் பதிலாக {label} மூலம் வாலட்டைத் திறக்கவும். உங்கள் கடவுச்சொல் இன்னும் வேலை செய்யும், அணுகலை மீட்டெடுக்கும் வழியாகவும் அதுவே இருக்கும்.", "Open this window from a site that asked to connect.": "இணையக் கேட்ட ஒரு தளத்திலிருந்து இந்தச் சாளரத்தைத் திறக்கவும்.", @@ -452,6 +495,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": { one: "அல்லது RandProtocol நிரூபிப்பான்களைப் பயன்படுத்தவும் — சரிபார்ப்பாளர்கள் இயக்கும் {n} கணினி, அதன் சொந்த விசையுடன்.", other: "அல்லது RandProtocol நிரூபிப்பான்களைப் பயன்படுத்தவும் — சரிபார்ப்பாளர்கள் இயக்கும் {n} கணினிகள், ஒவ்வொன்றும் அதன் சொந்த விசையுடன்." }, "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "அல்லது RandProtocol நிரூபிப்பான்களைப் பயன்படுத்தவும் — சரிபார்ப்பாளர்கள் இயக்கும் கணினிகள், ஒவ்வொன்றும் அதன் சொந்த விசையுடன்.", "Outcome unknown": "முடிவு தெரியவில்லை", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "வெளியீடு {slot} இந்தக் கணக்கினுடையதாகத் திறக்கவில்லை: பார்க்க முடியாத மதிப்புக்குக் கையொப்பமிட மறுக்கப்படுகிறது", "own={value}, expected 1 or absent": "own={value}, 1 அல்லது இல்லாமை எதிர்பார்க்கப்பட்டது", "Paid": "செலுத்தப்பட்டது", "Pair your own prover": "உங்கள் சொந்த நிரூபிப்பானை இணை", @@ -472,17 +516,20 @@ export default { "Paste your recovery key": "உங்கள் மீட்பு விசையை ஒட்டவும்", "path is not {n} levels": "பாதை {n} நிலைகள் அல்ல", "Pay me on Rand": "Rand இல் எனக்குப் பணம் செலுத்துங்கள்", + "Payee address": "பெறுபவர் முகவரி", "Payment link": "பணம்செலுத்தல் இணைப்பு", "Payment link · fingerprint {fingerprint}": "பணம்செலுத்தல் இணைப்பு · கைரேகை {fingerprint}", "Payment link copied": "பணம்செலுத்தல் இணைப்பு நகலெடுக்கப்பட்டது", "Payment links cannot be read in this app; paste the address instead.": "இந்தச் செயலியில் பணம்செலுத்தல் இணைப்புகளைப் படிக்க முடியாது; அதற்குப் பதிலாக முகவரியை ஒட்டவும்.", "Payment links cannot be read in this app.": "இந்தச் செயலியில் பணம்செலுத்தல் இணைப்புகளைப் படிக்க முடியாது.", + "Payments changed: check them again before signing.": "கட்டணங்கள் மாறின: கையொப்பமிடும் முன் மீண்டும் சரிபாருங்கள்.", "Peers": "பியர்கள்", "Pending": "நிலுவையில்", "Permission to reach that host was not granted, so the node was left as it was.": "அந்த ஹோஸ்டை அணுக அனுமதி வழங்கப்படவில்லை, எனவே நோட் முன்பு இருந்தபடியே விடப்பட்டது.", "Permission to reach that host was not granted.": "அந்த ஹோஸ்டை அணுக அனுமதி வழங்கப்படவில்லை.", "Permission to reach that prover was not granted, so nothing was saved.": "அந்த நிரூபிப்பானை அணுக அனுமதி வழங்கப்படவில்லை, எனவே எதுவும் சேமிக்கப்படவில்லை.", "Point the camera at a Rand QR code.": "கேமராவை ஒரு Rand QR குறியீட்டை நோக்கிக் காட்டவும்.", + "Policy file": "கொள்கைக் கோப்பு", "Pool fee": "பூல் கட்டணம்", "Price impact": "விலைத் தாக்கம்", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "இந்த செயினில் உள்ள durian.market பூல்களால் விலை நிர்ணயிக்கப்படுகிறது ({pct}% பூல் கட்டணம்). ஸ்வாப் என்பது இந்த வாலட் தானே நிரூபித்து அனுப்பும் ஒரு பரிவர்த்தனை.", @@ -492,6 +539,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "நிரூபணங்கள் இந்தச் சாதனத்தில் மட்டுமே உருவாக்கப்படுகின்றன. அதனால் உருவாக்க முடியாத இடத்தில், நீங்கள் ஒரு நிரூபிப்பானை இணைக்கும் வரை அல்லது மீண்டும் RandProtocol நிரூபிப்பானைப் பயன்படுத்தும் வரை அனுப்புதல் காத்திருக்கும்.", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "இந்தச் சாதனத்தால் உருவாக்க முடியாத நிரூபணங்கள் {name} க்குச் செல்லும். அதன் கைரேகை {fingerprint} — நிரூபிப்பான் அதையே காட்டுகிறதா எனச் சரிபார்க்கவும்.", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "இந்தச் சாதனத்தால் உருவாக்க முடியாத நிரூபணங்கள் அவற்றில் ஒன்றுக்குச் செல்லும்; ஒரு அனுப்புதலை நிரூபிக்கும் ஒவ்வொன்றும் அந்த வாலட்டின் பார்வை விசையைப் பார்க்கும்.", + "Proposal (proposal.json)": "முன்மொழிவு (proposal.json)", "Prove and send": "நிரூபித்து அனுப்பு", "Prove for my other devices": "என் மற்ற சாதனங்களுக்காக நிரூபி", "Prover": "நிரூபிப்பான்", @@ -540,6 +588,7 @@ export default { "Relayer fee (optional)": "ரிலேயர் கட்டணம் (விருப்பத்தேர்வு)", "Remove": "நீக்கு", "Remove {name}": "{name} ஐ நீக்கு", + "Remove this signer": "இந்தக் கையொப்பமிடுபவரை நீக்கு", "Request RAND": "RAND கோரு", "Request RAND from the faucet to try sending, receiving and proving on this network.": "இந்த நெட்வொர்க்கில் அனுப்புதல், பெறுதல், நிரூபித்தல் ஆகியவற்றை முயல ஃபாசெட்டிலிருந்து RAND கோரவும்.", "Rescan": "மீண்டும் ஸ்கேன் செய்", @@ -574,8 +623,12 @@ export default { "Running on": "இயங்குவது", "Save": "சேமி", "Save an address you pay often, and send to it by name.": "நீங்கள் அடிக்கடி பணம் செலுத்தும் முகவரியைச் சேமித்து, பெயரால் அதற்கு அனுப்புங்கள்.", + "Save as file": "கோப்பாகச் சேமி", + "Save signer": "கையொப்பமிடுபவரைச் சேமி", "Save your recovery key": "உங்கள் மீட்பு விசையைச் சேமிக்கவும்", "Saved": "சேமிக்கப்பட்டது", + "Saved to {path}": "{path}-இல் சேமிக்கப்பட்டது", + "Saved.": "சேமிக்கப்பட்டது.", "Scan": "ஸ்கேன் செய்", "Scan a payment link": "பணம்செலுத்தல் இணைப்பை ஸ்கேன் செய்", "Scan a QR code": "QR குறியீட்டை ஸ்கேன் செய்", @@ -596,6 +649,7 @@ export default { "Sent.": "அனுப்பப்பட்டது.", "Set on this device when this wallet was created.": "இந்த வாலட் உருவாக்கப்பட்டபோது இந்தச் சாதனத்தில் அமைக்கப்பட்டது.", "Settings": "அமைப்புகள்", + "Share": "கையொப்பப் பங்கு", "Share payment link": "பணம்செலுத்தல் இணைப்பைப் பகிர்", "shielded address decodes to {n} bytes, expected {expected}": "பாதுகாக்கப்பட்ட முகவரி {n} பைட்டுகளாக டிகோட் ஆகிறது, எதிர்பார்க்கப்பட்டது {expected}", "shielded address is not base58": "பாதுகாக்கப்பட்ட முகவரி base58 அல்ல", @@ -607,6 +661,12 @@ export default { "Show the full address": "முழு முகவரியைக் காட்டு", "Show viewing key": "பார்வை விசையைக் காட்டு", "Show viewing key for 10 seconds": "பார்வை விசையை 10 விநாடிகள் காட்டு", + "Sign": "கையொப்பமிடு", + "Sign a proposal": "முன்மொழிவில் கையொப்பமிடு", + "signer {index} is not in this policy (it has {count} signers)": "கையொப்பமிடுபவர் {index} இந்தக் கொள்கையில் இல்லை (இதில் {count} கையொப்பமிடுபவர்கள் உள்ளனர்)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "கையொப்பமிடுபவர் {index}-இன் கையொப்பப் பங்கு சரியானது. {threshold}-இல் {have} கையில் உள்ளன: கையொப்பமிடுபவர்கள் {list}.", + "signer {index}'s signature does not verify over this proposal": "கையொப்பமிடுபவர் {index}-இன் கையொப்பம் இந்த முன்மொழிவுக்குச் சரியானதல்ல", + "Signer key file, or its 64-hex seed": "கையொப்பமிடுபவர் விசைக் கோப்பு, அல்லது அதன் 64-ஹெக்ஸ் சீட்", "So the address above is one you have actually read.": "மேலே உள்ள முகவரி நீங்கள் உண்மையில் படித்த ஒன்று என்பதை உறுதிப்படுத்த.", "Something went wrong": "ஏதோ தவறு நடந்தது", "Something went wrong.": "ஏதோ தவறு நடந்தது.", @@ -639,6 +699,7 @@ export default { "That could not be copied.": "அதை நகலெடுக்க முடியவில்லை.", "That could not be saved.": "அதைச் சேமிக்க முடியவில்லை.", "That does not match — reveal the key again and check.": "அது பொருந்தவில்லை — விசையை மீண்டும் காட்டிச் சரிபார்க்கவும்.", + "That file is larger than a proposal, a policy or a key file can be.": "அந்தக் கோப்பு ஒரு முன்மொழிவு, கொள்கை அல்லது விசைக் கோப்பு இருக்கக்கூடியதை விடப் பெரியது.", "That is more than your balance of {balance} {symbol}.": "அது உங்கள் இருப்பான {balance} {symbol} ஐ விட அதிகம்.", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "அது 64 hex எழுத்துகளும் (ஹாஷ்) அல்ல, பிளாக் உயரமும் (இலக்கங்கள் மட்டும்) அல்ல.", "That is not a pairing link.": "அது இணைத்தல் இணைப்பு அல்ல.", @@ -674,6 +735,8 @@ export default { "The fee could not be worked out.": "கட்டணத்தைக் கணக்கிட முடியவில்லை.", "the genesis hash is not hex of a plausible length": "genesis ஹாஷ் ஏற்கத்தக்க நீளமுள்ள hex அல்ல", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "தொகுப்பு எந்த டோக்கனையும் எரிக்காதபோது மட்டுமே inflow `none` ஆக இருக்கும் (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "விசைக் கோப்பின் முகவரி அதன் சீட் வருவிப்பதல்ல", + "the key file's public_key is not the one its seed derives": "விசைக் கோப்பின் public_key அதன் சீட் வருவிப்பதல்ல", "The key you exported when this wallet was created.": "இந்த வாலட் உருவாக்கப்பட்டபோது நீங்கள் ஏற்றுமதி செய்த விசை.", "The language could not be saved.": "மொழியைச் சேமிக்க முடியவில்லை.", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "இணைப்பு {linked} {symbol} கேட்கிறது; நீங்கள் {typed} {symbol} என்று தட்டச்சு செய்தீர்கள்.", @@ -714,6 +777,7 @@ export default { "The password that unlocks this wallet on this device.": "இந்தச் சாதனத்தில் இந்த வாலட்டைத் திறக்கும் கடவுச்சொல்.", "The payer decides": "பணம் செலுத்துபவர் முடிவு செய்வார்", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "நிலுவையில் இருந்த நிரூபணம் அழிக்கப்பட்டது (வாலட் பூட்டப்பட்டது, அல்லது வேறு இடத்தில் ரத்துசெய்யப்பட்டது), எனவே எதுவும் அனுப்பப்படவில்லை. மீண்டும் அனுப்பவும்.", + "the policy does not parse: {reason}": "கொள்கையைப் பாகுபடுத்த முடியவில்லை: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "இந்தப் பக்கம் படித்த பிறகு பூல் மாறிவிட்டது. எதுவும் அனுப்பப்படவில்லை; தளம் மீண்டும் விலைமதிப்பு தரலாம்.", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "இது நிரூபிக்கப்படும்போது பூல் மாறிவிட்டது. எதுவும் அனுப்பப்படவில்லை; தளம் மீண்டும் விலைமதிப்பு தரலாம்.", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "இதைச் செலுத்தப் பூலில் போதுமான அளவு இல்லை. எதுவும் அனுப்பப்படவில்லை; தளம் மீண்டும் விலைமதிப்பு தரலாம்.", @@ -724,6 +788,12 @@ export default { "the proof does not verify: {reason}": "நிரூபணம் சரிபார்ப்பில் தேறவில்லை: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "இந்த வாலட் உருவாக்காத ஒரு digest ஐ நிரூபணம் வெளியிட்டது ({expected} க்குப் பதிலாக {published}); அது பயன்படுத்தப்படாது", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "நிரூபணம் இந்தச் சாதனத்தில் இயங்குகிறது. நீங்கள் மற்ற திரைகளைப் பார்க்கலாம் — அது தொடரும் — ஆனால் வாலட்டை மூடினால் அது நின்றுவிடும்.", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "முன்மொழிவுச் சரிபார்ப்பு முழுமையற்ற பதிலைத் தந்தது, எனவே இந்த முன்மொழிவில் கையொப்பமிடப்படவில்லை.", + "the proposal does not parse: {reason}": "முன்மொழிவைப் பாகுபடுத்த முடியவில்லை: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "முன்மொழிவு {expires}-இல் காலாவதியாகிறது, அதன் bundle-இன் சாளரத்துக்கு ({from}, {to}] வெளியே", + "the proposal's auth_commit is not this policy's account": "முன்மொழிவின் auth_commit இந்தக் கொள்கையின் கணக்கு அல்ல", + "the proposal's expected digest is not its bundle's": "முன்மொழிவின் எதிர்பார்க்கப்படும் digest அதன் bundle-இனுடையது அல்ல", + "the proposal's signing_message is not its binding's": "முன்மொழிவின் signing_message அதன் binding-இனுடையது அல்ல", "the prover": "நிரூபிப்பான்", "the prover accepted the job but named no job id": "நிரூபிப்பான் பணியை ஏற்றது, ஆனால் பணி id எதையும் குறிப்பிடவில்லை", "The prover answered with an unknown state ({state}).": "நிரூபிப்பான் அறியப்படாத நிலையுடன் ({state}) பதிலளித்தது.", @@ -785,6 +855,7 @@ export default { "The request was not approved in Rand Wallet.": "கோரிக்கை Rand Wallet இல் அங்கீகரிக்கப்படவில்லை.", "The rescan could not be started.": "மீண்டும் ஸ்கேன் செய்வதைத் தொடங்க முடியவில்லை.", "The rescan did not finish": "மீண்டும் ஸ்கேன் செய்வது முடியவில்லை", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "சேமித்த கையொப்பமிடுபவர் சுயவிவரங்களைத் திறக்க முடியவில்லை; எதுவும் மாற்றப்படவில்லை. இந்தச் சாதனத்தின் சேமிப்பை மீட்டெடுங்கள், அல்லது வாலட்டை அழித்து அதன் விசையிலிருந்து மீட்டெடுங்கள்", "The scan was cancelled.": "ஸ்கேன் ரத்துசெய்யப்பட்டது.", "The side panel could not be opened.": "பக்கப் பலகத்தைத் திறக்க முடியவில்லை.", "The site sent an invoke this wallet cannot read: {reason}.": "இந்த வாலட்டால் படிக்க முடியாத ஒரு invoke ஐத் தளம் அனுப்பியது: {reason}.", @@ -795,6 +866,10 @@ export default { "The swap did not go through.": "ஸ்வாப் நடைபெறவில்லை.", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "ஸ்வாப் தொடர்ந்து நடைபெறும். அனுப்பப்பட்டது என்று காட்டும் முன் வாலட்டை மூடினால் அது நின்றுவிடும், எதுவும் இழக்கப்படாது.", "The theme could not be saved.": "தீமைச் சேமிக்க முடியவில்லை.", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "பரிவர்த்தனை ஒரு செயலைக் கொண்டுள்ளது; தனிப்பட்ட மல்டிசிக் முன்மொழிவு என்பது அதன் bundle-இலிருந்து மட்டும் செய்யும் கட்டணம்", + "the transaction carries no bundle": "பரிவர்த்தனையில் bundle எதுவும் இல்லை", + "the transaction does not hash to the proposal's binding": "பரிவர்த்தனையின் ஹாஷ் முன்மொழிவின் binding அல்ல", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "பரிவர்த்தனை செயின் {tx}-க்கானது, முன்மொழிவு செயின் {proposal} என்கிறது", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "பரிவர்த்தனை விசை இந்த ஒரு பணம்செலுத்தலை — அதன் தொகையையும் பெறுநரையும் — நீங்கள் அதைக் கொடுக்கும் நபருக்கு வெளிப்படுத்தும். அதனால் எதையும் செலவிட முடியாது.", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "பரிவர்த்தனை ஏற்கனவே செயினில் இருக்கலாம். மீண்டும் திரும்பப் பெறுவதற்கு முன் செயல்பாட்டைச் சரிபார்க்கவும் — இருமுறை திரும்பப் பெற்றால் இருமுறை எரிக்கப்படும்.", "The transfer could not be proved.": "பரிமாற்றத்தை நிரூபிக்க முடியவில்லை.", @@ -812,6 +887,9 @@ export default { "The wallet was told: {message}": "வாலட்டுக்குத் தெரிவிக்கப்பட்டது: {message}", "The withdrawal could not be proved.": "திரும்பப் பெறுதலை நிரூபிக்க முடியவில்லை.", "The withdrawal was submitted, but the node did not return a usable hash.": "திரும்பப் பெறுதல் சமர்ப்பிக்கப்பட்டது, ஆனால் நோட் பயன்படுத்தக்கூடிய ஹாஷைத் தரவில்லை.", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "witness-இன் மதிப்பைச் சரிபார்க்க முடியவில்லை: அதன் nk அல்லது salt இந்த முன்மொழிவினுடையது அல்ல", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "witness-இன் மதிப்பு வெளியீடுகளுடன் பொருந்தவில்லை: சொத்து {asset}-இல் உள்ளீடு {held}, கட்டணம் மற்றும் எரிப்புகளுடன் வெளியீடு {spent}", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "witness-இன் மதிப்பு பரிவர்த்தனையுடன் பொருந்தவில்லை: உள்ளீடு {input} bundle-இன் nullifier-க்கு வரவில்லை", "Theme": "தீம்", "There is no pool for that pair.": "அந்த ஜோடிக்கு பூல் இல்லை.", "There is no program {program}… on this chain. Nothing was sent.": "இந்த செயினில் {program}… என்ற நிரல் இல்லை. எதுவும் அனுப்பப்படவில்லை.", @@ -853,6 +931,7 @@ export default { "This device, or where it cannot · {name} provers": "இந்தச் சாதனம், அது முடியாத இடத்தில் · {name} நிரூபிப்பான்கள்", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "பழைய செயினிலிருந்து படித்த நோட்டுகளையும் வரலாற்றையும் இது மறந்து, இந்த நோடை ஆரம்பத்திலிருந்து படிக்கும். உங்கள் விசைகளும் கடவுச்சொல்லும் மாற்றப்படாது.", "This key can spend your funds": "இந்த விசையால் உங்கள் நிதியைச் செலவிட முடியும்", + "this key is not one of the policy's signers": "இந்த விசை கொள்கையின் கையொப்பமிடுபவர்களில் ஒன்றல்ல", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "இந்த விசைதான் உங்கள் வாலட். இது உள்ள எவரும் உங்கள் நிதியைச் செலவிட முடியும், இது இல்லாவிட்டால் சாதனத்தை இழந்தால் நிதியையும் இழப்பீர்கள். உங்கள் கடவுச்சொல்லுடன் அமைப்புகளில் இதை மீண்டும் பார்க்கலாம்.", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "இது பாதுகாக்கப்பட்ட பூலை விட்டு வெளியேறும். சேருமிட முகவரியும் தொகையும் மற்ற செயினில் பொதுவில் தெரியும்.", "This link is too long for a QR code — copy it instead.": "இந்த இணைப்பு QR குறியீட்டுக்கு மிக நீளமானது — அதற்குப் பதிலாக நகலெடுக்கவும்.", @@ -868,6 +947,10 @@ export default { "This note was not found.": "இந்த நோட்டு கிடைக்கவில்லை.", "This passkey does not support the PRF extension.": "இந்தப் பாஸ்கீ PRF நீட்டிப்பை ஆதரிக்கவில்லை.", "This proof was already submitted from another window. Check Activity before sending again.": "இந்த நிரூபணம் ஏற்கனவே வேறொரு சாளரத்திலிருந்து சமர்ப்பிக்கப்பட்டது. மீண்டும் அனுப்பும் முன் செயல்பாட்டைச் சரிபார்க்கவும்.", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "இந்த முன்மொழிவு உயரம் {expires}-இல் காலாவதியாகும், செயின் {head}-இல் உள்ளது: கையொப்பமிட மிகவும் தாமதம். புதிய முன்மொழிவைக் கேளுங்கள்.", + "This proposal is bound to another genesis than the one this wallet's node serves.": "இந்த முன்மொழிவு, இந்த வாலட்டின் நோட் வழங்கும் genesis-ஐ விட வேறொன்றுடன் பிணைக்கப்பட்டுள்ளது.", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "இந்த முன்மொழிவு வேறொரு கொள்கைக்கானது: அது {theirs} என்கிறது, இந்தக் கொள்கை {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "இந்த முன்மொழிவு செயின் {theirs}-க்கானது; இந்த வாலட் செயின் {ours}-இல் உள்ளது.", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "இந்த நிரூபிப்பான் ஒவ்வொரு நிரூபணத்துக்கும் {fee} RAND வசூலிக்கிறது, வாலட்டின் இந்தப் பதிப்பு நிரூபிப்பான் கட்டணத்தைச் செலுத்தாது: கட்டணம் இல்லாத நிரூபிப்பானை இணைக்கவும், அல்லது இந்தச் சாதனத்தில் நிரூபிக்கவும்", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "இந்த நிரூபிப்பான் கட்டணம் வசூலிக்கிறது, வாலட்டின் இந்தப் பதிப்பு அதைச் செலுத்தாது. கட்டணம் இல்லாத நிரூபிப்பானை இணைக்கவும், அல்லது டெஸ்க்டாப் செயலியிலிருந்து அனுப்பவும்.", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "இந்த நிரூபிப்பான் இந்த வகைப் பணியை ஏற்பதில்லை ({reason}). அமைப்புகளில் வேறொரு நிரூபிப்பானை இணைக்கவும், அல்லது டெஸ்க்டாப் செயலியிலிருந்து அனுப்பவும்.", @@ -879,6 +962,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "இது இந்தச் சாதனத்திலிருந்து வாலட்டை நீக்கும். அதை மீட்டெடுக்க உங்கள் மீட்பு விசை தேவைப்படும்.", "This screen could not be shown.": "இந்தத் திரையைக் காட்ட முடியவில்லை.", "This screen ships in a later task. Your wallet is unlocked and ready.": "இந்தத் திரை பின்னர் வரும். உங்கள் வாலட் திறக்கப்பட்டு தயாராக உள்ளது.", + "this signer is already saved as {label}": "இந்தக் கையொப்பமிடுபவர் ஏற்கெனவே {label} என்று சேமிக்கப்பட்டுள்ளார்", + "This signer profile has no key on this device: remove it and add it again.": "இந்தக் கையொப்பமிடுபவர் சுயவிவரத்துக்கு இந்தச் சாதனத்தில் விசை இல்லை: அதை நீக்கி மீண்டும் சேருங்கள்.", "This tab is showing what it has so far, and will refresh when that finishes.": "இந்தத் தாவல் இதுவரை உள்ளதைக் காட்டுகிறது, அது முடிந்ததும் புதுப்பிக்கப்படும்.", "This takes a few minutes; keep this window open.": "இதற்குச் சில நிமிடங்கள் ஆகும்; இந்தச் சாளரத்தைத் திறந்தே வைக்கவும்.", "This transaction was not found.": "இந்தப் பரிவர்த்தனை கிடைக்கவில்லை.", @@ -925,6 +1010,7 @@ export default { "Type {word} to confirm": "உறுதிசெய்ய {word} எனத் தட்டச்சு செய்யவும்", "Type it once more.": "மீண்டும் ஒருமுறை தட்டச்சு செய்யவும்.", "Type the last four characters of that address": "அந்த முகவரியின் கடைசி நான்கு எழுத்துகளைத் தட்டச்சு செய்யவும்", + "Type the last four characters of the account fingerprint": "கணக்கு கைரேகையின் கடைசி நான்கு எழுத்துகளை உள்ளிடுங்கள்", "unavailable in this app": "இந்தச் செயலியில் கிடைக்காது", "unknown": "தெரியவில்லை", "unknown parameter {name}": "அறியப்படாத அளவுரு {name}", @@ -961,6 +1047,7 @@ export default { "Weak": "பலவீனமானது", "Welcome back": "மீண்டும் வருக", "What the pool pays you arrives as a note your wallet finds on its next sync.": "பூல் உங்களுக்குச் செலுத்துவது ஒரு நோட்டாக வந்துசேரும், அடுத்த ஒத்திசைவில் உங்கள் வாலட் அதைக் கண்டறியும்.", + "What this proposal does, as checked against your policy": "இந்த முன்மொழிவு என்ன செய்கிறது, உங்கள் கொள்கைக்கு எதிராகச் சரிபார்க்கப்பட்டது", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "இந்தச் சாதனத்தால் நிரூபணத்தை உருவாக்க முடியாத இடத்தில், ஒரு நிரூபிப்பானை இணைக்கவும். எப்படியிருந்தாலும் உங்கள் செலவு விசை இங்கேயே இருக்கும்; நீங்களே இயக்கும் நிரூபிப்பான் — டெஸ்க்டாப் செயலி, அல்லது உங்கள் சொந்தக் கணினியில் rand-prover — உங்கள் வரலாற்றையும் உங்களுக்கு மட்டுமே வைத்திருக்கும்.", "Where to?": "எங்கே?", "Which address?": "எந்த முகவரி?", @@ -1007,6 +1094,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": { one: "{n} நிமிடம் நிரூபித்த பிறகும் உங்கள் நிரூபிப்பான் முடிக்கவில்லை. பின்னர் தொடரவும், அல்லது ரத்துசெய்யவும்.", other: "{n} நிமிடங்கள் நிரூபித்த பிறகும் உங்கள் நிரூபிப்பான் முடிக்கவில்லை. பின்னர் தொடரவும், அல்லது ரத்துசெய்யவும்." }, "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": { one: "அதன் வரிசையில் {n} நிமிடம் காத்திருந்த பிறகும் உங்கள் நிரூபிப்பான் இந்த நிரூபணத்தைத் தொடங்கவில்லை. பின்னர் தொடரவும், அல்லது ரத்துசெய்யவும்.", other: "அதன் வரிசையில் {n} நிமிடங்கள் காத்திருந்த பிறகும் உங்கள் நிரூபிப்பான் இந்த நிரூபணத்தைத் தொடங்கவில்லை. பின்னர் தொடரவும், அல்லது ரத்துசெய்யவும்." }, "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "உங்கள் நிரூபிப்பானின் இணைப்பைத் திறக்க முடியவில்லை. வாலட்டைப் பூட்டித் திறக்கவும், அல்லது அமைப்புகளில் நிரூபிப்பானை மீண்டும் இணைக்கவும்.", + "Your share — send it to whoever proves the proposal": "உங்கள் கையொப்பப் பங்கு — முன்மொழிவை நிரூபிப்பவருக்கு அனுப்புங்கள்", "Your wallet picks up what it pays you on its next sync.": "அது உங்களுக்குச் செலுத்துவதை அடுத்த ஒத்திசைவில் உங்கள் வாலட் எடுத்துக்கொள்ளும்.", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "உங்கள் வாலட்டின் வரலாறு {chain} இலிருந்து படிக்கப்பட்டது — எதுவும் மாற்றப்படவில்லை, இரண்டும் பொருந்தும் வரை இந்த வாலட் நிதியை அனுப்பவோ கோரவோ செய்யாது. அமைப்புகளில் நோடை மாற்றவும், அல்லது புதிய செயினுக்காக இந்த வாலட்டை மீண்டும் ஸ்கேன் செய்யவும்.", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "இது {name} நிரூபிப்பான்களின் முகவரி. இவற்றை இணைக்கத் தேவையில்லை: இந்த வாலெட் இயல்பாகவே இவற்றைப் பயன்படுத்துகிறது.", diff --git a/ui/locales/ur.js b/ui/locales/ur.js index 08267d1..406e4bd 100644 --- a/ui/locales/ur.js +++ b/ui/locales/ur.js @@ -9,6 +9,7 @@ // the noun after a number reads naturally in one form here. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "اسے ثابت کرنے کے لیے {blocks} بلاک باقی ہیں۔", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} پیئر", "{count} peers": "{count} پیئرز", @@ -37,6 +38,7 @@ export default { "{label} is off": "{label} بند ہے", "{label} is on": "{label} آن ہے", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} اب یہ والیٹ نہیں کھولتا (اس کا پاس ورڈ بدل گیا ہے)۔ پاس ورڈ سے ان لاک کریں، پھر ترتیبات میں {label} دوبارہ آن کریں۔", + "{matched} of {total} payments matched.": "{total} میں سے {matched} ادائیگیاں مطابقت رکھتی ہیں۔", "{member} answered with another key than the one this wallet pins": "{member} نے اس کی کے بجائے کسی اور کی سے جواب دیا جو یہ والیٹ محفوظ رکھتا ہے", "{member} did not answer": "{member} نے جواب نہیں دیا", "{member} does not take this wallet's jobs": "{member} اس والیٹ کے کام نہیں لیتا", @@ -57,6 +59,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} نکالا نہیں جا سکتا", "{symbol} has {decimals} decimal places — that is more.": "{symbol} کے {decimals} اعشاری مقامات ہیں — یہ اس سے زیادہ ہے۔", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} آپ کے منتخب کردہ کوائن کی شکل میں، اسی کوائن کی اپنی چین پر جاری ہوتا ہے۔", + "{threshold} of {count} · signer {index}": "{count} میں سے {threshold} · دستخط کنندہ {index}", + "{threshold} of {count} · this key is signer {index}": "{count} میں سے {threshold} · یہ کی دستخط کنندہ {index} ہے", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "چین {chain} پر {token} کے {decimals} اعشاریے ہیں: رقم اور ریلیئر فیس {unit} کے مضرب ہونے چاہئیں", "{units} units of asset {asset}": "اثاثہ {asset} کی {units} اکائیاں", "{url} answered first — chain {chain} · height {height}.": "{url} نے سب سے پہلے جواب دیا — چین {chain} · اونچائی {height}۔", @@ -89,7 +93,12 @@ export default { "A prover address must be https://.": "پروور کا پتہ https:// ہونا چاہیے۔", "A prover is making the proof": "ایک پروور پروف بنا رہا ہے", "A prover sees your history": "پروور آپ کی تاریخ دیکھتا ہے", + "a share is index:hex": "دستخطی حصہ index:hex ہوتا ہے", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand نیٹ ورک کے لیے شیلڈڈ والیٹ۔ بیلنس اور تاریخ نجی رہتے ہیں — ہر بھیجی گئی رقم کا پروف اسی ڈیوائس پر بنتا ہے۔", + "a signer key is a rand-node keygen file or 64 hex characters": "دستخط کنندہ کی ایک rand-node keygen فائل یا 64 ہیکس حروف ہوتی ہے", + "a signer label is 1-64 characters": "دستخط کنندہ کا لیبل 1-64 حروف کا ہوتا ہے", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "دستخط کنندہ پروفائل اس والیٹ کی خفیہ کاری شدہ والٹ میں رکھی جاتی ہے۔ ہر دستخط پر آپ کا پاس ورڈ دوبارہ مانگا جاتا ہے۔", + "a signer seed is 64 hex characters": "دستخط کنندہ کا سیڈ 64 ہیکس حروف کا ہوتا ہے", "A site": "کوئی سائٹ", "a spend key is 64 hex characters, or a wallet.key.json": "اسپینڈ کی 64 ہیکس حروف کی ہوتی ہے، یا ایک wallet.key.json", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "پہلے لین دین کا ہیش آزمایا جاتا ہے؛ اگر کچھ نہ ملے تو اسی ہیش کو بلاک کے طور پر آزمایا جاتا ہے۔ صرف ہندسے ہوں تو اونچائی سے بلاک تلاش کیا جاتا ہے۔", @@ -106,8 +115,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "جو اس کی فہرست میں نہ ہو اس کی طرف رقم نکالنا رد ہو سکتا ہے۔ پروف بننے تک کچھ خرچ نہیں ہوتا۔", "About": "تعارف", "about {left} left": "تقریباً {left} باقی", + "Account fingerprint — compare it with every other signer before you save": "اکاؤنٹ کا فنگر پرنٹ — محفوظ کرنے سے پہلے اسے ہر دوسرے دستخط کنندہ سے ملائیں", "Activity": "سرگرمی", "Add a contact": "رابطہ شامل کریں", + "Add a signer": "دستخط کنندہ شامل کریں", + "Add payment": "ادائیگی شامل کریں", + "Add the policy file and your signer key to sign for a private multisig account.": "نجی ملٹی سگ اکاؤنٹ کے لیے دستخط کرنے کی غرض سے پالیسی فائل اور اپنی دستخط کنندہ کی شامل کریں۔", "Address": "پتہ", "Address copied": "پتہ کاپی ہو گیا", "Address or payment link": "پتہ یا ادائیگی کا لنک", @@ -116,14 +129,18 @@ export default { "almost done": "تقریباً مکمل", "also tried {endpoints}": "{endpoints} بھی آزمائے گئے", "Amount": "رقم", + "Amount in RAND": "رقم (RAND میں)", "amount must be greater than zero": "رقم صفر سے زیادہ ہونی چاہیے", "amount overflow": "رقم حد سے بڑھ گئی", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "غیر RAND ادائیگیوں کی رقم جانچی نہیں جا سکتی: ان کے لیے رقم خالی چھوڑیں۔", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "یہ وصول کنندہ RAND اور ایک ٹوکن دونوں وصول کرتا ہے: ایک وصول کنندہ کو RAND اور ٹوکن کی ادائیگیوں کے لیے الگ الگ تجاویز درکار ہیں۔", "amounts overflow": "رقمیں حد سے بڑھ گئیں", "an action is {n} characters, more than {max}": "ایک ایکشن {n} حروف کا ہے، جو {max} سے زیادہ ہے", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "پتہ (rand1…)، ادائیگی کا لنک (randpay:…)، یا محفوظ رابطے کا نام۔", "An address here is hexadecimal — 0-9 and a-f only.": "یہاں پتہ ہیکساڈیسیمل میں ہوتا ہے — صرف 0-9 اور a-f۔", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "اس چین پر پتہ 20 بائٹ کا ہوتا ہے — 40 ہیکس حروف۔ یہ {length} ہے۔", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "اس چین پر پتہ 32 بائٹ کا ہوتا ہے — 64 ہیکس حروف۔ یہ {length} ہے۔", + "An amount is a number of RAND, such as 1.5.": "رقم RAND کی ایک عددی مقدار ہے، جیسے 1.5۔", "an unknown chain": "نامعلوم چین", "another tab changed this wallet while it was syncing; try again": "ہم آہنگی کے دوران کسی اور ٹیب نے یہ والیٹ بدل دیا؛ دوبارہ کوشش کریں", "Another tab is syncing": "کسی اور ٹیب میں ہم آہنگی جاری ہے", @@ -190,11 +207,16 @@ export default { "Chains known": "معلوم چینز", "Change back to you": "بقایا رقم آپ کو واپس", "Change it": "اسے بدلیں", + "Check": "جانچیں", + "Check a share": "دستخطی حصہ جانچیں", "Check Activity": "سرگرمی دیکھیں", "Check Activity first": "پہلے سرگرمی دیکھیں", "Check before saving": "محفوظ کرنے سے پہلے جانچ لیں", "Check fingerprint": "فنگر پرنٹ جانچیں", + "Check payments": "ادائیگیاں جانچیں", + "Check proposal": "تجویز جانچیں", "Check settings": "ترتیبات دیکھیں", + "Check share": "دستخطی حصہ جانچیں", "Checking it against the chain before anything is proved.": "کچھ بھی پروف ہونے سے پہلے اسے چین سے ملایا جا رہا ہے۔", "Checking the pool and your notes…": "پول اور آپ کے نوٹ جانچے جا رہے ہیں…", "Checking the swap…": "سواپ جانچا جا رہا ہے…", @@ -221,6 +243,7 @@ export default { "Continue": "جاری رکھیں", "Copied": "کاپی ہو گیا", "Copied as JSON": "JSON کے طور پر کاپی ہو گیا", + "Copy": "کاپی کریں", "Copy address": "پتہ کاپی کریں", "Copy as JSON": "JSON کے طور پر کاپی کریں", "Copy link": "لنک کاپی کریں", @@ -269,9 +292,13 @@ export default { "Enter an amount to send.": "بھیجنے کے لیے رقم درج کریں۔", "Enter an amount.": "رقم درج کریں۔", "Enter at least 10 characters": "کم از کم 10 حروف درج کریں", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "جو بھی ادائیگی آپ کرنا چاہتے ہیں ہر ایک درج کریں: پتہ اور RAND میں رقم۔ ایک ہی پتے پر دو بار ادائیگی کل رقم کے ساتھ ایک بار درج ہوتی ہے۔ جو آؤٹ پٹ ان میں سے کسی سے مطابقت نہیں رکھتا اس پر دستخط نہیں ہو سکتے۔", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "ہر وصول کنندہ کا پتہ درج کریں: جو آؤٹ پٹ درج کیے گئے پتے سے مطابقت نہیں رکھتا، یا جس کا پتہ خالی ہے، اس پر دستخط نہیں ہو سکتے۔", + "Enter the address of every payment.": "ہر ادائیگی کا پتہ درج کریں۔", "Enter the address this should be released to.": "وہ پتہ درج کریں جس پر یہ رقم جاری ہونی چاہیے۔", "Enter the address you are sending to.": "وہ پتہ درج کریں جس پر آپ بھیج رہے ہیں۔", "Enter the amount as a number, for example 1.25.": "رقم عدد کی شکل میں درج کریں، مثلاً 1.25۔", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "ہر ادائیگی کی رقم درج کریں: یہی ادائیگی کو بانٹنے یا پھلانے سے روکتی ہے۔", "Enter the first 4 characters of your recovery key.": "اپنی ریکوری کی کے پہلے 4 حروف درج کریں۔", "Enter the relayer fee as a number, for example 1.25.": "ریلیئر فیس عدد کی شکل میں درج کریں، مثلاً 1.25۔", "Enter this wallet's password — the pairing is sealed under it.": "اس والیٹ کا پاس ورڈ درج کریں — جوڑنے کی معلومات اسی کے تحت محفوظ ہیں۔", @@ -281,6 +308,8 @@ export default { "Enter your recovery key.": "اپنی ریکوری کی درج کریں۔", "Epoch": "ایپاک", "Exactly those four letters, in capitals.": "بالکل یہی چار حروف، بڑے حروف میں۔", + "expected recipient {address}: {reason}": "متوقع وصول کنندہ {address}: {reason}", + "expected_recipients must be a list of addresses": "expected_recipients پتوں کی فہرست ہونی چاہیے", "Explore": "دریافت کریں", "Export spend key": "اسپینڈ کی ایکسپورٹ کریں", "Exporting the spend key is behind your password.": "اسپینڈ کی ایکسپورٹ کرنے کے لیے پاس ورڈ درکار ہے۔", @@ -289,6 +318,7 @@ export default { "Fee": "فیس", "fee must be at least {fee} RAND (the bridge burn floor)": "فیس کم از کم {fee} RAND ہونی چاہیے (برج burn کی کم از کم حد)", "fee must be at least {fee} RAND (the bundle floor)": "فیس کم از کم {fee} RAND ہونی چاہیے (بنڈل کی کم از کم حد)", + "Fee: {fee} RAND · expires at height {height}": "فیس: {fee} RAND · اونچائی {height} پر ختم", "Filter activity by asset": "سرگرمی کو اثاثے کے لحاظ سے فلٹر کریں", "Fingerprint": "فنگر پرنٹ", "fingerprint {fp}": "فنگر پرنٹ {fp}", @@ -326,9 +356,11 @@ export default { "I understand anyone with this key can spend my funds": "میں سمجھتا ہوں کہ جس کے پاس یہ کی ہو وہ میرے فنڈز خرچ کر سکتا ہے", "Import wallet": "والیٹ امپورٹ کریں", "Incorrect password.": "غلط پاس ورڈ۔", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "ان پٹس میں {held} ہے، لیکن رقم + فیس {need} ہے", "inputs hold {held}, but fee + RAND deposit is {need}": "ان پٹس میں {held} ہے، لیکن فیس + RAND جمع {need} ہے", "insufficient balance: have {have}, need {need}": "ناکافی بیلنس: موجود {have}، درکار {need}", + "Intended payments": "مطلوبہ ادائیگیاں", "is on chain {chain}, not {expected}": "چین {chain} پر ہے، {expected} پر نہیں", "It cannot see your balance or your activity, and it cannot move anything.": "یہ آپ کا بیلنس یا سرگرمی نہیں دیکھ سکتا، اور کچھ منتقل نہیں کر سکتا۔", "It cannot spend.": "یہ خرچ نہیں کر سکتی۔", @@ -351,6 +383,7 @@ export default { "key file has no spend_key": "کی فائل میں spend_key نہیں ہے", "key file must be version 2": "کی فائل کا ورژن 2 ہونا چاہیے", "Key hidden — enter your password to view it again.": "کی چھپا دی گئی — دوبارہ دیکھنے کے لیے اپنا پاس ورڈ درج کریں۔", + "Label": "لیبل", "Language": "زبان", "Leave empty to let the payer choose.": "ادائیگی کرنے والے کو منتخب کرنے دینے کے لیے خالی چھوڑیں۔", "Light": "ہلکا", @@ -361,11 +394,15 @@ export default { "machines": "مشینیں", "malformed parameter {parameter}": "خراب پیرامیٹر {parameter}", "Manage contacts": "رابطوں کا انتظام کریں", + "Manage multisig signers": "ملٹی سگ دستخط کنندگان کا انتظام کریں", "Max": "زیادہ سے زیادہ", "Memo": "میمو", "memo \"{memo}\"": "میمو \"{memo}\"", "memo is {n} bytes, at most {max}": "میمو {n} بائٹس کا ہے، زیادہ سے زیادہ {max}", "More is on the way": "مزید آ رہا ہے", + "Multisig share": "ملٹی سگ دستخطی حصہ", + "Multisig signers": "ملٹی سگ دستخط کنندگان", + "Multisig signing is not available in this app.": "اس ایپ میں ملٹی سگ دستخط دستیاب نہیں ہے۔", "My own prover · {name}": "میرا اپنا پروور · {name}", "Name": "نام", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "ان پتوں کے نام جنہیں آپ اکثر ادائیگی کرتے ہیں، صرف اسی ڈیوائس پر رکھے جاتے ہیں۔ رابطے کو نام سے بھیجیں؛ محفوظ کرتے وقت اس کا فنگر پرنٹ جانچا جاتا ہے۔", @@ -389,12 +426,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "ابھی کوئی رابطہ نہیں۔ ترتیبات سے ایک محفوظ کریں، یا پتہ پیسٹ کریں۔", "no endpoint could answer {method}": "کوئی اینڈ پوائنٹ {method} کا جواب نہیں دے سکا", "no endpoint could answer {method}; tried {endpoints}": "کوئی اینڈ پوائنٹ {method} کا جواب نہیں دے سکا؛ آزمائے گئے: {endpoints}", + "no expected recipient given: the payee was NOT checked": "کوئی متوقع وصول کنندہ نہیں دیا گیا: وصول کنندہ کی جانچ نہیں ہوئی (NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url} پر کسی نوڈ نے جواب نہیں دیا، اس لیے میعاد کی جانچ نہیں ہو سکتی اور اس تجویز پر دستخط نہیں کیے گئے۔ نوڈ دستیاب ہونے پر دوبارہ کوشش کریں۔", "No proof is pending.": "کوئی پروف زیر التوا نہیں۔", "No prover": "کوئی پروور نہیں", "No prover is paired.": "کوئی پروور جوڑا نہیں گیا۔", "No RAND to send yet": "بھیجنے کے لیے ابھی کوئی RAND نہیں", "No registry assets": "رجسٹری میں کوئی اثاثہ نہیں", "no reply": "کوئی جواب نہیں", + "No signer profiles yet": "ابھی کوئی دستخط کنندہ پروفائل نہیں", + "No such signer profile.": "ایسی کوئی دستخط کنندہ پروفائل نہیں ہے۔", "No transaction or block matches that.": "اس سے کوئی لین دین یا بلاک میل نہیں کھاتا۔", "no wallet on this device": "اس ڈیوائس پر کوئی والیٹ نہیں", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "بلٹ ان RandProtocol پروور لنکس میں سے کوئی بھی وہ کی نہیں بتاتا جو یہ والیٹ اس کے لیے محفوظ رکھتا ہے؛ انہیں استعمال نہیں کیا جا رہا۔", @@ -441,9 +482,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "ایک پروف اسی ڈیوائس پر بنتا ہے — تقریباً دو منٹ۔ آپ دوسری اسکرینیں دیکھ سکتے ہیں؛ والیٹ بند کرنے سے یہ رک جاتا ہے۔", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "چین {chain} پر اس کوائن میں صرف {locked} لاک ہے؛ کوئی اور پشت پناہی یا کم رقم منتخب کریں", "Open durian.market": "durian.market کھولیں", + "Open file": "فائل کھولیں", "Open in explorer": "ایکسپلورر میں کھولیں", "Open in randscan": "randscan میں کھولیں", "Open in side panel": "سائیڈ پینل میں کھولیں", + "Open in the side panel to choose files": "فائلیں منتخب کرنے کے لیے سائیڈ پینل میں کھولیں", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "پاس ورڈ ٹائپ کرنے کے بجائے Rand Wallet کو {label} سے کھولیں۔ آپ کا پاس ورڈ اب بھی کام کرتا ہے، اور رسائی بحال کرنے کا طریقہ وہی رہتا ہے۔", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "پاس ورڈ ٹائپ کرنے کے بجائے والیٹ کو {label} سے کھولیں۔ آپ کا پاس ورڈ اب بھی کام کرتا ہے، اور رسائی بحال کرنے کا طریقہ وہی رہتا ہے۔", "Open this window from a site that asked to connect.": "یہ ونڈو اس سائٹ سے کھولیں جس نے جڑنے کی درخواست کی تھی۔", @@ -453,6 +496,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "یا RandProtocol پروورز استعمال کریں — ویلیڈیٹرز کی چلائی ہوئی {n} مشینیں، ہر ایک اپنی کی کے ساتھ۔", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "یا RandProtocol پروورز استعمال کریں — ویلیڈیٹرز کی چلائی ہوئی مشینیں، ہر ایک اپنی کی کے ساتھ۔", "Outcome unknown": "نتیجہ نامعلوم", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "آؤٹ پٹ {slot} اس اکاؤنٹ کے طور پر نہیں کھلتا: جو قدر نظر نہیں آتی اس پر دستخط سے انکار", "own={value}, expected 1 or absent": "own={value}، متوقع 1 یا غیر موجود", "Paid": "ادا شدہ", "Pair your own prover": "اپنا پروور جوڑیں", @@ -473,17 +517,20 @@ export default { "Paste your recovery key": "اپنی ریکوری کی پیسٹ کریں", "path is not {n} levels": "پاتھ {n} سطحوں کا نہیں ہے", "Pay me on Rand": "مجھے Rand پر ادائیگی کریں", + "Payee address": "وصول کنندہ کا پتہ", "Payment link": "ادائیگی کا لنک", "Payment link · fingerprint {fingerprint}": "ادائیگی کا لنک · فنگر پرنٹ {fingerprint}", "Payment link copied": "ادائیگی کا لنک کاپی ہو گیا", "Payment links cannot be read in this app; paste the address instead.": "اس ایپ میں ادائیگی کے لنک پڑھے نہیں جا سکتے؛ اس کے بجائے پتہ پیسٹ کریں۔", "Payment links cannot be read in this app.": "اس ایپ میں ادائیگی کے لنک پڑھے نہیں جا سکتے۔", + "Payments changed: check them again before signing.": "ادائیگیاں بدل گئیں: دستخط سے پہلے انہیں دوبارہ جانچیں۔", "Peers": "پیئرز", "Pending": "زیر التوا", "Permission to reach that host was not granted, so the node was left as it was.": "اس ہوسٹ تک رسائی کی اجازت نہیں ملی، اس لیے نوڈ جیسا تھا ویسا ہی رہا۔", "Permission to reach that host was not granted.": "اس ہوسٹ تک رسائی کی اجازت نہیں ملی۔", "Permission to reach that prover was not granted, so nothing was saved.": "اس پروور تک رسائی کی اجازت نہیں ملی، اس لیے کچھ محفوظ نہیں ہوا۔", "Point the camera at a Rand QR code.": "کیمرے کا رخ Rand QR کوڈ کی طرف کریں۔", + "Policy file": "پالیسی فائل", "Pool fee": "پول فیس", "Price impact": "قیمت پر اثر", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "اس چین پر durian.market پولز کے ذریعے قیمت ({pct}% پول فیس)۔ سواپ ایک لین دین ہے جس کا پروف یہ والیٹ خود بناتا اور بھیجتا ہے۔", @@ -493,6 +540,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "پروف صرف اسی ڈیوائس پر بنتے ہیں۔ جہاں یہ پروف نہ بنا سکے، وہاں بھیجنا تب تک رکا رہے گا جب تک آپ پروور نہ جوڑیں یا دوبارہ RandProtocol پروور استعمال نہ کریں۔", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "جو پروف یہ ڈیوائس نہیں بنا سکتی وہ {name} کو جاتے ہیں۔ اس کا فنگر پرنٹ {fingerprint} ہے — جانچ لیں کہ پروور بھی یہی دکھاتا ہے۔", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "جو پروف یہ ڈیوائس نہیں بنا سکتی وہ ان میں سے کسی ایک کو جاتے ہیں؛ جو بھی کسی ترسیل کا پروف بناتا ہے وہ اس والیٹ کی ویوئنگ کی دیکھتا ہے۔", + "Proposal (proposal.json)": "تجویز (proposal.json)", "Prove and send": "پروف بنائیں اور بھیجیں", "Prove for my other devices": "میری دوسری ڈیوائسز کے لیے پروف بنائیں", "Prover": "پروور", @@ -541,6 +589,7 @@ export default { "Relayer fee (optional)": "ریلیئر فیس (اختیاری)", "Remove": "ہٹائیں", "Remove {name}": "{name} ہٹائیں", + "Remove this signer": "اس دستخط کنندہ کو ہٹائیں", "Request RAND": "RAND کی درخواست کریں", "Request RAND from the faucet to try sending, receiving and proving on this network.": "اس نیٹ ورک پر بھیجنے، وصول کرنے اور پروف بنانے کو آزمانے کے لیے فاسیٹ سے RAND کی درخواست کریں۔", "Rescan": "دوبارہ اسکین کریں", @@ -575,8 +624,12 @@ export default { "Running on": "چل رہا ہے", "Save": "محفوظ کریں", "Save an address you pay often, and send to it by name.": "ایسا پتہ محفوظ کریں جس پر آپ اکثر ادائیگی کرتے ہیں، اور نام سے اس پر بھیجیں۔", + "Save as file": "فائل کے طور پر محفوظ کریں", + "Save signer": "دستخط کنندہ محفوظ کریں", "Save your recovery key": "اپنی ریکوری کی محفوظ کریں", "Saved": "محفوظ ہو گیا", + "Saved to {path}": "{path} میں محفوظ ہوا", + "Saved.": "محفوظ ہو گیا۔", "Scan": "اسکین کریں", "Scan a payment link": "ادائیگی کا لنک اسکین کریں", "Scan a QR code": "QR کوڈ اسکین کریں", @@ -597,6 +650,7 @@ export default { "Sent.": "بھیج دیا گیا۔", "Set on this device when this wallet was created.": "یہ والیٹ بنتے وقت اسی ڈیوائس پر مقرر کیا گیا۔", "Settings": "ترتیبات", + "Share": "دستخطی حصہ", "Share payment link": "ادائیگی کا لنک شیئر کریں", "shielded address decodes to {n} bytes, expected {expected}": "شیلڈڈ پتہ {n} بائٹس میں ڈی کوڈ ہوتا ہے، متوقع {expected}", "shielded address is not base58": "شیلڈڈ پتہ base58 نہیں ہے", @@ -608,6 +662,12 @@ export default { "Show the full address": "پورا پتہ دکھائیں", "Show viewing key": "ویوئنگ کی دکھائیں", "Show viewing key for 10 seconds": "ویوئنگ کی 10 سیکنڈ کے لیے دکھائیں", + "Sign": "دستخط کریں", + "Sign a proposal": "تجویز پر دستخط کریں", + "signer {index} is not in this policy (it has {count} signers)": "دستخط کنندہ {index} اس پالیسی میں نہیں ہے (اس میں {count} دستخط کنندگان ہیں)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "دستخط کنندہ {index} کا دستخطی حصہ درست ہے۔ {threshold} میں سے {have} ہاتھ میں ہیں: دستخط کنندگان {list}۔", + "signer {index}'s signature does not verify over this proposal": "دستخط کنندہ {index} کا دستخط اس تجویز کے لیے درست نہیں", + "Signer key file, or its 64-hex seed": "دستخط کنندہ کی فائل، یا اس کا 64-ہیکس سیڈ", "So the address above is one you have actually read.": "تاکہ اوپر والا پتہ وہی ہو جو آپ نے واقعی پڑھا ہے۔", "Something went wrong": "کچھ غلط ہو گیا", "Something went wrong.": "کچھ غلط ہو گیا۔", @@ -640,6 +700,7 @@ export default { "That could not be copied.": "اسے کاپی نہیں کیا جا سکا۔", "That could not be saved.": "اسے محفوظ نہیں کیا جا سکا۔", "That does not match — reveal the key again and check.": "یہ میل نہیں کھاتا — کی دوبارہ دکھائیں اور جانچیں۔", + "That file is larger than a proposal, a policy or a key file can be.": "وہ فائل کسی تجویز، پالیسی یا کی فائل کے ممکنہ سائز سے بڑی ہے۔", "That is more than your balance of {balance} {symbol}.": "یہ آپ کے {balance} {symbol} بیلنس سے زیادہ ہے۔", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "یہ نہ 64 ہیکس حروف (ہیش) ہے اور نہ بلاک کی اونچائی (صرف ہندسے)۔", "That is not a pairing link.": "یہ جوڑنے کا لنک نہیں ہے۔", @@ -675,6 +736,8 @@ export default { "The fee could not be worked out.": "فیس کا حساب نہیں لگایا جا سکا۔", "the genesis hash is not hex of a plausible length": "genesis ہیش مناسب لمبائی کا ہیکس نہیں ہے", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "inflow صرف اسی وقت `none` ہوتا ہے جب بنڈل کوئی ٹوکن burn نہ کرے (inflow.amount == 0)", + "the key file's address is not the one its seed derives": "کی فائل کا پتہ وہ نہیں جو اس کا سیڈ اخذ کرتا ہے", + "the key file's public_key is not the one its seed derives": "کی فائل کی public_key وہ نہیں جو اس کا سیڈ اخذ کرتا ہے", "The key you exported when this wallet was created.": "وہ کی جو آپ نے یہ والیٹ بناتے وقت ایکسپورٹ کی تھی۔", "The language could not be saved.": "زبان محفوظ نہیں ہو سکی۔", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "لنک {linked} {symbol} مانگتا ہے؛ آپ نے {typed} {symbol} لکھا ہے۔", @@ -715,6 +778,7 @@ export default { "The password that unlocks this wallet on this device.": "وہ پاس ورڈ جو اس ڈیوائس پر یہ والیٹ ان لاک کرتا ہے۔", "The payer decides": "ادائیگی کرنے والا طے کرے گا", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "زیر التوا پروف صاف ہو گیا (والیٹ لاک ہو گیا، یا کہیں اور منسوخ کر دیا گیا)، اس لیے کچھ نہیں بھیجا گیا۔ دوبارہ بھیجیں۔", + "the policy does not parse: {reason}": "پالیسی پارس نہیں ہوتی: {reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "اس صفحے کے پڑھنے کے بعد پول بدل گیا۔ کچھ نہیں بھیجا گیا؛ سائٹ دوبارہ ریٹ دے سکتی ہے۔", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "پروف بنتے وقت پول بدل گیا۔ کچھ نہیں بھیجا گیا؛ سائٹ دوبارہ ریٹ دے سکتی ہے۔", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "پول کے پاس یہ ادائیگی کرنے کے لیے کافی نہیں ہے۔ کچھ نہیں بھیجا گیا؛ سائٹ دوبارہ ریٹ دے سکتی ہے۔", @@ -725,6 +789,12 @@ export default { "the proof does not verify: {reason}": "پروف کی تصدیق نہیں ہوتی: {reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "پروف نے ایسا ڈائجسٹ شائع کیا جو اس والیٹ نے نہیں بنایا ({expected} کی جگہ {published})؛ اسے استعمال نہیں کیا جا رہا", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "پروف اسی ڈیوائس پر بنتا ہے۔ آپ دوسری اسکرینیں دیکھ سکتے ہیں — یہ جاری رہتا ہے — لیکن والیٹ بند کرنے سے یہ رک جاتا ہے۔", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "تجویز کی جانچ نے ادھورا جواب لوٹایا، اس لیے اس تجویز پر دستخط نہیں کیے گئے۔", + "the proposal does not parse: {reason}": "تجویز پارس نہیں ہوتی: {reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "تجویز {expires} پر ختم ہوتی ہے، جو اس کے بنڈل کی حد ({from}, {to}] سے باہر ہے", + "the proposal's auth_commit is not this policy's account": "تجویز کا auth_commit اس پالیسی کا اکاؤنٹ نہیں ہے", + "the proposal's expected digest is not its bundle's": "تجویز کا متوقع ڈائجسٹ اس کے بنڈل کا نہیں ہے", + "the proposal's signing_message is not its binding's": "تجویز کا signing_message اس کی بائنڈنگ کا نہیں ہے", "the prover": "پروور", "the prover accepted the job but named no job id": "پروور نے کام قبول کر لیا لیکن کوئی جاب آئی ڈی نہیں بتائی", "The prover answered with an unknown state ({state}).": "پروور نے نامعلوم حالت ({state}) کے ساتھ جواب دیا۔", @@ -786,6 +856,7 @@ export default { "The request was not approved in Rand Wallet.": "Rand Wallet میں درخواست منظور نہیں ہوئی۔", "The rescan could not be started.": "دوبارہ اسکین شروع نہیں ہو سکا۔", "The rescan did not finish": "دوبارہ اسکین مکمل نہیں ہوا", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "محفوظ شدہ دستخط کنندہ پروفائلیں کھولی نہیں جا سکیں؛ کچھ نہیں بدلا۔ اس ڈیوائس کا اسٹوریج بحال کریں، یا والیٹ مٹا کر اس کی کی سے بحال کریں", "The scan was cancelled.": "اسکین منسوخ کر دیا گیا۔", "The side panel could not be opened.": "سائیڈ پینل نہیں کھل سکا۔", "The site sent an invoke this wallet cannot read: {reason}.": "سائٹ نے ایسا invoke بھیجا جسے یہ والیٹ پڑھ نہیں سکتا: {reason}۔", @@ -796,6 +867,10 @@ export default { "The swap did not go through.": "سواپ مکمل نہیں ہوا۔", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "سواپ جاری رہتا ہے۔ \"بھیج دیا گیا\" لکھا آنے سے پہلے والیٹ بند کرنے سے یہ رک جاتا ہے، اور کچھ ضائع نہیں ہوتا۔", "The theme could not be saved.": "تھیم محفوظ نہیں ہو سکی۔", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "لین دین میں ایک عمل ہے؛ نجی ملٹی سگ تجویز صرف اپنے بنڈل سے ادائیگی ہوتی ہے", + "the transaction carries no bundle": "لین دین میں کوئی بنڈل نہیں ہے", + "the transaction does not hash to the proposal's binding": "لین دین کا ہیش تجویز کی بائنڈنگ کا نہیں ہے", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "لین دین چین {tx} کے لیے ہے، تجویز چین {proposal} بتاتی ہے", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "ٹرانزیکشن کی صرف اس ایک ادائیگی — اس کی رقم اور وصول کنندہ — کو اس پر ظاہر کرتی ہے جسے آپ یہ دیں۔ یہ کچھ بھی خرچ نہیں کر سکتی۔", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "ہو سکتا ہے لین دین پہلے ہی چین پر ہو۔ دوبارہ رقم نکالنے سے پہلے سرگرمی دیکھیں — دو بار نکالنے سے دو بار burn ہو گا۔", "The transfer could not be proved.": "منتقلی کا پروف نہیں بن سکا۔", @@ -813,6 +888,9 @@ export default { "The wallet was told: {message}": "والیٹ کو بتایا گیا: {message}", "The withdrawal could not be proved.": "رقم نکالنے کا پروف نہیں بن سکا۔", "The withdrawal was submitted, but the node did not return a usable hash.": "رقم نکالنے کی درخواست جمع کرا دی گئی، لیکن نوڈ نے قابل استعمال ہیش واپس نہیں کیا۔", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "ویٹنس کی قدر جانچی نہیں جا سکتی: اس کا nk یا سالٹ اس تجویز کا نہیں ہے", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "ویٹنس کی قدر آؤٹ پٹس سے مطابقت نہیں رکھتی: اثاثہ {asset} میں {held} ان پٹ اور فیس و برن کے ساتھ {spent} آؤٹ پٹ ہے", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "ویٹنس کی قدر لین دین سے مطابقت نہیں رکھتی: ان پٹ {input} بنڈل کے نلیفائر تک نہیں پہنچتا", "Theme": "تھیم", "There is no pool for that pair.": "اس جوڑی کے لیے کوئی پول نہیں ہے۔", "There is no program {program}… on this chain. Nothing was sent.": "اس چین پر کوئی پروگرام {program}… نہیں ہے۔ کچھ نہیں بھیجا گیا۔", @@ -854,6 +932,7 @@ export default { "This device, or where it cannot · {name} provers": "یہ ڈیوائس، یا جہاں یہ نہ بنا سکے · {name} پروورز", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "یہ پرانی چین سے پڑھے گئے نوٹ اور تاریخ بھلا دیتا ہے اور یہ نوڈ شروع سے پڑھتا ہے۔ آپ کی کیز اور پاس ورڈ کو نہیں چھیڑا جاتا۔", "This key can spend your funds": "یہ کی آپ کے فنڈز خرچ کر سکتی ہے", + "this key is not one of the policy's signers": "یہ کی پالیسی کے دستخط کنندگان میں سے نہیں ہے", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "یہ کی ہی آپ کا والیٹ ہے۔ جس کے پاس یہ ہو وہ آپ کے فنڈز خرچ کر سکتا ہے، اور اس کے بغیر ڈیوائس کھونے کا مطلب فنڈز کھونا ہے۔ آپ اسے ترتیبات میں اپنے پاس ورڈ سے دوبارہ دیکھ سکتے ہیں۔", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "یہ رقم شیلڈڈ پول سے باہر جاتی ہے۔ منزل کا پتہ اور رقم دوسری چین پر عوامی ہو جاتے ہیں۔", "This link is too long for a QR code — copy it instead.": "یہ لنک QR کوڈ کے لیے بہت لمبا ہے — اس کے بجائے اسے کاپی کریں۔", @@ -869,6 +948,10 @@ export default { "This note was not found.": "یہ نوٹ نہیں ملا۔", "This passkey does not support the PRF extension.": "یہ پاس کی PRF ایکسٹینشن کو سپورٹ نہیں کرتی۔", "This proof was already submitted from another window. Check Activity before sending again.": "یہ پروف پہلے ہی کسی اور ونڈو سے جمع کرایا جا چکا ہے۔ دوبارہ بھیجنے سے پہلے سرگرمی دیکھیں۔", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "یہ تجویز اونچائی {expires} پر ختم ہوتی ہے اور چین {head} پر ہے: دستخط کے لیے بہت دیر ہو چکی۔ نئی تجویز مانگیں۔", + "This proposal is bound to another genesis than the one this wallet's node serves.": "یہ تجویز اس جینیسس سے مختلف جینیسس سے بندھی ہے جو اس والیٹ کا نوڈ فراہم کرتا ہے۔", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "یہ تجویز کسی اور پالیسی کی ہے: اس میں {theirs} ہے، جبکہ یہ پالیسی {ours} ہے", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "یہ تجویز چین {theirs} کے لیے ہے؛ یہ والیٹ چین {ours} پر ہے۔", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "یہ پروور فی پروف {fee} RAND لیتا ہے، اور والیٹ کا یہ ورژن پروور کی فیس ادا نہیں کرتا: ایسا پروور جوڑیں جو کوئی فیس نہ لے، یا اسی ڈیوائس پر پروف بنائیں", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "یہ پروور فیس لیتا ہے، جو والیٹ کا یہ ورژن ادا نہیں کرتا۔ ایسا پروور جوڑیں جو کوئی فیس نہ لے، یا ڈیسک ٹاپ ایپ سے بھیجیں۔", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "یہ پروور اس قسم کا کام قبول نہیں کرتا ({reason})۔ ترتیبات میں کوئی اور پروور جوڑیں، یا ڈیسک ٹاپ ایپ سے بھیجیں۔", @@ -880,6 +963,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "یہ اس ڈیوائس سے والیٹ ہٹا دیتا ہے۔ اسے بحال کرنے کے لیے آپ کو اپنی ریکوری کی درکار ہوگی۔", "This screen could not be shown.": "یہ اسکرین دکھائی نہیں جا سکی۔", "This screen ships in a later task. Your wallet is unlocked and ready.": "یہ اسکرین بعد کے مرحلے میں آئے گی۔ آپ کا والیٹ ان لاک اور تیار ہے۔", + "this signer is already saved as {label}": "یہ دستخط کنندہ پہلے سے {label} کے طور پر محفوظ ہے", + "This signer profile has no key on this device: remove it and add it again.": "اس دستخط کنندہ پروفائل کی اس ڈیوائس پر کوئی کی نہیں ہے: اسے ہٹا کر دوبارہ شامل کریں۔", "This tab is showing what it has so far, and will refresh when that finishes.": "یہ ٹیب اب تک جو کچھ ہے وہ دکھا رہا ہے، اور اس کے مکمل ہونے پر تازہ ہو جائے گا۔", "This takes a few minutes; keep this window open.": "اس میں چند منٹ لگتے ہیں؛ یہ ونڈو کھلی رکھیں۔", "This transaction was not found.": "یہ لین دین نہیں ملا۔", @@ -926,6 +1011,7 @@ export default { "Type {word} to confirm": "تصدیق کے لیے {word} ٹائپ کریں", "Type it once more.": "اسے ایک بار پھر ٹائپ کریں۔", "Type the last four characters of that address": "اس پتے کے آخری چار حروف ٹائپ کریں", + "Type the last four characters of the account fingerprint": "اکاؤنٹ کے فنگر پرنٹ کے آخری چار حروف لکھیں", "unavailable in this app": "اس ایپ میں دستیاب نہیں", "unknown": "نامعلوم", "unknown parameter {name}": "نامعلوم پیرامیٹر {name}", @@ -962,6 +1048,7 @@ export default { "Weak": "کمزور", "Welcome back": "خوش آمدید", "What the pool pays you arrives as a note your wallet finds on its next sync.": "پول آپ کو جو ادا کرتا ہے وہ ایک نوٹ کی شکل میں آتا ہے جو آپ کا والیٹ اگلی ہم آہنگی پر ڈھونڈ لیتا ہے۔", + "What this proposal does, as checked against your policy": "یہ تجویز کیا کرتی ہے، آپ کی پالیسی کے مقابل جانچا گیا", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "جہاں یہ ڈیوائس پروف نہ بنا سکے، وہاں پروور جوڑیں۔ دونوں صورتوں میں آپ کی اسپینڈ کی یہیں رہتی ہے؛ جو پروور آپ خود چلاتے ہیں — ڈیسک ٹاپ ایپ، یا آپ کی اپنی مشین پر rand-prover — وہ آپ کی تاریخ بھی آپ تک محدود رکھتا ہے۔", "Where to?": "کہاں بھیجنا ہے؟", "Which address?": "کون سا پتہ؟", @@ -1008,6 +1095,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "آپ کا پروور {n} منٹ پروف بنانے کے بعد بھی مکمل نہیں ہوا۔ بعد میں جاری رکھیں، یا منسوخ کریں۔", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "آپ کے پروور نے قطار میں {n} منٹ کے بعد بھی یہ پروف شروع نہیں کیا۔ بعد میں جاری رکھیں، یا منسوخ کریں۔", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "آپ کے پروور کا جوڑ نہیں کھل سکا۔ والیٹ کو لاک اور ان لاک کریں، یا ترتیبات میں پروور دوبارہ جوڑیں۔", + "Your share — send it to whoever proves the proposal": "آپ کا دستخطی حصہ — اسے اس کو بھیجیں جو تجویز ثابت کرتا ہے", "Your wallet picks up what it pays you on its next sync.": "آپ کا والیٹ اگلی ہم آہنگی پر وہ رقم لے لیتا ہے جو یہ آپ کو ادا کرتا ہے۔", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "آپ کے والیٹ کی تاریخ {chain} سے پڑھی گئی تھی — کچھ نہیں بدلا گیا، اور جب تک دونوں میل نہ کھائیں یہ والیٹ نہ فنڈز بھیجے گا نہ مانگے گا۔ ترتیبات میں نوڈ بدلیں، یا نئی چین کے لیے یہ والیٹ دوبارہ اسکین کریں۔", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "یہ {name} پرووَرز کا پتہ ہے۔ انہیں پیئر کرنے کی ضرورت نہیں: یہ والیٹ انہیں بطورِ طے شدہ استعمال کرتا ہے۔", diff --git a/ui/locales/zh-hk.js b/ui/locales/zh-hk.js index 73c04ed..e0d120c 100644 --- a/ui/locales/zh-hk.js +++ b/ui/locales/zh-hk.js @@ -1,6 +1,7 @@ // Rand Wallet — 中文(繁體), Cantonese as written in Hong Kong (zh-Hant-HK). Keys are ui/locales/en.js. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "尚欠 {blocks} 個區塊便可證明。", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} 個對等節點", "{count} peers": "{count} 個對等節點", @@ -29,6 +30,7 @@ export default { "{label} is off": "{label} 已關閉", "{label} is on": "{label} 已開啟", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} 已無法開啟此錢包(密碼已更改)。請先用密碼解鎖,然後在設定中重新開啟 {label}。", + "{matched} of {total} payments matched.": "{total} 筆付款中有 {matched} 筆相符。", "{member} answered with another key than the one this wallet pins": "{member} 回應的密鑰與此錢包綁定的不同", "{member} did not answer": "{member} 沒有回應", "{member} does not take this wallet's jobs": "{member} 不接受此錢包的工作", @@ -49,6 +51,8 @@ export default { "{symbol} cannot be withdrawn": "無法提取 {symbol}", "{symbol} has {decimals} decimal places — that is more.": "{symbol} 有 {decimals} 位小數,你輸入的位數較多。", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} 會以你所選的幣種,在該幣種本身的鏈上發放。", + "{threshold} of {count} · signer {index}": "{threshold}/{count} · 簽署人 {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold}/{count} · 此金鑰是簽署人 {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "鏈 {chain} 上的 {token} 有 {decimals} 位小數:金額和中繼費必須是 {unit} 的倍數", "{units} units of asset {asset}": "資產 {asset} 的 {units} 個單位", "{url} answered first — chain {chain} · height {height}.": "{url} 最先回應:鏈 {chain} · 高度 {height}。", @@ -81,7 +85,12 @@ export default { "A prover address must be https://.": "證明器地址必須是 https://。", "A prover is making the proof": "證明器正在生成證明", "A prover sees your history": "證明器會看到你的記錄", + "a share is index:hex": "簽署份額的格式為 index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand 網絡的隱私錢包。餘額和記錄保持私密,每筆傳送都在此裝置上生成證明。", + "a signer key is a rand-node keygen file or 64 hex characters": "簽署人金鑰是 rand-node 的 keygen 檔案或 64 位十六進制字元", + "a signer label is 1-64 characters": "簽署人標籤為 1-64 個字元", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "簽署人資料儲存在此錢包的加密保險庫內。每次簽署都會再次要求輸入密碼。", + "a signer seed is 64 hex characters": "簽署人種子為 64 位十六進制字元", "A site": "網站", "a spend key is 64 hex characters, or a wallet.key.json": "花費私鑰為 64 個十六進位字元,或 wallet.key.json 檔案", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "會先當作交易雜湊搜尋;如果找不到,再以同一雜湊搜尋區塊。純數字會按高度搜尋區塊。", @@ -98,8 +107,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "提取到不在其列表中的地址可能會被拒絕。在生成證明之前,不會花費任何資金。", "About": "關於", "about {left} left": "尚餘約 {left}", + "Account fingerprint — compare it with every other signer before you save": "帳戶指紋 — 儲存前請與其他每位簽署人核對", "Activity": "記錄", "Add a contact": "新增聯絡人", + "Add a signer": "新增簽署人", + "Add payment": "新增付款", + "Add the policy file and your signer key to sign for a private multisig account.": "新增政策檔案及你的簽署人金鑰,即可為私密多重簽署帳戶簽署。", "Address": "地址", "Address copied": "已複製地址", "Address or payment link": "地址或付款連結", @@ -108,14 +121,18 @@ export default { "almost done": "即將完成", "also tried {endpoints}": "亦曾嘗試 {endpoints}", "Amount": "金額", + "Amount in RAND": "金額(RAND)", "amount must be greater than zero": "金額必須大於零", "amount overflow": "金額溢位", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "非 RAND 付款無法核對金額:該類付款請將金額留空。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "此收款人同時收取 RAND 及代幣:向同一收款人支付 RAND 及代幣,需要分開的提案。", "amounts overflow": "金額溢位", "an action is {n} characters, more than {max}": "一個動作有 {n} 個字元,多於 {max} 個", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "地址(rand1…)、付款連結(randpay:…)或已儲存聯絡人的名稱。", "An address here is hexadecimal — 0-9 and a-f only.": "此處的地址為十六進位,只可包含 0-9 和 a-f。", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "此鏈上的地址為 20 位元組,即 40 個十六進位字元。你輸入了 {length} 個。", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "此鏈上的地址為 32 位元組,即 64 個十六進位字元。你輸入了 {length} 個。", + "An amount is a number of RAND, such as 1.5.": "金額是 RAND 的數值,例如 1.5。", "an unknown chain": "未知的鏈", "another tab changed this wallet while it was syncing; try again": "同步期間另一個分頁更改了此錢包;請再試一次", "Another tab is syncing": "另一個分頁正在同步", @@ -182,11 +199,16 @@ export default { "Chains known": "已知的鏈", "Change back to you": "找零(退回給你)", "Change it": "更改", + "Check": "核對", + "Check a share": "核對簽署份額", "Check Activity": "查看記錄", "Check Activity first": "請先查看記錄", "Check before saving": "儲存前先檢查", "Check fingerprint": "檢查指紋", + "Check payments": "核對付款", + "Check proposal": "核對提案", "Check settings": "檢查設定", + "Check share": "核對簽署份額", "Checking it against the chain before anything is proved.": "在生成任何證明之前,先與鏈核對。", "Checking the pool and your notes…": "正在檢查資金池和你的票據…", "Checking the swap…": "正在檢查兌換…", @@ -213,6 +235,7 @@ export default { "Continue": "繼續", "Copied": "已複製", "Copied as JSON": "已複製為 JSON", + "Copy": "複製", "Copy address": "複製地址", "Copy as JSON": "複製為 JSON", "Copy link": "複製連結", @@ -261,9 +284,13 @@ export default { "Enter an amount to send.": "輸入要傳送的金額。", "Enter an amount.": "輸入金額。", "Enter at least 10 characters": "至少輸入 10 個字元", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "請輸入你打算進行的每一筆付款:地址及以 RAND 計的金額。向同一地址的兩筆付款只輸入一次,填合計金額。與其中任何一筆都不相符的輸出無法簽署。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "請輸入每位收款人的地址:與所輸入地址不相符、或地址為空的輸出無法簽署。", + "Enter the address of every payment.": "請輸入每筆付款的地址。", "Enter the address this should be released to.": "輸入要發放到的地址。", "Enter the address you are sending to.": "輸入你要傳送到的地址。", "Enter the amount as a number, for example 1.25.": "以數字輸入金額,例如 1.25。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "請輸入每筆付款的金額:它可防止拆分或虛增付款。", "Enter the first 4 characters of your recovery key.": "輸入恢復密鑰的首 4 個字元。", "Enter the relayer fee as a number, for example 1.25.": "以數字輸入中繼費,例如 1.25。", "Enter this wallet's password — the pairing is sealed under it.": "輸入此錢包的密碼,配對資料會以它加密保存。", @@ -273,6 +300,8 @@ export default { "Enter your recovery key.": "輸入你的恢復密鑰。", "Epoch": "紀元", "Exactly those four letters, in capitals.": "必須是這四個字母,以大楷輸入。", + "expected recipient {address}: {reason}": "預期收款人 {address}:{reason}", + "expected_recipients must be a list of addresses": "expected_recipients 必須是地址清單", "Explore": "探索", "Export spend key": "匯出花費私鑰", "Exporting the spend key is behind your password.": "匯出花費私鑰需要輸入密碼。", @@ -281,6 +310,7 @@ export default { "Fee": "手續費", "fee must be at least {fee} RAND (the bridge burn floor)": "手續費最少須為 {fee} RAND(跨鏈橋銷毀下限)", "fee must be at least {fee} RAND (the bundle floor)": "手續費最少須為 {fee} RAND(交易包下限)", + "Fee: {fee} RAND · expires at height {height}": "手續費:{fee} RAND · 於高度 {height} 過期", "Filter activity by asset": "按資產篩選記錄", "Fingerprint": "指紋", "fingerprint {fp}": "指紋 {fp}", @@ -318,9 +348,11 @@ export default { "I understand anyone with this key can spend my funds": "我明白任何人取得此密鑰都可以花費我的資金", "Import wallet": "匯入錢包", "Incorrect password.": "密碼不正確。", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "輸入持有 {held},但金額加手續費為 {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "輸入持有 {held},但手續費加 RAND 存入為 {need}", "insufficient balance: have {have}, need {need}": "餘額不足:持有 {have},需要 {need}", + "Intended payments": "擬進行的付款", "is on chain {chain}, not {expected}": "在鏈 {chain} 上,而不是 {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "它看不到你的餘額或記錄,也不能轉移任何東西。", "It cannot spend.": "它不能花費。", @@ -343,6 +375,7 @@ export default { "key file has no spend_key": "密鑰檔案沒有 spend_key", "key file must be version 2": "密鑰檔案必須是第 2 版", "Key hidden — enter your password to view it again.": "密鑰已隱藏,請輸入密碼再次查看。", + "Label": "標籤", "Language": "語言", "Leave empty to let the payer choose.": "留空讓付款人自行選擇。", "Light": "淺色", @@ -353,11 +386,15 @@ export default { "machines": "部機器", "malformed parameter {parameter}": "參數 {parameter} 格式錯誤", "Manage contacts": "管理聯絡人", + "Manage multisig signers": "管理多重簽署簽署人", "Max": "最大", "Memo": "備註", "memo \"{memo}\"": "備註「{memo}」", "memo is {n} bytes, at most {max}": "備註有 {n} 位元組,上限為 {max}", "More is on the way": "更多功能即將推出", + "Multisig share": "多重簽署簽署份額", + "Multisig signers": "多重簽署簽署人", + "Multisig signing is not available in this app.": "此應用程式不支援多重簽署。", "My own prover · {name}": "我自己的證明器 · {name}", "Name": "名稱", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "你常付款的地址的名稱,只儲存在此裝置上。可按名稱傳送給聯絡人;儲存時會核對其指紋。", @@ -381,12 +418,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "暫無聯絡人。可在設定中儲存,或直接貼上地址。", "no endpoint could answer {method}": "沒有端點能回應 {method}", "no endpoint could answer {method}; tried {endpoints}": "沒有端點能回應 {method};已嘗試 {endpoints}", + "no expected recipient given: the payee was NOT checked": "未提供預期收款人:收款人未經核對(NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url} 沒有節點回應,因此無法核對過期時間,此提案不會簽署。待節點可連線後再試。", "No proof is pending.": "沒有待處理的證明。", "No prover": "沒有證明器", "No prover is paired.": "未配對證明器。", "No RAND to send yet": "尚未有可傳送的 RAND", "No registry assets": "沒有登記冊資產", "no reply": "沒有回覆", + "No signer profiles yet": "尚未有簽署人資料", + "No such signer profile.": "沒有這個簽署人資料。", "No transaction or block matches that.": "沒有相符的交易或區塊。", "no wallet on this device": "此裝置上沒有錢包", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "內置的 RandProtocol 證明器連結都沒有列明此錢包所綁定的密鑰;不會使用它們。", @@ -433,9 +474,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "一個證明會在此裝置上運行,約需兩分鐘。你可以瀏覽其他畫面;關閉錢包會停止它。", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "鏈 {chain} 上只有 {locked} 鎖定在該幣種中;請選擇其他支持幣種或較小的金額", "Open durian.market": "開啟 durian.market", + "Open file": "開啟檔案", "Open in explorer": "在區塊瀏覽器中開啟", "Open in randscan": "在 randscan 中開啟", "Open in side panel": "在側邊欄開啟", + "Open in the side panel to choose files": "請在側邊欄開啟以選擇檔案", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "以 {label} 開啟 Rand Wallet,無需輸入密碼。你的密碼仍然有效,亦是恢復存取權的方法。", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "以 {label} 開啟錢包,無需輸入密碼。你的密碼仍然有效,亦是恢復存取權的方法。", "Open this window from a site that asked to connect.": "請從要求連接的網站開啟此視窗。", @@ -445,6 +488,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "或使用 RandProtocol 證明器:由驗證者運行的 {n} 部機器,每部都有自己的密鑰。", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "或使用 RandProtocol 證明器:由驗證者運行的機器,每部都有自己的密鑰。", "Outcome unknown": "結果不明", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "輸出 {slot} 無法作為此帳戶的輸出開啟:拒絕簽署看不到的金額", "own={value}, expected 1 or absent": "own={value},應為 1 或不設定", "Paid": "已付款", "Pair your own prover": "配對你自己的證明器", @@ -465,17 +509,20 @@ export default { "Paste your recovery key": "貼上你的恢復密鑰", "path is not {n} levels": "路徑不是 {n} 層", "Pay me on Rand": "以 Rand 付款給我", + "Payee address": "收款人地址", "Payment link": "付款連結", "Payment link · fingerprint {fingerprint}": "付款連結 · 指紋 {fingerprint}", "Payment link copied": "已複製付款連結", "Payment links cannot be read in this app; paste the address instead.": "此應用程式無法讀取付款連結;請改為貼上地址。", "Payment links cannot be read in this app.": "此應用程式無法讀取付款連結。", + "Payments changed: check them again before signing.": "付款已更改:簽署前請重新核對。", "Peers": "對等節點", "Pending": "待確認", "Permission to reach that host was not granted, so the node was left as it was.": "未獲授權存取該主機,因此節點維持不變。", "Permission to reach that host was not granted.": "未獲授權存取該主機。", "Permission to reach that prover was not granted, so nothing was saved.": "未獲授權存取該證明器,因此沒有儲存任何內容。", "Point the camera at a Rand QR code.": "將相機對準 Rand QR 碼。", + "Policy file": "政策檔案", "Pool fee": "資金池費用", "Price impact": "價格影響", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "按此鏈上 durian.market 資金池定價(資金池費用 {pct}%)。兌換是一筆由此錢包自行生成證明並傳送的交易。", @@ -485,6 +532,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "證明只在此裝置上生成。如此裝置無法生成,傳送會等待至你配對證明器或再次使用 RandProtocol 證明器。", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "此裝置無法生成的證明會交給 {name}。其指紋為 {fingerprint},請核對證明器顯示的是否相同。", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "此裝置無法生成的證明會交給其中一部;每部為傳送生成證明的證明器都會看到該錢包的檢視密鑰。", + "Proposal (proposal.json)": "提案(proposal.json)", "Prove and send": "生成證明並傳送", "Prove for my other devices": "為我的其他裝置生成證明", "Prover": "證明器", @@ -533,6 +581,7 @@ export default { "Relayer fee (optional)": "中繼費(可選)", "Remove": "移除", "Remove {name}": "移除 {name}", + "Remove this signer": "移除此簽署人", "Request RAND": "索取 RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "從水龍頭索取 RAND,以在此網絡上試用傳送、接收和生成證明。", "Rescan": "重新掃描", @@ -567,8 +616,12 @@ export default { "Running on": "運行於", "Save": "儲存", "Save an address you pay often, and send to it by name.": "儲存你常付款的地址,並按名稱傳送。", + "Save as file": "另存為檔案", + "Save signer": "儲存簽署人", "Save your recovery key": "儲存你的恢復密鑰", "Saved": "已儲存", + "Saved to {path}": "已儲存至 {path}", + "Saved.": "已儲存。", "Scan": "掃描", "Scan a payment link": "掃描付款連結", "Scan a QR code": "掃描 QR 碼", @@ -589,6 +642,7 @@ export default { "Sent.": "已傳送。", "Set on this device when this wallet was created.": "於此錢包建立時在此裝置上設定。", "Settings": "設定", + "Share": "簽署份額", "Share payment link": "分享付款連結", "shielded address decodes to {n} bytes, expected {expected}": "隱私地址解碼為 {n} 位元組,應為 {expected}", "shielded address is not base58": "隱私地址不是 base58", @@ -600,6 +654,12 @@ export default { "Show the full address": "顯示完整地址", "Show viewing key": "顯示檢視密鑰", "Show viewing key for 10 seconds": "顯示檢視密鑰 10 秒", + "Sign": "簽署", + "Sign a proposal": "簽署提案", + "signer {index} is not in this policy (it has {count} signers)": "簽署人 {index} 不在此政策內(該政策有 {count} 位簽署人)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "簽署人 {index} 的簽署份額驗證通過。已收集 {have}/{threshold}:簽署人 {list}。", + "signer {index}'s signature does not verify over this proposal": "簽署人 {index} 的簽署對此提案驗證不通過", + "Signer key file, or its 64-hex seed": "簽署人金鑰檔案,或其 64 位十六進制種子", "So the address above is one you have actually read.": "確保上面的地址是你真正讀過的。", "Something went wrong": "發生錯誤", "Something went wrong.": "發生錯誤。", @@ -632,6 +692,7 @@ export default { "That could not be copied.": "無法複製。", "That could not be saved.": "無法儲存。", "That does not match — reveal the key again and check.": "不相符,請再次顯示密鑰並核對。", + "That file is larger than a proposal, a policy or a key file can be.": "該檔案大於提案、政策或金鑰檔案所容許的大小。", "That is more than your balance of {balance} {symbol}.": "這多於你的餘額 {balance} {symbol}。", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "這既不是 64 個十六進位字元(雜湊),也不是區塊高度(只限數字)。", "That is not a pairing link.": "這不是配對連結。", @@ -667,6 +728,8 @@ export default { "The fee could not be worked out.": "無法計算手續費。", "the genesis hash is not hex of a plausible length": "創世雜湊不是合理長度的十六進位", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "當且僅當交易包沒有銷毀代幣(inflow.amount == 0)時,inflow 才是 `none`", + "the key file's address is not the one its seed derives": "金鑰檔案內的地址與其種子衍生出的地址不一致", + "the key file's public_key is not the one its seed derives": "金鑰檔案內的 public_key 與其種子衍生出的不一致", "The key you exported when this wallet was created.": "建立此錢包時你匯出的密鑰。", "The language could not be saved.": "無法儲存語言。", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "連結要求 {linked} {symbol};你輸入了 {typed} {symbol}。", @@ -707,6 +770,7 @@ export default { "The password that unlocks this wallet on this device.": "在此裝置上解鎖此錢包的密碼。", "The payer decides": "由付款人決定", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "待處理的證明已被清除(錢包已鎖定,或已在其他地方取消),因此沒有傳送任何東西。請再傳送一次。", + "the policy does not parse: {reason}": "政策無法解析:{reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "此頁讀取資金池後,資金池已有變動。沒有傳送任何東西;網站可以重新報價。", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "生成證明期間資金池有變動。沒有傳送任何東西;網站可以重新報價。", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "資金池沒有足夠資金支付此數額。沒有傳送任何東西;網站可以重新報價。", @@ -717,6 +781,12 @@ export default { "the proof does not verify: {reason}": "證明無法通過驗證:{reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "證明公佈的摘要並非此錢包所建立({published},應為 {expected});不會使用它", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "證明在此裝置上運行。你可以瀏覽其他畫面,它會繼續進行,但關閉錢包會停止它。", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案核對傳回不完整的結果,因此此提案不會簽署。", + "the proposal does not parse: {reason}": "提案無法解析:{reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "提案在 {expires} 過期,超出其捆綁包的有效區間 ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "提案的 auth_commit 不是此政策的帳戶", + "the proposal's expected digest is not its bundle's": "提案的預期摘要與其捆綁包的不一致", + "the proposal's signing_message is not its binding's": "提案的 signing_message 與其綁定的不一致", "the prover": "證明器", "the prover accepted the job but named no job id": "證明器接受了工作,但沒有提供工作 ID", "The prover answered with an unknown state ({state}).": "證明器回應了未知狀態({state})。", @@ -778,6 +848,7 @@ export default { "The request was not approved in Rand Wallet.": "請求未在 Rand Wallet 中獲批准。", "The rescan could not be started.": "無法開始重新掃描。", "The rescan did not finish": "重新掃描未完成", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "無法開啟已儲存的簽署人資料;未作任何更改。請復原此裝置的儲存空間,或清除錢包後用其金鑰復原", "The scan was cancelled.": "掃描已取消。", "The side panel could not be opened.": "無法開啟側邊欄。", "The site sent an invoke this wallet cannot read: {reason}.": "網站傳來此錢包無法讀取的調用:{reason}。", @@ -788,6 +859,10 @@ export default { "The swap did not go through.": "兌換未能完成。", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "兌換會繼續進行。在顯示已傳送前關閉錢包會停止它,但不會損失任何東西。", "The theme could not be saved.": "無法儲存主題。", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "交易帶有一個動作;私密多重簽署提案只是僅來自其捆綁包的付款", + "the transaction carries no bundle": "交易不含捆綁包", + "the transaction does not hash to the proposal's binding": "交易的雜湊不是提案的綁定", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "交易屬於鏈 {tx},而提案指向鏈 {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "交易密鑰會向你給予的人披露這一筆付款,包括金額和收款方。它不能花費任何東西。", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "交易可能已經上鏈。再次提取前請先查看記錄,重複提取會銷毀兩次。", "The transfer could not be proved.": "無法為轉帳生成證明。", @@ -805,6 +880,9 @@ export default { "The wallet was told: {message}": "錢包收到的訊息:{message}", "The withdrawal could not be proved.": "無法為提取生成證明。", "The withdrawal was submitted, but the node did not return a usable hash.": "提取已提交,但節點沒有傳回可用的雜湊。", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "無法核對見證資料的金額:其 nk 或鹽值不屬於此提案", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "見證資料的金額與輸出不一致:資產 {asset} 輸入 {held},輸出 {spent}(含手續費及銷毀)", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "見證資料的金額與交易不一致:輸入 {input} 未對應到捆綁包的 nullifier", "Theme": "主題", "There is no pool for that pair.": "該交易對沒有資金池。", "There is no program {program}… on this chain. Nothing was sent.": "此鏈上沒有程式 {program}…。沒有傳送任何東西。", @@ -846,6 +924,7 @@ export default { "This device, or where it cannot · {name} provers": "此裝置;無法生成時 · {name} 證明器", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "這會忘記從舊鏈讀取的票據和記錄,並從頭讀取此節點。你的密鑰和密碼不受影響。", "This key can spend your funds": "此密鑰可以花費你的資金", + "this key is not one of the policy's signers": "此金鑰不屬於該政策的簽署人", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "此密鑰就是你的錢包。任何人取得它都可以花費你的資金;沒有它,遺失裝置即等於損失資金。你可以在設定中輸入密碼再次查看。", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "這筆資金會離開隱私池。目的地址和金額會在另一條鏈上公開。", "This link is too long for a QR code — copy it instead.": "此連結太長,無法製成 QR 碼,請改為複製。", @@ -861,6 +940,10 @@ export default { "This note was not found.": "找不到此票據。", "This passkey does not support the PRF extension.": "此密碼匙不支援 PRF 擴充功能。", "This proof was already submitted from another window. Check Activity before sending again.": "此證明已從另一個視窗提交。再次傳送前請先查看記錄。", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "此提案在高度 {expires} 過期,而鏈已去到 {head}:已來不及簽署。請索取新的提案。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "此提案綁定的創世區塊與此錢包的節點所提供的不同。", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "此提案屬於另一個政策:它指向 {theirs},而此政策是 {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "此提案屬於鏈 {theirs};此錢包在鏈 {ours} 上。", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "此證明器每個證明收取 {fee} RAND,而此版本的錢包不會支付證明器費用:請配對不收費的證明器,或在此裝置上生成證明", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "此證明器會收取費用,而此版本的錢包不會支付。請配對不收費的證明器,或從桌面應用程式傳送。", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "此證明器不接受這類工作({reason})。請在設定中配對另一部證明器,或從桌面應用程式傳送。", @@ -872,6 +955,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "這會從此裝置移除錢包。你需要恢復密鑰才能還原它。", "This screen could not be shown.": "無法顯示此畫面。", "This screen ships in a later task. Your wallet is unlocked and ready.": "此畫面會在稍後的工作中推出。你的錢包已解鎖並可使用。", + "this signer is already saved as {label}": "此簽署人已儲存為 {label}", + "This signer profile has no key on this device: remove it and add it again.": "此簽署人資料在此裝置上沒有金鑰:請移除後重新新增。", "This tab is showing what it has so far, and will refresh when that finishes.": "此分頁正顯示目前已有的內容,完成後會重新整理。", "This takes a few minutes; keep this window open.": "需時數分鐘;請保持此視窗開啟。", "This transaction was not found.": "找不到此交易。", @@ -918,6 +1003,7 @@ export default { "Type {word} to confirm": "輸入 {word} 以確認", "Type it once more.": "請再輸入一次。", "Type the last four characters of that address": "輸入該地址的最後四個字元", + "Type the last four characters of the account fingerprint": "請輸入帳戶指紋的最後四個字元", "unavailable in this app": "此應用程式不提供", "unknown": "不明", "unknown parameter {name}": "未知參數 {name}", @@ -954,6 +1040,7 @@ export default { "Weak": "弱", "Welcome back": "歡迎回來", "What the pool pays you arrives as a note your wallet finds on its next sync.": "資金池支付給你的金額會以票據形式到帳,你的錢包會在下次同步時找到它。", + "What this proposal does, as checked against your policy": "此提案的作用(已對照你的政策核對)", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "如此裝置無法生成證明,請配對證明器。無論如何,你的花費私鑰都會留在這裏;由你自己運行的證明器(桌面應用程式,或你自己機器上的 rand-prover)亦能令你的記錄保持私密。", "Where to?": "傳送到哪裏?", "Which address?": "哪個地址?", @@ -1000,6 +1087,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "你的證明器已生成證明 {n} 分鐘仍未完成。請稍後繼續,或取消。", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "你的證明器在佇列中 {n} 分鐘後仍未開始此證明。請稍後繼續,或取消。", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "無法開啟你的證明器配對。請鎖定再解鎖錢包,或在設定中重新配對證明器。", + "Your share — send it to whoever proves the proposal": "你的簽署份額 — 請交給負責證明提案的人", "Your wallet picks up what it pays you on its next sync.": "你的錢包會在下次同步時收到其支付給你的金額。", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "你錢包的記錄是從 {chain} 讀取的。沒有任何改變,在兩者一致之前,此錢包不會傳送或索取資金。請在設定中切換節點,或為新鏈重新掃描此錢包。", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "這是 {name} 證明器的地址。無需配對:本錢包預設使用它們。", diff --git a/ui/locales/zh.js b/ui/locales/zh.js index f3854ce..3939217 100644 --- a/ui/locales/zh.js +++ b/ui/locales/zh.js @@ -1,6 +1,7 @@ // 中文(简体). Glossary kept as is: RAND, zUSD, RPL, Rand, Rand Wallet, RandProtocol, USDT, USDC, STARK, randscan. export default { "{amount} {symbol}": "{amount} {symbol}", + "{blocks} blocks left to prove it.": "还差 {blocks} 个区块即可证明。", "{chain} · {genesis}": "{chain} · {genesis}", "{count} peer": "{count} 个对等节点", "{count} peers": "{count} 个对等节点", @@ -29,6 +30,7 @@ export default { "{label} is off": "{label} 已关闭", "{label} is on": "{label} 已开启", "{label} no longer opens this wallet (its password changed). Unlock with the password, then turn {label} on again in Settings.": "{label} 已无法打开此钱包(钱包密码已更改)。请用密码解锁,然后在设置中重新开启 {label}。", + "{matched} of {total} payments matched.": "{total} 笔付款中有 {matched} 笔匹配。", "{member} answered with another key than the one this wallet pins": "{member} 返回的密钥与此钱包固定的密钥不同", "{member} did not answer": "{member} 没有响应", "{member} does not take this wallet's jobs": "{member} 不接受此钱包的任务", @@ -49,6 +51,8 @@ export default { "{symbol} cannot be withdrawn": "{symbol} 无法提取", "{symbol} has {decimals} decimal places — that is more.": "{symbol} 只有 {decimals} 位小数,你输入的位数更多。", "{symbol} is released as the coin you pick, on that coin's own chain.": "{symbol} 将以你选择的币种,在该币种所在的链上释放。", + "{threshold} of {count} · signer {index}": "{threshold}/{count} · 签名人 {index}", + "{threshold} of {count} · this key is signer {index}": "{threshold}/{count} · 此密钥是签名人 {index}", "{token} on chain {chain} has {decimals} decimals: the amount and the relayer fee must be multiples of {unit}": "链 {chain} 上的 {token} 有 {decimals} 位小数:金额和中继费必须是 {unit} 的整数倍", "{units} units of asset {asset}": "资产 {asset} 的 {units} 个单位", "{url} answered first — chain {chain} · height {height}.": "{url} 最先响应——链 {chain} · 高度 {height}。", @@ -81,7 +85,12 @@ export default { "A prover address must be https://.": "证明器地址必须是 https://。", "A prover is making the proof": "证明器正在生成证明", "A prover sees your history": "证明器能看到你的历史记录", + "a share is index:hex": "签名份额的格式为 index:hex", "A shielded wallet for the Rand network. Balances and history stay private — every send is proved on this device.": "Rand 网络的屏蔽钱包。余额和历史记录保持私密——每笔发送都在此设备上生成证明。", + "a signer key is a rand-node keygen file or 64 hex characters": "签名人密钥是 rand-node 的 keygen 文件或 64 位十六进制字符", + "a signer label is 1-64 characters": "签名人标签为 1-64 个字符", + "A signer profile is kept in this wallet's encrypted vault. Every signature asks for your password again.": "签名人资料保存在此钱包的加密保险库中。每次签名都会再次要求输入密码。", + "a signer seed is 64 hex characters": "签名人种子为 64 位十六进制字符", "A site": "一个网站", "a spend key is 64 hex characters, or a wallet.key.json": "花费密钥是 64 个十六进制字符,或一个 wallet.key.json 文件", "A transaction hash is tried first; if nothing is found, the same hash is tried as a block. All digits looks up a block by height.": "先按交易哈希查找;若未找到,再将同一哈希作为区块查找。纯数字则按高度查找区块。", @@ -98,8 +107,12 @@ export default { "A withdrawal to one it does not list may be refused. Nothing is spent until it is proved.": "提取到未列出的链可能会被拒绝。在证明完成前不会花费任何资金。", "About": "关于", "about {left} left": "约剩 {left}", + "Account fingerprint — compare it with every other signer before you save": "账户指纹 — 保存前请与其他每位签名人核对", "Activity": "活动", "Add a contact": "添加联系人", + "Add a signer": "添加签名人", + "Add payment": "添加付款", + "Add the policy file and your signer key to sign for a private multisig account.": "添加策略文件和你的签名人密钥,即可为私密多签账户签名。", "Address": "地址", "Address copied": "地址已复制", "Address or payment link": "地址或收款链接", @@ -108,14 +121,18 @@ export default { "almost done": "即将完成", "also tried {endpoints}": "还尝试了 {endpoints}", "Amount": "金额", + "Amount in RAND": "金额(RAND)", "amount must be greater than zero": "金额必须大于零", "amount overflow": "金额溢出", + "Amounts cannot be checked for non-RAND payments: leave the amount empty for those.": "非 RAND 付款无法核对金额:这类付款请将金额留空。", + "This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.": "此收款人同时收取 RAND 和代币:向同一收款人支付 RAND 和代币需要分开的提案。", "amounts overflow": "金额溢出", "an action is {n} characters, more than {max}": "操作长度为 {n} 个字符,超过上限 {max}", "An address (rand1…), a payment link (randpay:…), or a saved contact’s name.": "地址(rand1…)、收款链接(randpay:…)或已保存联系人的名称。", "An address here is hexadecimal — 0-9 and a-f only.": "此处地址为十六进制——只能包含 0-9 和 a-f。", "An address on this chain is 20 bytes — 40 hex characters. That is {length}.": "这条链上的地址为 20 字节——40 个十六进制字符。你输入的是 {length} 个。", "An address on this chain is 32 bytes — 64 hex characters. That is {length}.": "这条链上的地址为 32 字节——64 个十六进制字符。你输入的是 {length} 个。", + "An amount is a number of RAND, such as 1.5.": "金额是 RAND 的数值,例如 1.5。", "an unknown chain": "未知的链", "another tab changed this wallet while it was syncing; try again": "同步期间另一个标签页修改了此钱包;请重试", "Another tab is syncing": "另一个标签页正在同步", @@ -182,11 +199,16 @@ export default { "Chains known": "已知的链", "Change back to you": "找零给你", "Change it": "修改", + "Check": "核对", + "Check a share": "核对签名份额", "Check Activity": "查看活动", "Check Activity first": "请先查看活动", "Check before saving": "保存前请核对", "Check fingerprint": "核对指纹", + "Check payments": "核对付款", + "Check proposal": "核对提案", "Check settings": "检查设置", + "Check share": "核对签名份额", "Checking it against the chain before anything is proved.": "在生成任何证明之前,先与链上数据核对。", "Checking the pool and your notes…": "正在检查资金池和你的票据…", "Checking the swap…": "正在检查兑换…", @@ -213,6 +235,7 @@ export default { "Continue": "继续", "Copied": "已复制", "Copied as JSON": "已复制为 JSON", + "Copy": "复制", "Copy address": "复制地址", "Copy as JSON": "复制为 JSON", "Copy link": "复制链接", @@ -261,9 +284,13 @@ export default { "Enter an amount to send.": "输入要发送的金额。", "Enter an amount.": "输入金额。", "Enter at least 10 characters": "至少输入 10 个字符", + "Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.": "请输入你打算进行的每一笔付款:地址和以 RAND 计的金额。向同一地址的两笔付款只输入一次,填合计金额。与其中任何一笔都不匹配的输出无法签名。", + "Enter the address of every payee: an output that does not match the address you entered, or whose address is empty, cannot be signed.": "请输入每位收款人的地址:与所输入地址不匹配、或地址为空的输出无法签名。", + "Enter the address of every payment.": "请输入每笔付款的地址。", "Enter the address this should be released to.": "输入释放的目标地址。", "Enter the address you are sending to.": "输入收款地址。", "Enter the amount as a number, for example 1.25.": "请以数字输入金额,例如 1.25。", + "Enter the amount of every payment: it is what stops a split or an inflated payment.": "请输入每笔付款的金额:它可以防止拆分或虚增付款。", "Enter the first 4 characters of your recovery key.": "输入恢复密钥的前 4 个字符。", "Enter the relayer fee as a number, for example 1.25.": "请以数字输入中继费,例如 1.25。", "Enter this wallet's password — the pairing is sealed under it.": "输入此钱包的密码——配对信息由它加密保存。", @@ -273,6 +300,8 @@ export default { "Enter your recovery key.": "输入恢复密钥。", "Epoch": "周期", "Exactly those four letters, in capitals.": "就是这四个字符,须大写。", + "expected recipient {address}: {reason}": "预期收款人 {address}:{reason}", + "expected_recipients must be a list of addresses": "expected_recipients 必须是地址列表", "Explore": "浏览", "Export spend key": "导出花费密钥", "Exporting the spend key is behind your password.": "导出花费密钥需要输入密码。", @@ -281,6 +310,7 @@ export default { "Fee": "手续费", "fee must be at least {fee} RAND (the bridge burn floor)": "手续费至少为 {fee} RAND(跨链桥销毁下限)", "fee must be at least {fee} RAND (the bundle floor)": "手续费至少为 {fee} RAND(交易包下限)", + "Fee: {fee} RAND · expires at height {height}": "手续费:{fee} RAND · 在高度 {height} 过期", "Filter activity by asset": "按资产筛选活动", "Fingerprint": "指纹", "fingerprint {fp}": "指纹 {fp}", @@ -318,9 +348,11 @@ export default { "I understand anyone with this key can spend my funds": "我了解任何拥有此密钥的人都能花费我的资金", "Import wallet": "导入钱包", "Incorrect password.": "密码错误。", + "index:hex": "index:hex", "inputs hold {held}, but amount + fee is {need}": "输入共有 {held},但金额 + 手续费为 {need}", "inputs hold {held}, but fee + RAND deposit is {need}": "输入共有 {held},但手续费 + RAND 存入额为 {need}", "insufficient balance: have {have}, need {need}": "余额不足:现有 {have},需要 {need}", + "Intended payments": "拟进行的付款", "is on chain {chain}, not {expected}": "在链 {chain} 上,而不是 {expected}", "It cannot see your balance or your activity, and it cannot move anything.": "它看不到你的余额和活动,也不能转移任何资产。", "It cannot spend.": "它不能花费资金。", @@ -343,6 +375,7 @@ export default { "key file has no spend_key": "密钥文件中没有 spend_key", "key file must be version 2": "密钥文件必须是版本 2", "Key hidden — enter your password to view it again.": "密钥已隐藏——输入密码可再次查看。", + "Label": "标签", "Language": "语言", "Leave empty to let the payer choose.": "留空则由付款人决定。", "Light": "浅色", @@ -353,11 +386,15 @@ export default { "machines": "多台机器", "malformed parameter {parameter}": "参数 {parameter} 格式错误", "Manage contacts": "管理联系人", + "Manage multisig signers": "管理多签签名人", "Max": "最大", "Memo": "备注", "memo \"{memo}\"": "备注“{memo}”", "memo is {n} bytes, at most {max}": "备注为 {n} 字节,最多 {max} 字节", "More is on the way": "更多内容即将推出", + "Multisig share": "多签签名份额", + "Multisig signers": "多签签名人", + "Multisig signing is not available in this app.": "此应用不支持多签签名。", "My own prover · {name}": "我自己的证明器 · {name}", "Name": "名称", "Names for the addresses you pay often, kept on this device only. Send to a contact by name; its fingerprint is checked when you save it.": "为常用的收款地址起个名称,仅保存在此设备上。可按名称向联系人发送;保存时会核对其指纹。", @@ -381,12 +418,16 @@ export default { "No contacts yet. Save one from Settings, or paste an address.": "暂无联系人。可在设置中保存一个,或粘贴地址。", "no endpoint could answer {method}": "没有端点能响应 {method}", "no endpoint could answer {method}; tried {endpoints}": "没有端点能响应 {method};已尝试 {endpoints}", + "no expected recipient given: the payee was NOT checked": "未给出预期收款人:收款人未经核对(NOT checked)", + "No node answered at {url}, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.": "{url} 没有节点应答,因此无法核对过期时间,此提案不会签名。待节点可访问后重试。", "No proof is pending.": "没有未完成的证明。", "No prover": "无证明器", "No prover is paired.": "未配对证明器。", "No RAND to send yet": "还没有可发送的 RAND", "No registry assets": "没有注册资产", "no reply": "无回复", + "No signer profiles yet": "尚无签名人资料", + "No such signer profile.": "没有这个签名人资料。", "No transaction or block matches that.": "没有匹配的交易或区块。", "no wallet on this device": "此设备上没有钱包", "None of the built-in RandProtocol prover links names the key this wallet pins for it; not using them.": "内置的 RandProtocol 证明器链接都没有给出此钱包为其固定的密钥;不使用它们。", @@ -433,9 +474,11 @@ export default { "One proof runs on this device — about two minutes. You can look at other screens; closing the wallet stops it.": "此设备上会运行一个证明——约两分钟。你可以查看其他页面;关闭钱包会中止证明。", "only {locked} is locked in that coin on chain {chain}; choose another backing or a smaller amount": "链 {chain} 上该币种仅锁定了 {locked};请选择其他锚定币或更小的金额", "Open durian.market": "打开 durian.market", + "Open file": "打开文件", "Open in explorer": "在浏览器中打开", "Open in randscan": "在 randscan 中打开", "Open in side panel": "在侧边栏中打开", + "Open in the side panel to choose files": "请在侧边栏中打开以选择文件", "Open Rand Wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "用 {label} 打开 Rand Wallet,无需输入密码。密码仍然有效,并且仍是恢复访问的方式。", "Open the wallet with {label} instead of typing your password. Your password still works, and stays the way to restore access.": "用 {label} 打开钱包,无需输入密码。密码仍然有效,并且仍是恢复访问的方式。", "Open this window from a site that asked to connect.": "请从请求连接的网站打开此窗口。", @@ -445,6 +488,7 @@ export default { "Or use the RandProtocol provers — {n} machines run by the validators, each with its own key.": "或使用 RandProtocol 证明器——由验证者运行的 {n} 台机器,各有自己的密钥。", "Or use the RandProtocol provers — machines run by the validators, each with its own key.": "或使用 RandProtocol 证明器——由验证者运行的机器,各有自己的密钥。", "Outcome unknown": "结果未知", + "output {slot} does not open as this account's: refusing to sign value it cannot see": "输出 {slot} 无法作为此账户的输出打开:拒绝对看不到的金额签名", "own={value}, expected 1 or absent": "own={value},应为 1 或不设置", "Paid": "已支付", "Pair your own prover": "配对你自己的证明器", @@ -465,17 +509,20 @@ export default { "Paste your recovery key": "粘贴你的恢复密钥", "path is not {n} levels": "路径不是 {n} 层", "Pay me on Rand": "在 Rand 上向我付款", + "Payee address": "收款人地址", "Payment link": "收款链接", "Payment link · fingerprint {fingerprint}": "收款链接 · 指纹 {fingerprint}", "Payment link copied": "收款链接已复制", "Payment links cannot be read in this app; paste the address instead.": "此应用无法读取收款链接;请改为粘贴地址。", "Payment links cannot be read in this app.": "此应用无法读取收款链接。", + "Payments changed: check them again before signing.": "付款已变更:签名前请重新核对。", "Peers": "对等节点", "Pending": "待确认", "Permission to reach that host was not granted, so the node was left as it was.": "未获得访问该主机的权限,因此节点保持不变。", "Permission to reach that host was not granted.": "未获得访问该主机的权限。", "Permission to reach that prover was not granted, so nothing was saved.": "未获得访问该证明器的权限,因此未保存任何内容。", "Point the camera at a Rand QR code.": "将摄像头对准 Rand 二维码。", + "Policy file": "策略文件", "Pool fee": "资金池手续费", "Price impact": "价格影响", "Priced by the durian.market pools on this chain ({pct}% pool fee). The swap is a transaction this wallet proves and sends itself.": "价格由这条链上的 durian.market 资金池给出(资金池手续费 {pct}%)。兑换是一笔由此钱包自行证明并发送的交易。", @@ -485,6 +532,7 @@ export default { "Proofs are made on this device only. Where it cannot make one, sending waits until you pair a prover or use the RandProtocol prover again.": "证明只在此设备上生成。在无法生成证明时,发送会一直等待,直到你配对证明器或重新使用 RandProtocol 证明器。", "Proofs this device cannot make go to {name}. Its fingerprint is {fingerprint} — check that the prover shows the same.": "此设备无法生成的证明将交给 {name}。其指纹为 {fingerprint}——请核对证明器显示的指纹是否一致。", "Proofs this device cannot make go to one of them; each one that proves a send sees that wallet's viewing key.": "此设备无法生成的证明将交给其中一个证明器;为发送生成证明的每个证明器都能看到该钱包的查看密钥。", + "Proposal (proposal.json)": "提案(proposal.json)", "Prove and send": "证明并发送", "Prove for my other devices": "为我的其他设备生成证明", "Prover": "证明器", @@ -533,6 +581,7 @@ export default { "Relayer fee (optional)": "中继费(可选)", "Remove": "移除", "Remove {name}": "移除 {name}", + "Remove this signer": "移除此签名人", "Request RAND": "申请 RAND", "Request RAND from the faucet to try sending, receiving and proving on this network.": "从水龙头申请 RAND,在此网络上试用发送、接收和证明。", "Rescan": "重新扫描", @@ -567,8 +616,12 @@ export default { "Running on": "运行于", "Save": "保存", "Save an address you pay often, and send to it by name.": "保存常用的收款地址,即可按名称发送。", + "Save as file": "另存为文件", + "Save signer": "保存签名人", "Save your recovery key": "保存你的恢复密钥", "Saved": "已保存", + "Saved to {path}": "已保存到 {path}", + "Saved.": "已保存。", "Scan": "扫描", "Scan a payment link": "扫描收款链接", "Scan a QR code": "扫描二维码", @@ -589,6 +642,7 @@ export default { "Sent.": "已发送。", "Set on this device when this wallet was created.": "在此设备上创建此钱包时设置。", "Settings": "设置", + "Share": "签名份额", "Share payment link": "分享收款链接", "shielded address decodes to {n} bytes, expected {expected}": "屏蔽地址解码后为 {n} 字节,应为 {expected}", "shielded address is not base58": "屏蔽地址不是 base58", @@ -600,6 +654,12 @@ export default { "Show the full address": "显示完整地址", "Show viewing key": "显示查看密钥", "Show viewing key for 10 seconds": "显示查看密钥 10 秒", + "Sign": "签名", + "Sign a proposal": "签署提案", + "signer {index} is not in this policy (it has {count} signers)": "签名人 {index} 不在此策略中(该策略有 {count} 位签名人)", + "Signer {index}'s share verifies. {have} of {threshold} in hand: signers {list}.": "签名人 {index} 的签名份额验证通过。已收集 {have}/{threshold}:签名人 {list}。", + "signer {index}'s signature does not verify over this proposal": "签名人 {index} 的签名对此提案验证未通过", + "Signer key file, or its 64-hex seed": "签名人密钥文件,或其 64 位十六进制种子", "So the address above is one you have actually read.": "以确认你确实核对过上面的地址。", "Something went wrong": "出错了", "Something went wrong.": "出错了。", @@ -632,6 +692,7 @@ export default { "That could not be copied.": "无法复制。", "That could not be saved.": "无法保存。", "That does not match — reveal the key again and check.": "不一致——请再次显示密钥并核对。", + "That file is larger than a proposal, a policy or a key file can be.": "该文件大于提案、策略或密钥文件所允许的大小。", "That is more than your balance of {balance} {symbol}.": "超出了你的余额 {balance} {symbol}。", "That is neither 64 hex characters (a hash) nor a block height (digits only).": "这既不是 64 个十六进制字符(哈希),也不是区块高度(纯数字)。", "That is not a pairing link.": "这不是配对链接。", @@ -667,6 +728,8 @@ export default { "The fee could not be worked out.": "无法计算手续费。", "the genesis hash is not hex of a plausible length": "创世哈希不是合理长度的十六进制", "the inflow is `none` exactly when the bundle burns no token (inflow.amount == 0)": "当且仅当交易包不销毁任何代币(inflow.amount == 0)时,inflow 为 `none`", + "the key file's address is not the one its seed derives": "密钥文件中的地址与其种子派生出的地址不一致", + "the key file's public_key is not the one its seed derives": "密钥文件中的 public_key 与其种子派生出的不一致", "The key you exported when this wallet was created.": "创建此钱包时你导出的密钥。", "The language could not be saved.": "无法保存语言设置。", "The link asks for {linked} {symbol}; you typed {typed} {symbol}.": "链接请求 {linked} {symbol};你输入的是 {typed} {symbol}。", @@ -707,6 +770,7 @@ export default { "The password that unlocks this wallet on this device.": "在此设备上解锁此钱包的密码。", "The payer decides": "由付款人决定", "The pending proof was cleared (the wallet locked, or it was cancelled elsewhere), so nothing was sent. Send again.": "未完成的证明已被清除(钱包已锁定,或在别处被取消),因此未发送任何内容。请重新发送。", + "the policy does not parse: {reason}": "策略无法解析:{reason}", "The pool changed since this page read it. Nothing was sent; the site can quote again.": "自此页面读取以来资金池已发生变化。未发送任何内容;网站可以重新报价。", "The pool changed while this was being proved. Nothing was sent; the site can quote again.": "生成证明期间资金池发生了变化。未发送任何内容;网站可以重新报价。", "The pool does not hold enough to pay this out. Nothing was sent; the site can quote again.": "资金池余额不足以支付。未发送任何内容;网站可以重新报价。", @@ -717,6 +781,12 @@ export default { "the proof does not verify: {reason}": "证明验证失败:{reason}", "the proof published a digest this wallet did not build ({published} for {expected}); not using it": "证明公布的摘要并非此钱包所构建({published},应为 {expected});不使用该证明", "The proof runs on this device. You can look at other screens — it keeps going — but closing the wallet stops it.": "证明在此设备上运行。你可以查看其他页面——证明会继续进行——但关闭钱包会中止它。", + "The proposal check returned an incomplete answer, so this proposal is not signed.": "提案核对返回了不完整的结果,因此此提案不会签名。", + "the proposal does not parse: {reason}": "提案无法解析:{reason}", + "the proposal expires at {expires}, outside its bundle's window ({from}, {to}]": "提案在 {expires} 过期,超出其捆绑包的有效区间 ({from}, {to}]", + "the proposal's auth_commit is not this policy's account": "提案的 auth_commit 不是此策略的账户", + "the proposal's expected digest is not its bundle's": "提案的预期摘要与其捆绑包的不一致", + "the proposal's signing_message is not its binding's": "提案的 signing_message 与其绑定的不一致", "the prover": "证明器", "the prover accepted the job but named no job id": "证明器接受了任务,但未给出任务 ID", "The prover answered with an unknown state ({state}).": "证明器返回了未知状态({state})。", @@ -778,6 +848,7 @@ export default { "The request was not approved in Rand Wallet.": "该请求未在 Rand Wallet 中获得批准。", "The rescan could not be started.": "无法开始重新扫描。", "The rescan did not finish": "重新扫描未完成", + "the saved signer profiles could not be opened; nothing was changed. Restore this device's storage or wipe the wallet and restore it from its key": "无法打开已保存的签名人资料;未作任何更改。请恢复此设备的存储,或清除钱包后用其密钥恢复", "The scan was cancelled.": "扫描已取消。", "The side panel could not be opened.": "无法打开侧边栏。", "The site sent an invoke this wallet cannot read: {reason}.": "网站发送的调用此钱包无法读取:{reason}。", @@ -788,6 +859,10 @@ export default { "The swap did not go through.": "兑换未成功。", "The swap keeps going. Closing the wallet before it says sent stops it, and nothing is lost.": "兑换会继续进行。在显示已发送前关闭钱包会中止兑换,但不会造成任何损失。", "The theme could not be saved.": "无法保存主题。", + "the transaction carries an action; a private multisig proposal is a payment from its bundle alone": "交易带有一个动作;私密多签提案只是仅来自其捆绑包的付款", + "the transaction carries no bundle": "交易不含捆绑包", + "the transaction does not hash to the proposal's binding": "交易的哈希不是提案的绑定", + "the transaction is for chain {tx}, the proposal says chain {proposal}": "交易属于链 {tx},而提案指向链 {proposal}", "The transaction key discloses this one payment — its amount and its recipient — to whoever you give it to. It cannot spend anything.": "交易密钥会向你提供给的人披露这一笔付款——其金额和收款人。它不能花费任何资金。", "The transaction may already be on chain. Check Activity before withdrawing again — withdrawing twice would burn twice.": "该交易可能已经上链。再次提取前请先查看活动——提取两次就会销毁两次。", "The transfer could not be proved.": "无法为该转账生成证明。", @@ -805,6 +880,9 @@ export default { "The wallet was told: {message}": "钱包收到的信息:{message}", "The withdrawal could not be proved.": "无法为该提取生成证明。", "The withdrawal was submitted, but the node did not return a usable hash.": "提取已提交,但节点没有返回可用的哈希。", + "the witness's value cannot be checked: its nk or salt is not this proposal's": "无法核对见证数据的金额:其 nk 或盐值不属于此提案", + "the witness's value does not match the outputs: asset {asset} has {held} in and {spent} out with the fee and burns": "见证数据的金额与输出不一致:资产 {asset} 输入 {held},输出 {spent}(含手续费和销毁)", + "the witness's value does not match the transaction: input {input} does not nullify to the bundle's nullifier": "见证数据的金额与交易不一致:输入 {input} 未对应到捆绑包的 nullifier", "Theme": "主题", "There is no pool for that pair.": "该交易对没有资金池。", "There is no program {program}… on this chain. Nothing was sent.": "这条链上没有程序 {program}…。未发送任何内容。", @@ -846,6 +924,7 @@ export default { "This device, or where it cannot · {name} provers": "此设备,无法生成时使用 · {name} 证明器", "This forgets the notes and history read from the old chain and reads this node from the start. Your keys and your password are not touched.": "这会忘记从旧链读取的票据和历史记录,并从头读取此节点。你的密钥和密码不受影响。", "This key can spend your funds": "此密钥可以花费你的资金", + "this key is not one of the policy's signers": "此密钥不属于该策略的签名人", "This key is your wallet. Anyone who has it can spend your funds, and without it a lost device means lost funds. You can view it again in Settings with your password.": "此密钥就是你的钱包。任何拥有它的人都能花费你的资金;没有它,设备丢失就意味着资金丢失。你可以在设置中输入密码再次查看。", "This leaves the shielded pool. The destination address and amount become public on the other chain.": "这会离开屏蔽池。目标地址和金额将在另一条链上公开。", "This link is too long for a QR code — copy it instead.": "此链接太长,无法生成二维码——请改为复制。", @@ -861,6 +940,10 @@ export default { "This note was not found.": "未找到此票据。", "This passkey does not support the PRF extension.": "此通行密钥不支持 PRF 扩展。", "This proof was already submitted from another window. Check Activity before sending again.": "此证明已从另一个窗口提交。再次发送前请先查看活动。", + "This proposal expires at height {expires} and the chain is at {head}: too late to sign. Ask for a new proposal.": "此提案在高度 {expires} 过期,而链已到 {head}:已来不及签名。请索取新的提案。", + "This proposal is bound to another genesis than the one this wallet's node serves.": "此提案绑定的创世区块与此钱包的节点所提供的不同。", + "this proposal is for another policy: it names {theirs}, this policy is {ours}": "此提案属于另一个策略:它指向 {theirs},而此策略是 {ours}", + "This proposal is for chain {theirs}; this wallet is on chain {ours}.": "此提案属于链 {theirs};此钱包在链 {ours} 上。", "this prover charges {fee} RAND per proof, and this version of the wallet does not pay a prover's fee: pair a prover that charges nothing, or prove on this device": "此证明器每个证明收取 {fee} RAND,而此版本的钱包不支付证明器费用:请配对一个免费的证明器,或在此设备上生成证明", "This prover charges a fee, which this version of the wallet does not pay. Pair a prover that charges nothing, or send from the desktop app.": "此证明器收取费用,而此版本的钱包不支付该费用。请配对一个免费的证明器,或从桌面应用发送。", "This prover does not accept this kind of job ({reason}). Pair another prover in Settings, or send from the desktop app.": "此证明器不接受此类任务({reason})。请在设置中配对其他证明器,或从桌面应用发送。", @@ -872,6 +955,8 @@ export default { "This removes the wallet from this device. You will need your recovery key to restore it.": "这会从此设备上移除钱包。你需要恢复密钥才能恢复它。", "This screen could not be shown.": "无法显示此页面。", "This screen ships in a later task. Your wallet is unlocked and ready.": "此页面将在后续版本中推出。你的钱包已解锁,可以使用。", + "this signer is already saved as {label}": "此签名人已保存为 {label}", + "This signer profile has no key on this device: remove it and add it again.": "此签名人资料在此设备上没有密钥:请移除后重新添加。", "This tab is showing what it has so far, and will refresh when that finishes.": "此标签页显示的是目前已有的内容,完成后会自动刷新。", "This takes a few minutes; keep this window open.": "这需要几分钟;请保持此窗口打开。", "This transaction was not found.": "未找到此交易。", @@ -918,6 +1003,7 @@ export default { "Type {word} to confirm": "输入 {word} 以确认", "Type it once more.": "再输入一次。", "Type the last four characters of that address": "输入该地址的最后四个字符", + "Type the last four characters of the account fingerprint": "请输入账户指纹的后四个字符", "unavailable in this app": "此应用中不可用", "unknown": "未知", "unknown parameter {name}": "未知参数 {name}", @@ -954,6 +1040,7 @@ export default { "Weak": "弱", "Welcome back": "欢迎回来", "What the pool pays you arrives as a note your wallet finds on its next sync.": "资金池支付给你的资产将以票据形式到账,钱包会在下次同步时找到它。", + "What this proposal does, as checked against your policy": "此提案的作用(已对照你的策略核对)", "Where this device cannot make a proof, pair a prover. Your spend key stays here either way; a prover you run yourself — the desktop app, or rand-prover on your own machine — also keeps your history to yourself.": "在此设备无法生成证明时,请配对证明器。无论哪种方式,你的花费密钥都留在这里;你自己运行的证明器——桌面应用,或你自己机器上的 rand-prover——还能让你的历史记录只有你自己知道。", "Where to?": "发往哪里?", "Which address?": "哪个地址?", @@ -1000,6 +1087,7 @@ export default { "Your prover has not finished after {n} minutes of proving. Resume later, or cancel.": "你的证明器已证明 {n} 分钟仍未完成。请稍后继续,或取消。", "Your prover has not started this proof after {n} minutes in its queue. Resume later, or cancel.": "此证明已在你的证明器队列中等待 {n} 分钟仍未开始。请稍后继续,或取消。", "Your prover's pairing could not be opened. Lock and unlock the wallet, or pair the prover again in Settings.": "无法打开你的证明器配对信息。请锁定后重新解锁钱包,或在设置中重新配对证明器。", + "Your share — send it to whoever proves the proposal": "你的签名份额 — 请发给负责证明提案的人", "Your wallet picks up what it pays you on its next sync.": "你的钱包会在下次同步时收到支付给你的资产。", "Your wallet's history was read from {chain} — nothing has been changed, and this wallet will not send or request funds until the two agree. Switch node in Settings, or rescan this wallet for the new chain.": "你的钱包历史记录读取自 {chain}——没有做任何更改,在两者一致之前此钱包不会发送或请求资金。请在设置中切换节点,或为新链重新扫描此钱包。", "That is the address of the {name} provers. They need no pairing: this wallet uses them by default.": "这是 {name} 证明器的地址。无需配对:本钱包默认使用它们。", diff --git a/ui/screens/settings.js b/ui/screens/settings.js index f472ece..99e67f5 100644 --- a/ui/screens/settings.js +++ b/ui/screens/settings.js @@ -275,6 +275,12 @@ function contactsMarkup() { `); } +// Offered only where the backend has the (optional) `signers` group. +function signersMarkup() { + return raw(h`

${t('Multisig signers')}

+ `); +} + function appearanceMarkup(settings) { const current = settings.theme || 'system'; const segments = THEMES.map((theme) => h` @@ -395,6 +401,7 @@ registerScreen('settings', { ${raw(proverGroup ? proverMarkup(settings, platform, proverHost ? proverHostMarkup() : '') : proverHost ? sectionMarkup(t('Prover'), proverHostMarkup()) : '')} ${raw(ctx.backend.contacts && typeof ctx.backend.contacts.list === 'function' ? contactsMarkup() : '')} + ${raw(ctx.backend.signers && typeof ctx.backend.signers.list === 'function' ? signersMarkup() : '')} ${raw(appearanceMarkup(settings))} ${raw(securityMarkup(settings))} ${raw(aboutMarkup(platform, settings))}`; diff --git a/ui/screens/signers.js b/ui/screens/signers.js new file mode 100644 index 0000000..271bc33 --- /dev/null +++ b/ui/screens/signers.js @@ -0,0 +1,346 @@ +// Private multisig signing (spec 2026-10-11-msauth-signing-design.md §4): `#signers` lists the +// signer profiles and adds one; `#signer/` checks a proposal, shows exactly what the core +// checked, signs it after the fingerprint's last four characters and the password, and checks a +// share. Every value from a file, a paste, storage or the core reaches the page as text. The +// proposal lives in this screen's closure only and is dropped when it unmounts. +import { h, raw } from '../lib/dom.js'; +import { icons } from '../lib/icons.js'; +import { registerScreen } from '../app.js'; +import { formatUnits, parseUnits } from '../lib/format.js'; +import { t } from '../i18n.js'; +import { isZero, isNonRand, payeeAssets } from '../engine/signers.js'; + +export const MAX_FILE_BYTES = 1 << 20; +/** An output the signer must name a payee for: it leaves the account and moves something. Decided + * here from the output itself, not from the core's own flag. */ +export const needsPayee = (o) => o && o.to_account !== true && !isZero(o.amount); +export const isMatched = (o) => typeof o.matches === 'string' && o.matches.trim() !== ''; +/** + * One intended payment as the core's `addr:AMOUNT`. AMOUNT is RAND in display units, the wallet's + * decimal text, as the send screen takes it (checked by `parseUnits`, passed as typed and trimmed). + * The amount is required unless `amountOptional` (a non-RAND paying output: the core refuses a pin + * there, so that payee is the address alone). Throws an Error whose message is for the user. + */ +export function entryOf({ addr, amount }, amountOptional = false) { + const a = String(addr || '').trim(); + const v = String(amount || '').trim(); + if (a === '') throw new Error(t('Enter the address of every payment.')); + if (v === '') { + if (amountOptional) return a; + throw new Error(t('Enter the amount of every payment: it is what stops a split or an inflated payment.')); + } + let units; + try { units = parseUnits(v); } catch { throw new Error(t('An amount is a number of RAND, such as 1.5.')); } + if (units <= 0n) throw new Error(t('An amount is a number of RAND, such as 1.5.')); + return `${a}:${v}`; +} +const unitsOf = (a) => { try { return formatUnits(BigInt(String(a).trim() || '0')); } catch { return String(a); } }; + +export function lastFourOf(fingerprint) { + return String(fingerprint || '').replace(/-/g, '').slice(-4).toLowerCase(); +} +export function confirmsFingerprint(fingerprint, typed) { + const want = lastFourOf(fingerprint); + return want.length === 4 && String(typed || '').replace(/[-\s]/g, '').toLowerCase() === want; +} +async function readPicked(input) { + const file = input.files && input.files[0]; + if (!file) return null; + if (file.size > MAX_FILE_BYTES) throw new Error(t('That file is larger than a proposal, a policy or a key file can be.')); + return file.text(); +} +const topbar = (title, back) => h`
${title}
`; +function fileField(ctx, { role, label }) { + const popup = ctx.mode === 'popup'; + const picker = popup ? '' : raw(h``); + return h`
${picker}
`; +} +function sidebarOffer(ctx) { + return ctx.mode === 'popup' && typeof ctx.backend.platform.openSidebar === 'function' + ? h`` : ''; +} +function wireFiles(ctx, root, onError) { + for (const input of root.querySelectorAll('input[type=file][data-file]')) { + input.addEventListener('change', async () => { + try { + const text = await readPicked(input); + if (text != null && ctx.isCurrent()) { + const ta = root.querySelector(`textarea[data-role="${input.dataset.file}"]`); + ta.value = text; + ta.dispatchEvent(new Event('input', { bubbles: true })); + } + } catch (err) { onError(err.message); } + input.value = ''; + }); + } + const side = root.querySelector('[data-role="to-sidebar"]'); + if (side) side.addEventListener('click', () => { Promise.resolve(ctx.backend.platform.openSidebar()).catch(() => {}); }); +} +function showError(el, message) { + el.textContent = message || ''; + if (message) el.removeAttribute('hidden'); else el.setAttribute('hidden', ''); +} + +registerScreen('signers', { + render: () => h`

${t('Multisig signers')}

${raw(topbar(t('Multisig signers'), 'settings'))}
`, + async after(ctx, root) { + const group = ctx.backend.signers; + const body = root.querySelector('[data-role="body"]'); + if (!group || typeof group.list !== 'function') { + body.innerHTML = h`
${t('Multisig signing is not available in this app.')}
`; + return; + } + body.innerHTML = h` +
+

${t('Add a signer')}

+
+ ${raw(sidebarOffer(ctx))} +
+ ${raw(fileField(ctx, { role: 'policy', label: t('Policy file') }))} + ${raw(fileField(ctx, { role: 'key', label: t('Signer key file, or its 64-hex seed') }))} + + +
+
+

${t('A signer profile is kept in this wallet\'s encrypted vault. Every signature asks for your password again.')}

`; + const errorEl = body.querySelector('[data-role="error"]'); + const out = body.querySelector('[data-role="preview-out"]'); + const field = (r) => body.querySelector(`[data-role="${r}"]`); + wireFiles(ctx, body, (m) => showError(errorEl, m)); + for (const r of ['label', 'policy', 'key']) field(r).addEventListener('input', () => { out.textContent = ''; }); + + async function paintList() { + const listEl = body.querySelector('[data-role="list"]'); + let rows; + try { rows = await group.list(); } catch (err) { if (ctx.isCurrent()) { listEl.textContent = ''; showError(errorEl, err.message); } return; } + if (!ctx.isCurrent()) return; + if (rows.length === 0) { + listEl.innerHTML = h`
${t('No signer profiles yet')}${t('Add the policy file and your signer key to sign for a private multisig account.')}
`; + return; + } + listEl.innerHTML = h`
    ${raw(rows.map((r) => h`
  • `).join(''))}
`; + listEl.querySelectorAll('li').forEach((li, i) => { + li.querySelector('[data-role="row-label"]').textContent = rows[i].label; + li.querySelector('[data-role="row-fp"]').textContent = rows[i].fingerprint; + }); + } + + field('preview').addEventListener('click', async () => { + showError(errorEl, ''); + try { + const p = await group.preview({ policy: field('policy').value, key: field('key').value }); + if (!ctx.isCurrent()) return; + out.innerHTML = h`
+ ${t('Account fingerprint — compare it with every other signer before you save')} + + ${t('{threshold} of {count} · this key is signer {index}', { threshold: String(p.threshold), count: String(p.signers), index: String(p.index) })} +
+
`; + out.querySelector('[data-role="pv-fp"]').textContent = p.fingerprint; + out.querySelector('[data-role="pv-address"]').textContent = p.address; + out.querySelector('[data-role="save"]').addEventListener('click', async () => { + const pw = out.querySelector('[data-role="add-password"]'); + try { + await group.add({ label: field('label').value, policy: field('policy').value, key: field('key').value, password: pw.value }); + for (const r of ['label', 'policy', 'key']) field(r).value = ''; + out.textContent = ''; + await paintList(); + } catch (err) { showError(errorEl, err.message); } finally { pw.value = ''; } + }); + } catch (err) { showError(errorEl, err.message); } + }); + await paintList(); + }, +}); + +registerScreen('signer', { + render: () => h`

${t('Sign a proposal')}

${raw(topbar(t('Sign a proposal'), 'signers'))}
`, + async after(ctx, root, id) { + const group = ctx.backend.signers; + const body = root.querySelector('[data-role="body"]'); + let profile; + try { profile = (await group.list()).find((p) => p.id === id); } catch (err) { if (ctx.isCurrent()) body.textContent = err.message; return; } + if (!ctx.isCurrent()) return; + if (!profile) { body.textContent = t('No such signer profile.'); return; } + let proposal = null; // this screen's only copy; never stored + let payees = []; // the `addr:amount` entries last sent to check + let rows = [{ addr: '', amount: '' }]; // the intended payments being edited + let lastSummary = null; + const inHand = new Set(); + body.innerHTML = h` +
+ ${t('{threshold} of {count} · signer {index}', { threshold: String(profile.threshold), count: String(profile.signers), index: String(profile.index) })}
+
${raw(sidebarOffer(ctx))}${raw(fileField(ctx, { role: 'proposal', label: t('Proposal (proposal.json)') }))} + +
+
+
+

${t('Check a share')}

+
+

+
+
`; + body.querySelector('[data-role="p-label"]').textContent = profile.label; + body.querySelector('[data-role="p-fp"]').textContent = profile.fingerprint; + const errorEl = body.querySelector('[data-role="error"]'); + const review = body.querySelector('[data-role="review"]'); + wireFiles(ctx, body, (m) => showError(errorEl, m)); + + // The Sign button signs the list as last checked: a later edit to any row disarms it. + let stale = false; + const rowsKey = () => JSON.stringify(rows.map((r) => [r.addr, r.amount])); + let checkedKey = rowsKey(); // the rows as they were when `payees` was built + let landingStale = false; + let confirmed = false; + function applyStale() { + const sign = review.querySelector('[data-role="sign"]'); + const note = review.querySelector('[data-role="stale-note"]'); + if (sign) sign.disabled = stale || !confirmed; + if (note) { if (stale) note.removeAttribute('hidden'); else note.setAttribute('hidden', ''); } + } + function markStale() { stale = true; applyStale(); } + function paintPayments() { + const box = body.querySelector('[data-role="payments"]'); + const paying = lastSummary && Array.isArray(lastSummary.outputs) ? lastSummary.outputs.filter(needsPayee) : null; + if (paying && paying.length === 0) { box.textContent = ''; return; } + // An amount may be left out only on a row whose own address owns a non-RAND paying output. A row + // the core has not yet fingerprinted (or whose address was edited) may be sent address-only so + // the next check can tell; the engine refuses it unless its fingerprint owns such an output. + // A payee that receives RAND too never gets the exception: its RAND amount must be pinned. + const anyNonRand = !!(paying && paying.some(isNonRand)); + const { tokenOwners: owners, randOwners } = payeeAssets(paying || []); + const expected = lastSummary && Array.isArray(lastSummary.expected) ? lastSummary.expected : []; + const knownFor = (row) => { + const a = String(row.addr || '').trim(); + return expected.find((x) => x && typeof x.entry === 'string' && (x.entry.indexOf(':') < 0 ? x.entry : x.entry.slice(0, x.entry.indexOf(':'))).trim() === a && a !== ''); + }; + const mixedFor = (row) => { + const known = knownFor(row); + return !!known && typeof known.fingerprint === 'string' && owners.has(known.fingerprint) && randOwners.has(known.fingerprint); + }; + const optionalFor = (row) => { + const known = knownFor(row); + if (known) return typeof known.fingerprint === 'string' && owners.has(known.fingerprint) && !randOwners.has(known.fingerprint); + return anyNonRand; + }; + const rowOptional = rows.map(optionalFor); + const rowMixed = rows.map(mixedFor); + box.innerHTML = h`
+ ${t('Intended payments')} + ${t('Enter every payment you mean to make: the address and the amount in RAND. A payment to the same address twice is entered once, with the total. An output that matches none of them cannot be signed.')} +
${raw(rows.map((_, i) => h`${raw(rowOptional[i] ? h`${t('Amounts cannot be checked for non-RAND payments: leave the amount empty for those.')}` : '')}${raw(rowMixed[i] ? h`${t('This payee receives both RAND and a token: RAND and token payments to one payee need separate proposals.')}` : '')}
`).join(''))}
+ + +
`; + box.querySelectorAll('input[data-role="payee"]').forEach((n, i) => { n.value = rows[i].addr; n.addEventListener('input', () => { rows[i].addr = n.value; markStale(); }); }); + box.querySelectorAll('input[data-role="payee-amount"]').forEach((n, i) => { n.value = rows[i].amount; n.addEventListener('input', () => { rows[i].amount = n.value; markStale(); }); }); + box.querySelectorAll('[data-role="remove-payment"]').forEach((n, i) => n.addEventListener('click', () => { rows.splice(i, 1); markStale(); paintPayments(); })); + box.querySelector('[data-role="add-payment"]').addEventListener('click', () => { rows.push({ addr: '', amount: '' }); markStale(); paintPayments(); }); + box.querySelector('[data-role="check-payees"]').addEventListener('click', () => { + const errP = box.querySelector('[data-role="payee-error"]'); + try { payees = rows.map((row) => entryOf(row, optionalFor(row))); } catch (err) { showError(errP, err.message); return; } + showError(errP, ''); + checkedKey = rowsKey(); + runCheck(); + }); + } + function paintRefusal(act, r) { + act.innerHTML = h``; + act.querySelector('[data-role="refusal"]').textContent = r.refusal.message; + act.querySelector('[data-role="retry"]').addEventListener('click', runCheck); + } + function paintReview(r) { + const s = r.summary; + // A summary the core did not shape as expected is shown as its refusal and nothing else. + if (!r.signable && (s === null || typeof s !== 'object' || !Array.isArray(s.lines) || !s.lines.every((l) => typeof l === 'string') || !Array.isArray(s.outputs))) { + review.innerHTML = '
'; + paintRefusal(review.querySelector('[data-role="act"]'), r); + return; + } + review.innerHTML = h`
+ ${t('What this proposal does, as checked against your policy')} +
${raw(s.lines.map(() => '
').join(''))}
+ ${t('Fee: {fee} RAND · expires at height {height}', { fee: unitsOf(s.fee), height: String(s.expires_at) })} + ${t('{matched} of {total} payments matched.', { matched: String(s.outputs.filter((o) => needsPayee(o) && isMatched(o)).length), total: String(s.outputs.filter(needsPayee).length) })} +
`; + review.querySelectorAll('[data-role="line"]').forEach((n, i) => { n.textContent = s.lines[i]; }); + const act = review.querySelector('[data-role="act"]'); + if (!r.signable) { + paintRefusal(act, r); + return; + } + stale = landingStale; confirmed = false; + act.innerHTML = h` + ${t('{blocks} blocks left to prove it.', { blocks: String(r.blocksLeft) })} +
+
+ + +
`; + applyStale(); + const confirm = act.querySelector('[data-role="confirm"]'); + const sign = act.querySelector('[data-role="sign"]'); + confirm.addEventListener('input', () => { confirmed = confirmsFingerprint(s.policy_fingerprint, confirm.value); applyStale(); }); + sign.addEventListener('click', async () => { + const pw = act.querySelector('[data-role="password"]'); + const errEl = act.querySelector('[data-role="sign-error"]'); + showError(errEl, ''); + try { + const { share, index } = await group.sign(id, proposal, payees, pw.value); + if (!ctx.isCurrent()) return; + paintShare(act.querySelector('[data-role="share-out"]'), share, index); + } catch (err) { showError(errEl, err.message); } finally { pw.value = ''; } + }); + } + function paintShare(el, share, index) { + const p = ctx.backend.platform; + el.innerHTML = h`
+ + + ${raw(typeof p.saveText === 'function' ? h`` : '')} + ${raw(typeof p.share === 'function' ? h`` : '')}
`; + el.querySelector('[data-role="share"]').value = share; + const note = el.querySelector('[data-role="share-note"]'); + const run = (fn, okText) => async () => { + note.removeAttribute('hidden'); + try { const r = await fn(); note.textContent = okText ? okText(r) : ''; } catch (err) { note.textContent = (err && err.message) || String(err); } + }; + el.querySelector('[data-role="copy"]').addEventListener('click', run(() => p.copy(share))); + el.querySelector('[data-role="save-share"]')?.addEventListener('click', run(() => p.saveText({ name: `share-${index}.txt`, text: `${share}\n` }), (path) => (path ? t('Saved to {path}', { path: String(path) }) : t('Saved.')))); + el.querySelector('[data-role="share-sheet"]')?.addEventListener('click', run(() => p.share({ title: t('Multisig share'), text: share }))); + } + async function runCheck() { + showError(errorEl, ''); + const text = body.querySelector('textarea[data-role="proposal"]').value; + if (text !== proposal) { payees = []; rows = [{ addr: '', amount: '' }]; checkedKey = rowsKey(); lastSummary = null; } // a new proposal: its payments are asked again + proposal = text; + const sentKey = checkedKey; // the rows these payees came from + try { + const r = await group.check(id, proposal, payees); + if (!ctx.isCurrent()) return; + lastSummary = r.summary && typeof r.summary === 'object' ? r.summary : null; + paintPayments(); + landingStale = sentKey !== rowsKey(); // edited since, or while in flight: stays stale + paintReview(r); + } catch (err) { review.textContent = ''; showError(errorEl, err.message); if (ctx.isCurrent()) paintPayments(); } + } + body.querySelector('[data-role="check"]').addEventListener('click', runCheck); + // An edit to the proposal after its check clears the review and disarms Sign, as on iOS and Android. + body.querySelector('textarea[data-role="proposal"]').addEventListener('input', (e) => { + if (proposal !== null && e.target.value !== proposal) { review.textContent = ''; stale = true; confirmed = false; } + }); + body.querySelector('[data-role="verify"]').addEventListener('click', async () => { + const outEl = body.querySelector('[data-role="verify-out"]'); + try { + const { index } = await group.verifyShare(id, proposal ?? '', body.querySelector('[data-role="share-in"]').value); + inHand.add(index); + outEl.textContent = t('Signer {index}\'s share verifies. {have} of {threshold} in hand: signers {list}.', { index: String(index), have: String(inHand.size), threshold: String(profile.threshold), list: [...inHand].sort((a, b) => a - b).join(', ') }); + } catch (err) { outEl.textContent = err.message; } + }); + body.querySelector('[data-role="remove"]').addEventListener('click', async () => { + const pw = body.querySelector('[data-role="remove-password"]'); + try { await group.remove(id, pw.value); ctx.go('#signers'); } catch (err) { showError(errorEl, err.message); } finally { pw.value = ''; } + }); + return () => { proposal = null; }; + }, +}); diff --git a/ui/test/core-errors.test.mjs b/ui/test/core-errors.test.mjs index 0616f0c..798b313 100644 --- a/ui/test/core-errors.test.mjs +++ b/ui/test/core-errors.test.mjs @@ -93,3 +93,29 @@ test('coreApi rejects with the translated message', async (t) => { await assert.rejects(coreApi(core).call('parse_amount', { text: '1.0000000001', decimals: 9 }), { message: 'DE: zu viele Nachkommastellen (höchstens 9)' }); }); + +test('the private-multisig refusals are translated, values kept', async (t) => { + await setLocale('de', { dictionary: { + 'output {slot} does not open as this account\'s: refusing to sign value it cannot see': 'DE: Ausgabe {slot} öffnet nicht', + 'this key is not one of the policy\'s signers': 'DE: kein Unterzeichner', + } }); + t.after(() => setLocale('en')); + assert.equal(translateCoreError('output 2 does not open as this account\'s: refusing to sign value it cannot see'), 'DE: Ausgabe 2 öffnet nicht'); + assert.equal(translateCoreError('this key is not one of the policy\'s signers'), 'DE: kein Unterzeichner'); +}); + +test('coreApi names the five msauth methods', async () => { + const seen = []; + const api = coreApi({ async call(m, p) { seen.push([m, p]); return true; } }); + await api.msauthCheck('P', 'Q', ['rand1a']); + await api.msauthSign('P', 'Q', 'S', ['rand1a:5']); + await api.msauthVerifyShare('P', 'Q', '0:aa'); + await api.msauthSignerImport('K', 'Q'); + await api.msauthSignerImport('K'); + await api.msauthPolicyInfo('Q'); + assert.deepEqual(seen, [ + ['msauth_check', { proposal: 'P', policy: 'Q', expected_recipients: ['rand1a'] }], ['msauth_sign', { proposal: 'P', policy: 'Q', seed: 'S', expected_recipients: ['rand1a:5'] }], + ['msauth_verify_share', { proposal: 'P', policy: 'Q', share: '0:aa' }], ['msauth_signer_import', { input: 'K', policy: 'Q' }], + ['msauth_signer_import', { input: 'K' }], ['msauth_policy_info', { policy: 'Q' }], + ]); +}); diff --git a/ui/test/crypto-bytes.test.mjs b/ui/test/crypto-bytes.test.mjs new file mode 100644 index 0000000..5dd2b05 --- /dev/null +++ b/ui/test/crypto-bytes.test.mjs @@ -0,0 +1,11 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { encryptBytes, decryptBytes, encryptSecret, decryptSecret } from '../engine/crypto.js'; + +test('a byte record opens to the same bytes, and only with its password', async () => { + const seed = Uint8Array.from({ length: 32 }, (_, i) => i); + const rec = await encryptBytes('pw-long-enough', seed); + assert.deepEqual(await decryptBytes('pw-long-enough', rec), seed); + await assert.rejects(decryptBytes('another-password', rec), /wrong password/); + assert.equal(await decryptSecret('pw-long-enough', await encryptSecret('pw-long-enough', 'text')), 'text', 'string records unchanged'); +}); diff --git a/ui/test/fake-backend.mjs b/ui/test/fake-backend.mjs index 78bc1f2..2763f3f 100644 --- a/ui/test/fake-backend.mjs +++ b/ui/test/fake-backend.mjs @@ -4,6 +4,24 @@ const MIN_PASSWORD_LEN = 10; +export const FAKE_SUMMARY = Object.freeze({ + outputs: [ + { owner_fingerprint: 'cb5c9747f9a2ef35', to_account: false, amount: '0', asset: 0, memo: '', matches: null, needs_payee: false }, + { owner_fingerprint: '2c94b28813d4ce65', to_account: false, amount: '0', asset: 0, memo: '', matches: null, needs_payee: false }, + { owner_fingerprint: '35bad5bd21a6f0f7', to_account: false, amount: '10000000000', asset: 0, memo: '', matches: 'ABCD-EFGH-JKMN-PQRS', needs_payee: true }, + { owner_fingerprint: '4333dd507bcc4286', to_account: true, amount: '14999000000', asset: 0, memo: '', matches: null, needs_payee: false }, + ], recipients_checked: true, fee: '1000000', burns: { burn_a: '0', burn_asset: 0, burn_r: '0' }, expires_at: 2024, + signing_message: '00', policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', chain_id: 21, genesis: '00'.repeat(32), binding_domain: 1, + lines: [ + '0: → 0 RAND to cb5c9747f9a2ef35 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '1: → 0 RAND to 2c94b28813d4ce65 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '2: → 10 RAND to 35bad5bd21a6f0f7 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + '3: → 14.999 RAND to this account 4333dd507bcc4286 memo "" (owner key verified; delivery key NOT verifiable from this proposal)', + 'fee 0.001 RAND', 'burn 0 of asset 0, 0 RAND', + ], +}); + + /** * `wallet-core`'s `NO_SPENDABLE_RAND`, as the real engine surfaces it. A token transfer pays its * fee in RAND out of the other half of the same bundle, so a wallet holding only the token cannot @@ -120,6 +138,9 @@ function createBackend(initial = {}, overrides = {}) { // that carries no memo. envelopeBytes: 1860, contacts: new Map(), + // Private multisig signer profiles: `[{id, label, index}]`, and what `signers.check` answers. + signers: [], + signerCheck: { summary: FAKE_SUMMARY, signable: true, head: 100, blocksLeft: 1924 }, // Delegated proving: the remote proof still in flight (`send.pending`), `{job, name, kind, // startedAt}` or null, and what `prover.probe` answers for a paired prover. pendingProof: null, @@ -434,6 +455,24 @@ function createBackend(initial = {}, overrides = {}) { } return { key: m[1], url: String(q.url).replace(/\/+$/, ''), host: url.host, own: q.own === '1', fingerprint: fakeFingerprint(`prover:${m[1]}`) }; }; + const signersDefs = { + list: () => state.signers.map(({ id, label, index }) => ({ id, label, index, fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(40) })), + preview: () => ({ fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(40), policyHash: '33'.repeat(32), index: 1, publicKey: 'bb' }), + add: ({ label, password }) => { + if (password !== state.wallet.password) throw new Error('wrong password'); + state.signers.push({ id: `s${state.signers.length + 1}`, label, index: 1 }); + return signersDefs.list(); + }, + remove: (id) => { state.signers = state.signers.filter((s) => s.id !== id); return signersDefs.list(); }, + check: () => state.signerCheck, + sign: (id, proposal, expected, password) => { + if (password !== state.wallet.password) throw new Error('wrong password'); + state.signCalls = (state.signCalls || 0) + 1; + return { share: '1:' + 'ab'.repeat(8), index: 1 }; + }, + verifyShare: (id, proposal, share) => ({ index: Number(String(share).split(':')[0]) }), + }; + const proverDefs = { preview: (link) => { const r = readProverLink(link); @@ -502,6 +541,7 @@ function createBackend(initial = {}, overrides = {}) { address: buildGroup('address', addressDefs, overrides.address, calls), contacts: buildGroup('contacts', contactsDefs, overrides.contacts, calls), prover: buildGroup('prover', proverDefs, overrides.prover, calls), + signers: buildGroup('signers', signersDefs, overrides.signers, calls), rpc: buildGroup('rpc', rpcDefs, overrides.rpc, calls), settings: buildGroup('settings', settingsDefs, overrides.settings, calls), platform: buildGroup('platform', platformDefs, overrides.platform, calls), diff --git a/ui/test/fixtures/msauth.sha256 b/ui/test/fixtures/msauth.sha256 new file mode 100644 index 0000000..4aacde7 --- /dev/null +++ b/ui/test/fixtures/msauth.sha256 @@ -0,0 +1,11 @@ +f4575f72a6890ede4b9972961f83d4bbe48db3f0905b97a73ff7efd3dc7fb783 expected.json +31fbeb3212e024e5839e29f9bdf23c0bbd6e34eb2ffcefb5c9085b3196992ff2 policy.json +2144cafbbd06c1cd165105754a8ee1043167ed68b2256e150c991f82b055b8f7 proposal-expired.json +457f0ab49d28ba0c18e3be1e266ed09fa94f1bd7f3105155b88c8dc95fc786c2 proposal-fee-raised.json +773be56449eefaba00f819a92aa48e6d8be8057f96e39e5a8da45a01b2491453 proposal-substituted-recipient.json +c621fc9531f95a04be2aa83d217d0040b19b27254d7bce483dbf6243a558b81b proposal-tampered-output.json +6f2135c940a38af710e137d2da0ff42147bd6d9a2bf18bd3fc6973bef044b3bd proposal-unbound.json +50f1f1cc5df58bfc1126bc9279a96fc1a47c0d0a192fa6239988d8f4d828129b proposal-valid.json +dddb4624c6f290948be9769735b13a445d7e4d201c85bf8e044438d808f91016 proposal-value-mismatch.json +7ce496a463c09dec560070875c12e10781b14a31f944bfb1905c07e6cd751832 proposal-wrong-expected.json +f75a09d8ec62da397010202e053d192a429710ecb566faf29d8d1381f07ee8aa signer-seeds.json diff --git a/ui/test/fixtures/msauth/expected.json b/ui/test/fixtures/msauth/expected.json new file mode 100644 index 0000000..e837f7b --- /dev/null +++ b/ui/test/fixtures/msauth/expected.json @@ -0,0 +1,123 @@ +{ + "lines": [ + "chain 7 · genesis 0000000000000000000000000000000000000000000000000000000000000000", + "WARNING: no expected recipient given: the payee was NOT checked (the coordinator could be paying itself; pass the payee's address to check)", + "0: → 0 RAND to cb5c9747f9a2ef35 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "1: → 0 RAND to 2c94b28813d4ce65 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "2: → 10 RAND to 35bad5bd21a6f0f7 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "3: → 14.999 RAND to this account 4333dd507bcc4286 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "fee 0.001 RAND", + "burn 0 of asset 0, 0 RAND" + ], + "lines_expecting_recipient": [ + "chain 7 · genesis 0000000000000000000000000000000000000000000000000000000000000000", + "0: → 0 RAND to cb5c9747f9a2ef35 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "1: → 0 RAND to 2c94b28813d4ce65 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "2: → 10 RAND to 35bad5bd21a6f0f7 matches A5SR-HRMB-SY4M-5ZYF (10 RAND) memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "3: → 14.999 RAND to this account 4333dd507bcc4286 memo \"\" (owner key verified; delivery key NOT verifiable from this proposal)", + "fee 0.001 RAND", + "burn 0 of asset 0, 0 RAND" + ], + "recipient": "rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "recipient_owner_fingerprint": "35bad5bd21a6f0f7", + "share_0": "0:28afe78bfd0038374fcdac2f80fac8b0a1296a7f42b88f65791fff4ca31d077bc744a6d754367c94e891afee2481a802f8cef49da46ce435bfe161d302916e4ed4717ca811e78c53ff15d94b4c1bbe90138fc63dc517ea37e473a7f4d1dbbf748cfb6ec2ef38af68a254c1ca4314a2786d6dfa067ba7454e76b068790b1f8df324f9d10d0781e6ffbe32dc2a27c87e5bcddf9b5cd03fb1c8c7aadf28f0a3c37562aa13de1e3814af0d0db428e9720b9bec2ec9c54088793244ae43123b215b3134a507d6600fb6653100c8a51f3e4cdbc34a1dd6e611f334c677c942bbb8e8b444462ae63fc70082fadf17942bbe07646c7db86839eff00d0a5eaa9111933cad08a73e3cfe65f89ff679c4c95e1d5d69a6d3d53d629fa7be0343b8c74ef30303e6ece35716f00f602bab803fddd1401c06e67f959e32d341567dc0ee157d28726cffccb58050dff31c69f496709f337cd2f4cd9aed29e3c910829363ad05f402194db457820fd31eb654807c35bc1499ef344c72fb96e9936b52f66863ba634e13c98f145d3234e543f0d54cd1f683edd56a77087d0e265f81a6b519687183d092dc0591ac45ea0b73bb27be856f6d3d2c5981d0937fd02a714ee363a2fa65df37f53286c5d9561e24a8b913c23e58da8dda9df341a34a2e3ef065434c89419f32f758c061730a0ba5cad649c97885227486e042fb0ef6a112ac35a3ff3398d2c57c8299488a04021306b37e409b9dc69a5820b2dfcd7e4956c42c57cee95bd3bd76a0b3b6bdcc49722258f3598bcdeeae0618a88327b6d0de3135b8966dc9a69b2bdc5250cbc5d12bcd14642e3c2c2eddf47f67bf2f216fac0d6facdbf59e79d6826d7b4f7bc5304f26a0f0e5d41ad6b8f85b3379934d20b8e102f22d87ed145a912f0bc72ceb40156e1609d94b238fff7a23bd69e191e2421fc7c78ec39201ef8fb710969626789b5534f8ffd873c8c3a9ac69334458d2655bc64793a50c6f5c300a2cee2d780cb36d4e30bf3b7970c7b3b7fc868c1ed4618484ca3d5865d84d41c5f14afebbac533cbc56fda4165b4c5a8cdc55ef1f28c7c1bb96bb93377a6e3e4703c4ab733da086e432b823e69ca516f5144f5f518fff66ef9c0589ea6dbd6b591eb8a97130b9f46f6d2cdf6e200be07932c62c496ba67b15708d6063d7b62908230a45e9bad6c6439dad84f6e309bdf2eee517b9ae3b7ebff526c06097d59f6b3312f84ce2aa82d0a82d0b08c8f3b7827a333c33ccc74b1ef5d9f07df78c8272e9c95f9639c72855cfdeb59a89fd9603cefa11a742c04bdc591f141f338d766e33aaecfd5b5e6299dcb75de803d45dcda48cea10f5aeddabffa92cb259d3123051f665e33dbbdb43c7ca366c1cc672e7254028262542deb53f7e12514880e723b4deb8594a3a1e1a2445920626bead91a12e2674bc78a3c954981118c95078d0975cdd7db1da00e0872f05b4a61fe234f2a9f3b13b85653e820da13c0c6fc90cd5b81429d35ff8af32b9a2f293357020e851112b0b1d1601aa8ec279f92566bd86d0eeb16f51960580a6ce17c81aa6017571db64cd8dde0fc948724af73ec3a01a4ac89ae061e109e2783c0d7faedaddb03293bbb58dc95a830114f59f2358a8287dd649651c52c28bdf26b0f8a6b64d812a8463e5fd7e4a67fcb7730a1c7706aa473a9f21f631689a40afb2e635ab3850aad872825202bd8d260155edde60bce8d802a715214eed25d161143d56a96152cef775f82de36a54e24410ab1af31cadc8e0f6b3d74fb0c81837839a90ed175345cf945031fef8af50373295986808c06682e1150189c925027ac5ec96018aaa47258ee7108269d4041cd43573c4447f8c0c435b8e1e41b567623ec33cba519eaf14d04c45854dac1540a3c0c046d2498f32a8dd7fb570f983fef65767fdb51b36973b0afbadf9b959c6b3e99588bc5dc5bcb5161ebad10ab859bedb008104ca4c788f999b9ec5f542dd10608264466342fb11a616f8adbce460a01994af3a6cd829716ed444392fa741d2be0e67b725599be27f6314001382545e546080e15b471c56d5ae3ed6a125dc9ee5e3f04302b57b51d3afb7c746c7647eeb9832f065505ca0f39102fded02612bb105b827787db6bc6a8ecf8e54ac39505427e19c3d94e88a2cc90fe36b48c0e0d57e379dccfdbd92346fd04a1e4a9d4d3a38c5f231f70374b764a1967f7281b357749d3fec7eb5338b715c412410863141cda64005704fce7de43a82bdc213b234ffc14e844ccd2e2cefc4f8b7db7237c98bdf3ba3fc0d56f8e4f3f8e831edd4ed82831d42ab2ef0df5b37b6e4dcab4bb3afb54564c5c5deccd3ab04aa6af6d6b3dd40fa2531049a10bb5eefc431b1710f48fc570ece05b314449920983371c15df81a99bf44e4a547a2cf68ad0b4b96944b73727bede9af9eb04f26dd13f4fb92c14171fd4cb06ce68e28b6aebb85a32dba67f273287d03fd49a47b85624d28b6c86d003c4cb9e1b74ab9e0cb6907539ad57c6840c70c72e384dc82c3ffae1095616d580c4ccf7c9267f899b3a2ee456114bacfc38bec59fb5db2e8525e384dfd5b1f795a679497f784a22fa5cbc8d1d54017219869b7166df16f35419e6cef5ff0e9beff80c346b87bbce57b82178cefacca841bd1496971bbf8981804f84b815842ff2dc87b8664f7170fa4ef32fb6b6f914b69b48b4acf8bdefeb38111fc4e309ca5e942ccfa000a98356d4894d6b953492db13e65593307afea34edb1f3e477d506835776928da9cd7a85213a4ee9cfa19dcabe7ed04432dd3f8ef722cd32b51252b2f5e2d17303809aaa411d688ef4d602c79ed223ee2121511cc5ef7960cc2a72bb3c86197ae79b4cb5e3a34e27edcf314d7dc5d4bf3b2742a10b9b457d13c16d64c392ee9e88c7ce3178edeadaeb7ce2d455fc32af439543d668a679dfc789b42064514009c1df6bfe23a57159679355e577f0d21991e4c9ae0b5e5ccee032e489b74375f073df10242c1a9b0833ad2525e8b6c1b5c4e47badf877aa77a9630971b5aa434323d22169c16179a31ed169a5e37518d8fbc8c96f2c0815cbb4f4308ea220b54bef0161491c4cb643510d2ffa26da323a56c65b07c8c158a1248193b556a31767c11811c112a8ca2156cac446a525fa982f28d073d962cb951523c8074af2b3634eb45c5d3f540f31e448f970d9d8675464b74b45d9c947083c88237aef8e17c0125fd04bc390b79bb67dcb7d8c1caada7cba3ca3ee66f0bcd478ef4affb6d402a62d0fcaf3897197e7896d2702f2802583532e9abb321d9d00041964707a7e8392a5f3f4f5031522303d3e53626a717f818da1a5b8bcc0212a30363d4b4d536a7f8a91a9c4c5dae0f6f8f912333c3e7e8791a6afc3d1d3e9000000000000000000000000000000000d1f3340", + "share_2": "2:8e18d763ebb8cf6bc43952a8a0460d45ee556534641cc25a16b1b07bf02d014b926f34a92cd0ccf926367fd08ca9158de83e2d39aba85581c7d755a735c455bec4a093a9c537407b85f6eca662821a679d8665731456a1c493114785c71e9b76cea6781f32a32b8357d1185f4bb586c9944ed3bc0cafff890f3daedb4748f551fbee6a3a13796160a6f1e6a24578a02374b3ddfc1dd331fb9cb3f5728c3cb95dfd2f7699704ab9eb1d17ce5ab3f0dc8cf6a45a5e613be88737e1b3a72072b3980888bbc7747a8ed2ba7b26547436113ab556d97a2e0419b758c15307062d4d9b22ab83104be1f8ce6ca5515bac318886e429a3b96fca67e04796919b3d43d22b7b6cc51cf6f685a67afd982af81a5f6bc43eb91dd6f6675b74302c50fc3cd348595c3c7904926cf54a33684d30e0142861b726303871d32f8d39da692e94e4a7e02a87c12403818adf9bb7dc276f2a3128d13b387f9e408d165edefefa864cb0b8cec9198d2f8eaf9a243f88305c84be4bc07b892f1b02471005459be585da22eba5bdbb56b3f9588180478aac2c67a0d9d4a7a243be3f23bdd9a6b4b94876670bd868725eb818ce42cd1dac48797fc844355924d2bd7560cc1ef2e1dc9536c53343f2c8faec4769b663fa8af695b2d59badffe27fee82446e5065047754c82b1829c94c3845c28d3af751d78fc4677f6a1731d941a736cda60a881985b4a223979e1c4faecb210b25319d423f478a5f42298ff066442c0f1e091c82351c67d7d5b4a606273117f8e527d0a9ab59a0f647bf7a80011fbd6843d28e42262da82e7043222c699dd91b60b203b8312780166ab167c482f866f979befef6023e5861958521fcafec8b9011bb3c21e3628fd9b34a8833e3f68d9f706b4f5824dd97a3ffe4d15e0a586d1d4220d8b9a453bb531a947f08f28a3f682ea4709387e26927341a5cd821e9c6c4d872a1bda7455509cd333a41558f68ab6ec8c9b46b1202199569180c7fc72abf769e2137023ba8219be70cb18a5cd3f337f8e58a5f8a28a3b794e9301f94ce2cb307fc0fd87ef40ab382f21bd22a411315252f17eece2c0d936d376572a4d014476c816e4e8b4a24e7bd01a0332089a6b5aac19821355d1364be7e8f79b9fb3d7c013e24833e1b1aca8debd865720201e1c8f0647b6f7b422347f981710a687cc07842429dd83e936bfca94db41f80b114b8fe0723481362f065519fffdfcba2ae0bc11a1bc7fd7c3503ec3bd59483bcc943555149ea3ccfb83945670e483f284f319185ca0e30e7f77f79371ee3441a9f59f6793444041c84428815fac1daac3d320da89e926ef522cc4f94657985729cb0ac89f3a65d3c6c485fc567b9c0e7f79265468d442fadeee6d819c2c2550527163b26169e414cc64ba4c23ab4594c961f65be444e613321f1cc68f9161712b4e0fef0f313e0b6c14bc8b185d20d1cae97719410ff98241779adbe86ff365f9fae14805888d005ab9efb8600aef04011471db2ed9f4d3eec5b7ebb325f9116ece7f838b119e0809c3fc6da8b1fd339e7f53e0c506627f1c00fb426db5896caa66adea2ed3701f8532c87f286d3dfcfbaa4f6ba8852993b4a084622ad6d32ca08c87f7de60ba612e55bdd23112ff539b9e60342167877bf777a40c5024114fdbb33a0469561e77d4a09be57b63708cee385b7c5c94545eacc5cf72940082c549ece1a69e4fa84446778c33fa7a34be0bcc74c9fc2321fdb49dac4ee6bdb420c1e84158a6e73981f2c931d05e932c15fe9e727bcd5d22897983ea5f686bf80f46236b0c2c494033aaa55c5d1101b2def16a01d3c11270957765c32076f2d70743a6d3e29082d75a97ed3de7c1c7c79527a7716aab6a35e0fdef51e4c605c72b1ec168d6f1093aa851948aae2837e06cd039040fc6390fb3d2b982e02e0a03d6cf113ca0ef9127e0184a49d37fc7419674134dfb7723f8253ebbd21264dc44c5f40841f9b629467f0a2500ab888d369ca35b22bf0d5eec71b53d1d62ab49ff2f19893aea262212934cea3cb40a08c1689512b0f641c016f04d81524846f5083e34a3bad9738bbb406169c6f462b3e070f0f830eba4ce5025db95f3fb4dabe32c0b279b3615128be55f13b07d3e93a3fedd4201f666ea15e1fe7579e33f611996e3d9231c7cfc92fefadd7edb1fbacba692669925c04303cccc05e2c36efbf8b39359c839fc9e9c917d46ad68ff5ae6cdfeebb018676beab679ad2285d5eb1ed7eb44cf420b49a461427b69e15b42e492b7420b9fa0a1253f1c708ac56809753de24cf4d7c9918f62ed1b856084c9ffad67f5fa06a9c6a41ea17aaaa067880e1ff225712f3786a3f8102c5aa53797bf30b8d5bdfbd61eacf667fb4692c7d76ff4f82d4fb83055972a996b920bd907d042de71511125b6f7d6f71ed2e197503c0960daa99f9c48237e28afa062692de29db69598fdae4801bfdb55413032e7838f2165945706a6fc3cabaccdfc83d0beb996cfdfe5198298c184c17f6615f2bd7d3f54c762b94052baca637f5a76c55d839d611249b590d5ce04c0a046eeb61d0c81b143719f9decfaea0c5b24d1499df66bb8ee38e7b26fe874c54bb9d26c4b1f34534c994ac30a5d2c8f35335c8e4cce71e3d0807cf211df3b7aaa63d3b8712bc8f81d52c0a21a5b8e9c22229a2e2da4d5bcd6e70ab879dd9f5280f163a36278911848a3617a85c95006d769d03fc3ec20cd99c75efdf3a9c19187b9abeb055ab6b8574979d2bc9aa5d5186762bfb1e2d1e0f45af5cb1ed6912c2ecab03f1e02b8c45aeb1a3859b89c0caecb0bfca37b24ec0867a269b6a5f50d553c5c937b5783f47bb577edbaccee0d2de640d46304760ceb8a06b0f98b8c3063ab56e7aef1ad78b5b3a880182cb847009deeebc5eabcd934475e90ed17e881146291fa846a71232d4dc56a36811508cc42bd2928f1b972d91d359feb0e3ac17bfc120c28db0ac7429b8a923012812c6d446946f73d46f4662a0922322a9fc9a52ca9850c108641a31dcc9722d7d2bff8b57b978943d82445850a25b95a4b814a76d705b1a1fa7afcce504fed0ad2e9023f835fcf27e10197dea90ee4a34fabf05ee9e75ded54293e514faa919294a2a8af6b92a211ae0fa3a774af1322dbc57f5bd5e80afe2280bf25ee943b20518926dfd665fb19e985f894fb9f3bc9320cf335fd42f8554ec0e26771439049caf8c6c697647657b02138f38d17db44aa13b5585a2b6ccf09558b1ae65cd37fb864b70a82ae246751536568960a025965be1ad927c17dbf6da330c3cbacc952b0c1053606e8182858b8cb4b8c9d3ddfd040c1c262f7f8194a7b0b1b3b4bfc4d1e2ef0106343f596a8389c0c2c7e0e721293f406c76989a9ba8b4b7dae90000000000000000000000000000000000000010222f3d", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "substituted_refusal": "output 2 pays b9856a063c1fb742, not an expected recipient", + "summary": { + "burns": { + "burn_a": 0, + "burn_asset": 0, + "burn_r": 0 + }, + "chain_id": 7, + "expires_at": 2024, + "fee": 1000000, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "outputs": [ + { + "amount": 0, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "cb5c9747f9a2ef35", + "to_account": false + }, + { + "amount": 0, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "2c94b28813d4ce65", + "to_account": false + }, + { + "amount": 10000000000, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "35bad5bd21a6f0f7", + "to_account": false + }, + { + "amount": 14999000000, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "4333dd507bcc4286", + "to_account": true + } + ], + "policy_fingerprint": "NFYW-K8E9-E5K9-AAK3", + "recipients_checked": false, + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5" + }, + "summary_expecting_recipient": { + "burns": { + "burn_a": 0, + "burn_asset": 0, + "burn_r": 0 + }, + "chain_id": 7, + "expires_at": 2024, + "fee": 1000000, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "outputs": [ + { + "amount": 0, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "cb5c9747f9a2ef35", + "to_account": false + }, + { + "amount": 0, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "2c94b28813d4ce65", + "to_account": false + }, + { + "amount": 10000000000, + "asset": 0, + "matches": "A5SR-HRMB-SY4M-5ZYF (10 RAND)", + "memo": "", + "owner_fingerprint": "35bad5bd21a6f0f7", + "to_account": false + }, + { + "amount": 14999000000, + "asset": 0, + "matches": null, + "memo": "", + "owner_fingerprint": "4333dd507bcc4286", + "to_account": true + } + ], + "policy_fingerprint": "NFYW-K8E9-E5K9-AAK3", + "recipients_checked": true, + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5" + } +} diff --git a/ui/test/fixtures/msauth/policy.json b/ui/test/fixtures/msauth/policy.json new file mode 100644 index 0000000..2c394fc --- /dev/null +++ b/ui/test/fixtures/msauth/policy.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "threshold": 2, + "signers": [ + "ab9df2d45591fea9ba01b2b6a8186fa00b3ee663f6a5ee36b97a304c67214123ff48c952b7984eb50d0274f1a5feb7f6cd48b6af199ee0c7f40aaa3a3a28e52899550e0ff125ca945e86082b9b2a71a252bab3ddaf2091295f0968f67615e8821d94213e29e8e7e68ac5fc44ba91d3c98117edee27bc9c3948b634c313ecb9c25e5ca1ed249140730b0411bf6e94ca7e179b457e43ff94de1d716a1ed28c29e1fb4dcbe8ac6f6fe33b3a751f6b1dd69e684292827aebfe7dc27ae34a04efc4589be3c6d69bef378e21914e8de7aa0b842feec6527fc55e0f8eb2286573b5afbd3fd879f8414eb56fcc4c613b05451f159ac2c63158ee34d4745cc83ba852e83a74669387921b5d3f58c21e7adb30d6eb9a241466309db8ea700fc846939513b427dc81616dd4c7ac1decfd495c68267c12ec732b35376797acce90543f53a33b7a3e54d6e5ae38d1658427e6b2516ab00242a2cf943bde72f13d869a8094f1e6dd99ed35383a0060aec854d1f4ae5d1371bdf93ed0a353fdcef934a343f7e5fff8cfc317f98fe3d509051f571905b3829aa341bfa31d672ed210ac031e7e19be7db3a93c4c44dfba937e5c7c547a611a0e4c8a3eac1ce18df431aa67e1e37c40ca23faa446a7aa28503267fc4ab35d232d5a4b9eec7ec08b6e8f80c39014a91a362235355bf529cf8152767c553368f6ca80404eeb8c6474e2c5ebada6818b125a36e5b7bd198439fb1cad9d1e347612423896a156f25924bfbb92d00aa13f39849e8738dfbb3a7ab30bd61d6539636b334c7a78de754e93d2c89aecf3d537c70799b79986e763063fcc6ebe82edd62f1758c6d5612aa604b75f90277e4121a6dc5ed957be61d721cee018f4195765081fdb54a1c705948da9eb4fc9e8779f768b0e1f2fe0e420b4b7901948e2536195f93437d05ccbb98944180141a9b9e7af359af1bda8992e8be06ef4bce05219069ff4c56d8f6f0a27376b9f4901d184a2eb9625f9e32a7d6bb21946c03874d4cac08eadc29de8e259afa3587944bc63ef895883d9dde055b838554fa67d13cc455c28c78a49ea64ff628dae2a0efccdb49d360153145d9f8a071001fa7c873496c0972a8d5a8aeb3ae6311fc528a7f5748bd18c525ce3e29fec6a7569a8403bf3da011fb2cfa6276f7e1089dea6be94468e347afe993208723103e364f9e0352b9ca14af7770f304d28c5b13eb999b2daf37e2c01e56b32043edec621b906e418de4071c1d7047282aa9e464a9971047cf3132223e3ac533433c59a0dce7cc2c2b736a9470e1c26f3b93cc0fae23faf3e72c432023c3a9d08bf13255057c02c8e6582b30f2aca88309ce0b47a764e88f4972e890b9a4e6a0e55ddd8a8121bec2b27ccad5641df00510d538e3721587323585b91159f02375b40bac115be5661a9cb32b86356f4614edb590ee67d20c06fbbe7d4925368f30222d2116f4e5010e34f33556b1c1f89431c011315d3e6f176eed93d10db3e1dbc569d9008ffd43231f3e57318ce938b1dd0381f64062869a5fcd86b2d538475a7c30930ba1bec664bf4c9696ec4a51f7625d9fbf6a618d73ce07f8205ee00b6da72e25ebcd7462a29ea8db08df2da21e78a80b54363fdfe167f5e9cf458181b84b4eed3bd4b82822834f21c1898a585820076db2147aea89924e62ea522ebf0eb1a4c5b3f983a5ce6120b3d463d319f82fc3a65412f6e2a1b810287a358bda84968d60a98fb9db950d5cffbd975c07622a81e43a1e43a8773877700723af82f413d51d975debf9a2f8a534504befc54694362d4e6e245af8c717ddbcdde38322735b07771337430adf33e17541fe0bb3a47e8e59d7b8cfda0", + "663cd73423dfa2d2e45f091b672aa3b205b8a63f28b72463dab6efad3d1574f4c455ba12d9ad55636c6560deb4db21e88b4e4dc81ebf6cceadce84b45bdeb0e06a4a0cdea760541e62355dcfe07353db66ceef4d1a3004ee4166ab81984ee0c9c8bcc8f25c3912ac260389eae8ad6d7cacf192a100f973d8d1a140306bfb2cbf546dd72643c871a12c032a2f360444ac2ec728a73518fa4d6b151969cee117627137d78cc7402ef8344c8dfa3ed7622c26ca9e1b64243682adb07a1ee30db456606c6bd073c595e1bb4b62d0b238326141b26e7c422bbfcf429ac5a4cb586454f5f20e0a08badd92c07576566e1faab05afb0d39e57ccdfc612098e93dac057feda2495927ae65b77fbfbafa58a71ce88a12b45eeecad04557869bff3c91acef608ab00fe95b9702f5a3535d2daedfb50784e2edc7add45e509cb80b28c8fac0486831402ee6699ec070962c08a7ae929121217ca781b931f10fdbff852f01942b84274277ea23f0acf1d86e406d5f214d3dac39fb1e3e620e16a155b5a4b47b98fa5ca8373164253f61f8faeb572e9e146b100db0d2408282542f8c018205a41fbf5f06180e80af4a7b8642dfe3435a904bf18aa175b13653e08161633ee317ea3f9bcc19d1dbb9701c1c6fbdac8e0d6e1e75df6549c9dc06b5eee53a5799a9242bd2d8e36e6785dcebd71ff6485432081aaced31e2b4d2c9f554d616ec55475e3b09c895814ddcf0113475758503d4958b2e462a3a4993d08af95e07f601755ff78e54de33c8a47c353e343b9c717ab95cbc08751c2be91755b7ba8c0863072b0ee604e44de9da230961d4375dac747781dcd8d5297ae51630702caad0efdf0c5fe6520b06bd77113c140077370d92727e26b04ed2fef3c0c51f4c536a82448783dbc3b9bac17afae05a7f29a9303b187f32473a97ab71f3b365224e871c123682f4309f310505a83b319d87c0098f6c95d6448d30d5d8891175147ea14bc895fe5625e35f490b71d0c223e85e40c1a2bddfb5b7b5acb07ee0086a1833efb87c352033e51d4d9b3436002e1d260dc5b874433fc473f63c58f28fa4f1962462127f19b34e7922301c47e8cdcb29221255215f9e43a8a9b3afad1fa316e96a05d70ff1de62ea06d95675b67afba2045f6c1b42f8f17a3834d6ea04f5460b989c22d8189fa08e0ef38fbb461605c1dc3698d200c0badd01e020eeedfd85321c64e2f01f863a12d5313f94eadd45916fef5d396e4ddac48805d25901d5056abf8ae04fa0c8e5d994ba54705127a0b6da203d00111f209cb14ad695b4f77e693f1668ffe56d1accd043ce213faa4bd69c6d31ef1a3b24a3ed2837d0aeb0097f3a23381e310dff1110e75dc526f4025a68e31cd05811ecb9067b26695b9f145a7e2621482d0e15c2447b8d3955c7e87c2e78cd46a949a9ecbc9fc7621c8154c0b550c9047224f701f2037266501f70c41e3f122d41e03d18338184051a0e3ffd80d22c49121846aa00a6f61569467eda8a52e28b13109e15e23c306ae086dd1147db16cbffebd295c4d9a59fcc90185ef1c65db0fe916424ef9998762d883dd3af34f23790555325e2e39714a2beef52fac0eb99b535e0f34cfd19537e83d19543b0742b70a22799cf941c9bf6e6e4d7ca8546d280bec8c3ea01ba89026ef189d27a06eca3eb8ec97d8ea191f4ed7541d89330361894208ccf034629bc73d7f2601cfb1503490cc31fcb4dad1ab9b32b44130dc11700922cae6f5083db8eade79579aaec4342f40aba312ff058bfc14dc69b3243cc9923c6dd8a71a08ede08b9332e02e3bdec0afa1dfcf229461f986a5bbc1e588d311c0b98a35c9d1e9372d4f5df", + "362813e8868c130b36cf07c4d2a088a76c2d90a5cb3ad062dc3afee7d2341a8fbd1b5674b43dbbd0b3bce87fd33def905a495c88b0f366f491a0477570efc9f6b8f7d84541dd0495004550ca4e3742e65b706b7e71c3584e37bced0fd4b1bbc8b1b4550c64c7740e84fceac527d08fec6ccb23bf090e4f49f4d773f181614eefd9aa3f9ac9190b9ec78af2598676bf70dbb9d0f5f0ba09543d7b272145b7dfd9ce6fcd8f2cf35438f53b9099769046f81446e3abe9916eda712697578b22cc167dc21624eefa2972f72167d3544ae829bd1417cfc7127a1aed12344a8d21a9a30eb09003374dfa4e3803f12a65872c821f45e1cc625e7bd7b683ce6f92e33353184fdd8d16aaff477e632adcca9a87d0ffe2fdc7fb6b22f55f6b07fba8c12a70572ba81c08640e14b26c2c9fba72eb40abf45c783b98444e490d1735f734611d6e988996dc17c441018d8221fbd8d542ee2be8eb8adbaa8475659cc814fafad2898cd6a435943add0367cd21bcc722a8a3a943683b60cafc13aafb82b1b831fd004de9079326b91193742b73387cedd71c43161c14084882f2762331c2a47dec0838d8705f1c28d5406a71be2370ef5ad3e8aba9e6a7b15522f092d80b18093762093dd71e092de7f7d5ff80a701dfa115a70d76021201ddb01ea63c40a00115d23511e4b268267ae12c5f87f0776f1bd9c3235718edded6ec8dce7fb016b48cbfc7c2340468d715ecddad71df8332cd46e6635f099a84a15d5dbcda8b07751583844ee7423ddb2af911b911c57aaed09dab22a09a71ac5d804396eed1ce870603dd017762d0309a1672e7a62eff6c3779744535d0d512373027a055887b7fa74e5d1d1b8640b98a97671a8f44e602a8780c2148c5ea2ad6e56fda55369f35cc8a189784798e40c2dcf02d502cb2084902815ee1866d98f30877f29b54a50c1b0dc7c9ed0b319cf30045bc96d196546983ded16b637da43b92ad0644163a1d84fd6f30611d85aedf6f0f9e6270dc8c285fbdb5be13d04e806dd33f897f9285b35a67bf8c8e3c8180e1621a6aa52ec35df337a74c5bd3b004a63c5d3509472afd1c9e4d41a8ffd3dfec3ff8d728e2d55d0be7577832a2e75759e5d24cdd6dba7375ce2ce9d4a15e65e85c4ad8e5df3cfdb8a06635bbe01b38bb7e46f702a617563dd7211dba9f97b8863362ef08a7466456b564da335b9b7b707223cb92e4a9584c1279aab4b698eee9e1304672cea889267ccf9a41e53918a320723238a8cfbdc88426f83426ea2044f40db3fe3b83c07c66e1d2fb80ad93408ae6c259ac68abdb4ea02088c919339ccd301f05083dfabc3cbb5b4a03caf172802d81d91617f4e59f28849172dc69e00b833e2e6313b6569492096f115bda91f29445b61c257840ba9e20974b27d2573983f618582b38949f18bb4846fbb78d96611bf6dc8d744c05cb9e607acf7db00d170fd20c35236bd6e06804fb12fcc211bf3a94fd901ae75014bd5a80ad5b92a3c27a1b39eae6ee326ee4d5d0c5c680e2a46eae1ff6680442fd4fe16d641c16a8d61ab1f549878e4e1259a0d493b1e96215970e0b0dd6cd59ff0a81042f647af975e818025283b76fd04cae9960e5242ffa433daed5229b674fd26ddf7cae50328a629f5abfc3001d1cbd1751c01296a89b273a66bf08f238d8e51bace6ce22291b38d2cc827122fb87daad65d45643cc322db3d1e8f932b58429e67bd5da27a9f25e9ed238698b515657b4af24af8ae1302909abf799b67129c77e8d1f469c09cf857ca3dd3db5f2c8ef1d4990b48b352b8d66b99dfd4f2c2e07222fb114bb656f292a2b261420a5b8dd9b850c0fa12bd4317a70c797" + ], + "msk": "187cc2ba35a9afb2d110edf993ced7c7703cd48887e3f0f0890dfb21c53c6e12" +} diff --git a/ui/test/fixtures/msauth/proposal-expired.json b/ui/test/fixtures/msauth/proposal-expired.json new file mode 100644 index 0000000..caa2eb1 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-expired.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ac62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "4e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "expires_at": 5097, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-fee-raised.json b/ui/test/fixtures/msauth/proposal-fee-raised.json new file mode 100644 index 0000000..7465a93 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-fee-raised.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61041420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f41420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "b726e81af430b8d12c1f9bedb8c3d1206074740fe4f5d1bc80dafe3c8084c9f6", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "43d63d18af28a4fd34a4bc985f30ab5227649242aab78b03aa7530f019c3e9e5", + "signing_message": "72616e642d6d73617574682d3143d63d18af28a4fd34a4bc985f30ab5227649242aab78b03aa7530f019c3e9e5", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-substituted-recipient.json b/ui/test/fixtures/msauth/proposal-substituted-recipient.json new file mode 100644 index 0000000..1335d70 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-substituted-recipient.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a1d0779c45d82c6c5951644247c0225d68e9c9ff827dfa5a5ca3cc63e170f01ea0aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e2400400000000000060c7fa2946713079e618e42ba09a3cb75db9dac7d00d2cce83bd3cf5b01742ca6195c54d4c8d2e98b501e713048acc106a8ef1aca47f6d618556ae537668904164793e1ca8e7b3734ad2400eaf6ddb01fc45081682736a3c38572d9886a0423d8ec203201046b582c9fbbafd8d1bd66c19ca58b7384603eedcdd3f3be9d7741d0f9be74b8cdc15c2711362112fbea605b797b16763dddcce4c4db2371be2d7c3552497bed3a2ced2896886fcdb18e2539bf070ee51deacd7eddbe8bcce5db3b6dadefe6b8776742a89a9d1374dc51d7fc9ecf981b6a6e1e46d97f7d0f074e938efa795b807623425b72b4efa2f1a11f9c47baec677c96f3c1d8ef6d709e8b77937700b86a2bfc0d44220d958f64eb09619cb797284a9fc3dcd046cc8fa3e8039d5bc6c9ec1528d2bf777f39039401b8a2fdb6bc0106669c1fbcfa63c604384c2cdda622de4adb5ca8963bdc8e37896763a02db8bf5e43a917d5dad131583ccea212d0ac9ead233033e38a38bfca1b66d8a459d947a18003e93e95076e2ad4c6f80ed2ff98e1eedec73f236976b52298f0eda161ceeab2c65404c133e2ac66da75786870912c60a5ad38351d40ef6c5e31bf098848e5068775c7dbcef6d462d883380cb810d6861c4be83165e570fdfcec111c4160f54c51660e6da88fb36073134e45fbd29338260ba32f7c86cbded4c6a91c11c8103bff6df67281c122a2dac5934c962be0b4d549791dfdc31495614774ec17cd2366c81e0fe869e492cbd6c0e9e4f7d88daa79bc73def26343cb8204fae68793ef112de5e9fea4a009ceb12bc06cfcf809931fb48c21665cc5a6022a792ec4a8adcceaf17513205334c4ea0845591b41fc22fc30a193cecb9e19be984d198fe3ed8132c8f316a8915bf06223420464c07a5708c5544c73d646af9d201a6d848404608e6f0867ceb503d46910a5d1c2c3b742c8028ab3964774263d01d9fdd20ba5a4df76ef5cb3488749b384f41e500f7a02781dd5ec6d72ed0524754067eaa56213501274592bac02ebe27508d1d570e8de59ab7f4e1cfbff1ec8e4c51f707d65cad40fd164b327ca808712e064ccbf096f9fde7ddccc7f886f14216e17feb2f91ef58033d923b30074616149c3ea703b5ac55d2e1e3eb2dc791a24e60a840d178e6bb810f762b9f0f75b6821b9d2de5920e578daf4f83f3a370aefe1648a02298f5288b6e511e38eed7ded9ad3919fccd4933fb9da95a9570b7785e0ef74c39258cddca15c459be2558a2ba18306fad36a3edcfcef669d0e192ad49475d805e5ef5d05b5b5069b7e840778090be3d2f049165e66d5ffca7a9403838d8d5f9ddd73f0a30845fe82297192e1df03c63d6639cb2af40720d4e7ebd883eeea454ff81229262f6bfc42b7ccb3429696e0f161d6b87899578554a383c6a5cde94739f2af0cd7320cefcda0b11edf87a725d51fe6ba424e4fa4c572b38154ad4130d93f2a4c3e2fef8f8309b64431d6434b245519bdb8ad7c38628a132cdb55d957edfa62c29e8c170c34175310a3c00000000000000b713f2482b7b911458629117f161e9d7ac5ffee5ff8acd87dbc83e9ec50d79e3ee5ebba305260b11ab49382faec659e2eb78621ca9b66a5544deafa63c00000000000000ad0dcdc76a8cdad1f3854baa034290dca3deda4e07f70f27d058a97b26d0fed3c2fcb3619f4baedb4bb5379703a8878fb8540d84110d3f5429c9cc978c00000000000000ab8aff03f2f3dde73ea011facaa5b2b5de9d4e739d2edfd27ecb9309491651f8996df4a7aa4694a52710e263fe5dab22e0721a61a49fae45bda38447f11175cb7e89053df96f6cf5b307525cbef01b1c3caa802c41992d242b6e30350039e28e32af52429ee988a18378d95c43fe647bb330d5c5d006f0d225c73e96d1fa48188d899ce53c0e5f50a433cde2400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb9a9c95f367d5f44351a469f55f6c1fb9a7afac21e5448fd77a5879217901cce3600e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "daa6cc2a3e0892cb5507275bc16546b2814cee10dcfe2a736ee705cca9ba15ac", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "2d40b90ba5045572c045330686f60ccc0b2cb93d884a22c948a91b040b535688", + "signing_message": "72616e642d6d73617574682d312d40b90ba5045572c045330686f60ccc0b2cb93d884a22c948a91b040b535688", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-tampered-output.json b/ui/test/fixtures/msauth/proposal-tampered-output.json new file mode 100644 index 0000000..bb25a78 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-tampered-output.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000040a7e215faa9eb1650423be2cb02ba01a97e47a3c121ad0557fef6cbe24a69b1dab724c4d4ff333ae4fc2ba72ceeadfe672134d2d99755027eb17c7f9ae4dfb34a65e5a6cd1275f9314305e49141bb422c12145649ee503054471f83ea0e3b312847ef31ed47ad85f0c66b3b6c9a1376f00a43427778ced81e9b5e1c61baa302a60ff6ba76dda904380f90f1ab43fe20bdc0bb67c0f3b68f6ee5b567aa6b65a92cbccb1e5afb9be96e8581d5996ab0004377f0a607b249013905a2c870e4e489eebc00cdb7acb837e5d0154f1044d20c6522d11479c3c508531ddeac2814cbd3b1a3ec3e3399bc737878f32e55a57a62aedb18f965021738bca9d9bef2a8bd48fc0f66f5c2184fdadb67f0477166e9f082160980b5effecb2c88f7501cb16c0f3cf28b730449e97a59feef4df6e315941e936ed107886d772998a5cca0141837c716308e8e0ef660829e3183ab95dca0422cec99f7bad822f7c3e9fc9c08f304761f4d67e7e85a46e7224d4ffe8f895cbd4ce56ff0499e9cfbd2bcc48672c2a699fbd52534e6363cd9bc6856c6d091f5b09018e1eae49e7b5fafd07d9cb8508afeb13a3e6b26963117e063085e9d1baa1d7e358909fd3939cef498f54aa7467cf2de93bc6f47d596862da92bed635774743cb7d17354e22cff5ee35436689fb5846c4ed5c918200501f2c9ce8aecaf84d92709ac5c336f536485328775fe6716346f09ac5e2b3b20947346504fe892d10b44f22b4e24a5fc211b3b3bd81231f7148d7ebea956de482b049acd1d99d295d83da44f7e5eb3e7c5c2a8b54ba49236e8f7d4ada0596b139bdb7e29a902dd9269e242116f7f280e78cd9560f0a1927587497a50add690f42c39795ca78b3d8b0fd65828d4fb3d68948c52ae2f3e3eadda45e4d0063665d3897309363e5917018d2cb541f9c09e9378deb05e01f98f6ef021666b74b3723f76e2e2dfc722d4acc0d9c9ee38d0e1513059eeda81dd3b0b400768bd1c60ec9184ffbb7923b085e586c9286e43f81913def067d39c89f399563a514b3fa11f1f07c0df616a2a27e3984a3eb870cf6e10c9fcb8b1e08a0452deaded1a41a6e0d5b21eae1eac475005f1d4fe0b5624fdbfc19512a4f3713490c1a5a218cf0be8974fd2bef2a10275a7e12b4c3035ef8d6164639674cd051f8204ee02b9d4fcba9e57cf469dee8b34d401069d6675e54264bcd139624c90e773f73552dace4a022d0a3c6295614796c0ecf5784921d0cca2bf94e36040bc7aebbf9734a160d6abc2d5c60187d2365a0ebf2dc9b37c1d665b0cd0c88806410df9ccf8b1df687815ea5bbfa7f0b30d3c7f10828140e4981f4696a96591f9513c69768a0f4f9cd1084b7a59ba3b0a147b69d37fde8d949b87869cd4b8199054ace50a5b7a6e8235d1332c717c6a3f9521924667c22ea9e192acf2769515ec33fcb89e3d10dc7a34848d548e75ac4447216072a62f92e0cf6f92c93533b028b92a884ca6938dfe0f2519adc9f930eab3c827816dab3f7de2f98ca7a44f2f2324c1123c000000000000006a94ca40cb34650fe39a35cfc1d08bd2eb3dd14a1f16ec9d4e6e5a89deae12fde89be0678ab04102000c3a247a15533dc80194cf8bd586951c760b513c00000000000000449cc90ba3e28ab8d016a3ab137718d2221b295b007d42465336f894210f5307e81e6a8bec61da682cd955b503fcfd098bb53e1d98105d941acc37878c0000000000000061a6a25a65f1e5996f0d62b8c2388c211c7a4e70ce00dd1544032651faa33dea46bec6b1083e279b661597899bc7b6b1beeb5f80dde6aadb2e24bd1ac88391259583e94e0cd559cf83692de9739cf7421cf03f1aa825e8c9f6e556ea46583037c29480cd064ea947aee1effd526b8cb1075f347a55d2fae6a12e4beeeff971f75c18b9a1fa8776451dab4bbb0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ac62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "25612820c22bef29bcbeacaeb48c6297687efe90c4fc630eb52bb3fb099d619d", + "signing_message": "72616e642d6d73617574682d3125612820c22bef29bcbeacaeb48c6297687efe90c4fc630eb52bb3fb099d619d", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-unbound.json b/ui/test/fixtures/msauth/proposal-unbound.json new file mode 100644 index 0000000..3c241b5 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-unbound.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000040a7e215faa9eb1650423be2cb02ba01a97e47a3c121ad0557fef6cbe24a69b1dab724c4d4ff333ae4fc2ba72ceeadfe672134d2d99755027eb17c7f9ae4dfb34a65e5a6cd1275f9314305e49141bb422c12145649ee503054471f83ea0e3b312847ef31ed47ad85f0c66b3b6c9a1376f00a43427778ced81e9b5e1c61baa302a60ff6ba76dda904380f90f1ab43fe20bdc0bb67c0f3b68f6ee5b567aa6b65a92cbccb1e5afb9be96e8581d5996ab0004377f0a607b249013905a2c870e4e489eebc00cdb7acb837e5d0154f1044d20c6522d11479c3c508531ddeac2814cbd3b1a3ec3e3399bc737878f32e55a57a62aedb18f965021738bca9d9bef2a8bd48fc0f66f5c2184fdadb67f0477166e9f082160980b5effecb2c88f7501cb16c0f3cf28b730449e97a59feef4df6e315941e936ed107886d772998a5cca0141837c716308e8e0ef660829e3183ab95dca0422cec99f7bad822f7c3e9fc9c08f304761f4d67e7e85a46e7224d4ffe8f895cbd4ce56ff0499e9cfbd2bcc48672c2a699fbd52534e6363cd9bc6856c6d091f5b09018e1eae49e7b5fafd07d9cb8508afeb13a3e6b26963117e063085e9d1baa1d7e358909fd3939cef498f54aa7467cf2de93bc6f47d596862da92bed635774743cb7d17354e22cff5ee35436689fb5846c4ed5c918200501f2c9ce8aecaf84d92709ac5c336f536485328775fe6716346f09ac5e2b3b20947346504fe892d10b44f22b4e24a5fc211b3b3bd81231f7148d7ebea956de482b049acd1d99d295d83da44f7e5eb3e7c5c2a8b54ba49236e8f7d4ada0596b139bdb7e29a902dd9269e242116f7f280e78cd9560f0a1927587497a50add690f42c39795ca78b3d8b0fd65828d4fb3d68948c52ae2f3e3eadda45e4d0063665d3897309363e5917018d2cb541f9c09e9378deb05e01f98f6ef021666b74b3723f76e2e2dfc722d4acc0d9c9ee38d0e1513059eeda81dd3b0b400768bd1c60ec9184ffbb7923b085e586c9286e43f81913def067d39c89f399563a514b3fa11f1f07c0df616a2a27e3984a3eb870cf6e10c9fcb8b1e08a0452deaded1a41a6e0d5b21eae1eac475005f1d4fe0b5624fdbfc19512a4f3713490c1a5a218cf0be8974fd2bef2a10275a7e12b4c3035ef8d6164639674cd051f8204ee02b9d4fcba9e57cf469dee8b34d401069d6675e54264bcd139624c90e773f73552dace4a022d0a3c6295614796c0ecf5784921d0cca2bf94e36040bc7aebbf9734a160d6abc2d5c60187d2365a0ebf2dc9b37c1d665b0cd0c88806410df9ccf8b1df687815ea5bbfa7f0b30d3c7f10828140e4981f4696a96591f9513c69768a0f4f9cd1084b7a59ba3b0a147b69d37fde8d949b87869cd4b8199054ace50a5b7a6e8235d1332c717c6a3f9521924667c22ea9e192acf2769515ec33fcb89e3d10dc7a34848d548e75ac4447216072a62f92e0cf6f92c93533b028b92a884ca6938dfe0f2519adc9f930eab3c827816dab3f7de2f98ca7a44f2f2324c1123c000000000000006a94ca40cb34650fe39a35cfc1d08bd2eb3dd14a1f16ec9d4e6e5a89deae12fde89be0678ab04102000c3a247a15533dc80194cf8bd586951c760b513c00000000000000449cc90ba3e28ab8d016a3ab137718d2221b295b007d42465336f894210f5307e81e6a8bec61da682cd955b503fcfd098bb53e1d98105d941acc37878c0000000000000061a6a25a65f1e5996f0d62b8c2388c211c7a4e70ce00dd1544032651faa33dea46bec6b1083e279b661597899bc7b6b1beeb5f80dde6aadb2e24bd1ac88391259583e94e0cd559cf83692de9739cf7421cf03f1aa825e8c9f6e556ea46583037c29480cd064ea947aee1effd526b8cb1075f347a55d2fae6a12e4beeeff971f75c18b9a1fa8776451dab4bbb0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ac62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "4e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-valid.json b/ui/test/fixtures/msauth/proposal-valid.json new file mode 100644 index 0000000..6eeeee4 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-valid.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ac62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "4e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-value-mismatch.json b/ui/test/fixtures/msauth/proposal-value-mismatch.json new file mode 100644 index 0000000..88ce7a5 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-value-mismatch.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa601ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ac62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "4e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/proposal-wrong-expected.json b/ui/test/fixtures/msauth/proposal-wrong-expected.json new file mode 100644 index 0000000..fda4dd2 --- /dev/null +++ b/ui/test/fixtures/msauth/proposal-wrong-expected.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "chain_id": 7, + "genesis": "0000000000000000000000000000000000000000000000000000000000000000", + "binding_domain": 0, + "policy": "0034dc5cd62bb377a420afc93bb108567072572dfe863353f18614aff6c63cf9", + "tx": "070000000000000001b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824e54b07ecefbea8343e66bf13dba118fb0218c565f0bca72aa9e69686cb492784099f4e886622aeda7483838c2f8b4edfe2c8b0b180eeca79e882a5e3c59324ffebbfb78eb5fb0cf4fc6de2d813057d6b49f355335a90034f8c83c3b6a2f90553e5342b2292cb965cccfd67d45c1c0ceff29b29c7c72d0d5e934e7ba4658c0ddbe3ebc55a057c9621bf056cb2e9608183da003338162308ebc374ec94969c6d53570f2be92016651b74129b81e8ccab62055d5afd1fe69a32a784d2899549355a10ecf2e541d97320116b200072d0e7b3bbf4619d7545cbbfe41a788dea11c5f62aea58a1f4ab286faf29f5eb6cd2d081c67f0ff64caca7aed4b1e4238c9bab61040420f00000000000000000000000000000000000000000000000000e8030000400400000000000051b7263f3f0f03b0655ffebf63ced1208ea6c94bf56c0568007bbe58610f1d19a24c6155d44dc10f2f45c5f57d18f5e32c2125175dc50fab0de4ae77ea3fadaa9c089fbc710e6dba082c106d5cd5c0e829c18813c00d91a8c3998dea03a56e9f3fc6931ce63c8e000948726a3b18943127fc01302fc203010845605f7f29e5087401208a63b51593340dab999408b387808bc4abf9c6003371065c589025c27b59a19997da08ba05f8d2c2f195facb79f9332c81ff5a9f3c2b426a2a03966e74a2f18847b59c0155a96b7b59e23546f20be0a07b724b0f8a587f4d301b9af34dc901d8cf97ef2305f2181ebb699c18d76716d3726b2c1d1e22aa5cc6c78778809505e4e5e5b699432cee2409f1ba1cbdc5d61ba0c1def74cdbd6cf5d110527ac920b6ba2bd4fb20d31445f2e7b07dcd4f6cb37833d4f91d124e1d98b99670e790770a6b8c8a03529192d0e7c0db26e5890a9e0c7c6960c5abe5c6da82e1879b6ac234c7edc949ee5be0bddbc7eede96b38b418fe127b30149251265c378a27ddb50f6e0115bd138caaf45d08716fd5b3cd8081377e8485ff4bbe044c5a65f0ee664c5ab46ea274ab1e2e9d48eab008455d7223941a64438f6d9f0c0445f9038920dfd0315ea32619cdd68b09a2b71c558ef7c4d024cff0b13554bb764c802d69af91bafea56246889e87d5f8af69c0037f48966d210d5d2a7c88946571c8385f37d6daf6b8cea5acc4c9a28acee00f2b443386b9f271ce6d217b128fb2010c75cfb1d9bd7e06fb2fdcbf6532dc7a2c9315a5f5435dbcfae055aaf3d3e965a8642c37f07ad041c3170eeac41f156945a3bf6346ecbfb65a6dae3e09eef853368399235cea33784717e30895ed19f0a92c47c5e40a2ddd4dbd6b5bb9af0c536acabaa7a49a0bf39a6118553192190f4d1630b6f0692d81f9fccfdd6bf1e4b376885cd10e0a3fdf1d52d6bbfb8c73f0c43de236581a508239e72fb743c50236b69919db1f3e500aa31cbd4f3e49f153aeda2f39b294277919d37ec88402a164b1246e67d4c1c94473834d50ccb636a4af6eaf5f4c206a02315fd453e8877479e5ad61d125682ee0ddaa420c61ac35717940d41e8e2e5c39fa99ae8ef8831b8eb6fe232fea2cd69b3235b714a040699d1617e62883a0fa84715601bd8349fa312387fd45e5ad70bea68afddc530ec98a7c7dfd077db11dcdda8c1ca0573d5a531eb52975c67824fe83002657661c01d9c589f434479d3478d77df5553c85f30a79fccecf241730c9df550f76796f41dd2f50bc1158fac2dd54b9d9c88121fbbeb6082e3526ed007c1f4c5146390d8b2a0ca414ba08d75346bca4348ceac00106e4d175233bdc8cc0c3ac86d17b1ca388dcf793d959e94f66f4613f0c2d606f9fcfa93660d91bd528f87434109df5fbd0b6d9ebf564ce58564e321f5d4c4ae3a3e10e0af293c5b119b6fc3a98f7c43ddad4245a14fb14a3cec0dc58218dc5523bf55f1ffc283340a45f26b78ff62b53e42fa300413c9aa8cabe07cc688c86454e1bdb3c00000000000000ecac63729960026f8398ca927d29032fba1b1ea32a2fe0709f44f692122d099e31f7afa14eeca6fa78df35c20368759051bee43783c350e48a20bb263c00000000000000d538d9e1be371845d034fd8da816f4545825b83b8ab0168ea508ed26b00134905621236d643af4a7ad62775c992b14059aa590b98dd317c1933290f18c0000000000000050fd8c86b4b6bf228077f772a0faa41fc39539560762fc31ef9d37cf1f61ad4193fd67d77d2924a0fc6e414a73e5c0463589de3b50b4e353e206c580d58b3e6852b1900448d7f19533c62f11f00f61142bb791fea8a345b908384bac3af50a7c6527d3d97c9345529b4c4abdf9145d561b638f933c9c039bcff453293c08f9e27002048a10ec09d6dd9f1db840040000000000008bf4bca37fa5e20a3e7869c86b9c3b70dd8bc40fcd7c452a26abca3bc5c3ea2a19263ab64a0663dd40aeab5e67c23435ee2fd84d2622f440c63bac8325146f0273afe7ecff2b89db8b9a948120e7085ad14130597996c9d9489e5b10fd8a5d477a5e4ad2e6ecb9c3c160c5b74209ed6881cb9f095c5b72edf6fb500c1185f51898dae7b85cb26059a380b01f3813c74659035d2deb8b690e757a1766f4cea06521021898a7e1c9d2c4b236602d9676c712f41f80f8212394424ac0496dba1353a6a6af6767d00932ea207d314e845f3c93662fc0b0e8c4dceb2fd425757099b40086ad31ff63d2036fe1f2e07136a774b2d9c1e6cdf61f9960524db121f7303d344ee11d97b1d6093f070066e1a51c29da90a133ad4585dfb5237751fa3fa9ae42873d26532ab0a59a061679275369b2b151e64970c34d3ddc8b3b405ef24e6851c2fbdc9410bc3827d226b75ba41b75d9e25653f8e693a9cb2e8a705b77f766357e5591151326f0c50eb216e4cfa007457e5210e85bf0cecd1c0d9768a055ceb81de5d26357c6d62ff3623508e56f1aba7e21c5ddfec7f7bdbbc6cd2f213d2a1d01c8a044ee469ec7d0350a16220dc68af1b7bba0d1bb80a9cc9f7c85dc641286b8ef1e7e0e397e6aa12fad741ff9ab17659de0d0ed70948835e0ff8229d1feeadb5cd0ec224b1df9c1b710fe93e591c0412cdce063ee8c0627933a6c1c044e4055aa1b87b9715073cbeaa91850cd3f302ee1c3de0dc1ba7882a486ec48ee9c70fda7d29241b508494ee4b055cb4bf852871787e95412a2b0ae8262eb90e6116f165a272877b9ca30563773c6916b8cbbaed44ae3ade387b1f7185580ca4c6e38f2aa22c0237c70a9330c9d9546a00bee6ec02d78974402343d79e9c91e39a0df20a2aaa302a26b978787472968c775ea660376967cdbfcab7e045516dbce6b9dd1fcde791e3d172297f1eca80f712279407aa259bebb767e972b9f6b5fddb08c487d95a53a24e91273a9a57be072c11f947e2bc66508cd85a14108ad7db9f884a10321d4e64c6ab736f4b435d7a82a972d1e364f8b60ecc72da9cea6cd40967d27f1fca9159b1de9eed37395073cddd95bf72227793ca2383513e830b5d922dedb3fd8acde11032506d63910cf01ac0d75f6576b68aee378f199b91b2280c6b79684b449f02a377666cc765c77d28bba5a336f32af0c1829335d42e45afccf78eab0cee48cfb10cd38ae1b35c263d671818d981c6cb6ab0fe56479e12d6c069de1bd7db7da4f98a59f0831e2e0a9060a5791485262cc6a2a15bb584fc73a9a9022ab7fea1aeac6a81dbf4fb3e86f3c2c6990de3563a72cfba7d723982b9a766b51186c0f4404bdebbb88b5608fb21fe1deef8c6e8f7314123ca04362bcd70f8e8538bcea0b7b0940995548cbfb2984b344c5b54a51670512505a979091ca72b559870fd359705044a00966aba1ca9586bf0fad00707a5a655ea1eab87aa721543102cc5e6e086743526bd1067d879b9d65cbd96e5e425c9386b0c079a294b73c000000000000002846978da612f7c5dbb8f4a1367dfb802c63d8645c00014319db06eee3a065c3750fc8076065e242aa96ea5aaa52094bc004e2477f087b24dcfcf61b3c00000000000000ff5de770616d58b139465f33113f0162335532a88b329b2061aa4323b0dc807d854aebfa5352cdcef8f4943f2d2a9e6dc938be3d89fd28e42865b14d8c00000000000000816057edfdeac8ac0e95f8a9307999eb9b78463d146e1e0313e2233b4ab8cb1a9f6352175b7dab85d5a370ef42e86c9186ae9fbc61616b1c0a256cde80ed55cf29334cb4089a26146be5ed63b1e9aacddc2e7b6721fddfdb055e5b87e772f90defd5c7017dc332e921ba0bfe1fbaa68185b1578597ece5df8ad08cd2faa5076d5d47aafe7decc7e82f9647e24004000000000000f2d1e33b34c1610db9f4ac803601e37eb017dbac14bb440610137f4b9b4157ffc9650e190bc8cf5142f1b403f2bd44b95d21dcf42b8d30b41c7c1d62be70ade1bccae19776c3a7e69366884170f0e94e7fbf44fa9be608586aba2fe8313e49fa5717ac15d896fce5778a06d857ccc03486fd7a360e3912135a6fca9be75bfcdf48713b42c6d99d2bbbe99e76bf7204a051c3b5d5339ace600235b5bebffb5860ede8901e667e88a69c139eb225c54a1fb46b454bf92740419cfe0fda397d96b1da0f1e6526071e922b9f00c26252bcfef5177f7a71e89f0922a9bff0136d2cce77bb64d20f4b0c644c4500c68bddfc857667b27061547e8a2b50710c1703ca68c1a2eb293f1be99f95e96367900bed48025e61a55cfb152feeba4babc70385927d302dd9675f69a9ab3b1fc64519fe68c4b94f7af20a5c901fa10b37d203195264b908a21911da2e267dacc774eccffe2f07215c2515e6611bbbf6b9cb3c13eb136757cf192c99409dd7dc3f9ad05eedec2fabbbc8fdd3357903533a5e6a8794713520ccc6be17a704b71dd860ada4c7ce3672721e21b31e3fcbbfd097fbeaae8ea789465b86551581bb855987af85ab7a0587225a269f1faa0efc0505477f206d17f0f11a89d2d0c349ccc69bd938b49f86ebfbba75efa276734c35a10e3776c39b5c6a7495c961835904caa9d03fd0628b29c07cf2974a18d90eae652a9359534e451324b49be21c4c183ebc02876cc166c30b8e15ac3e9a33dbbca693945817361a2655711e581032643f343fe1dbe9d12d991030b0228aaeb7769f6f37470ddaa10a718dc656a185afbedc0930a8c34910649a426e455579e2c6eae9f17e883fc31add854144101f62971b816df1419d2670cbd8ecce45e52e1b5422f527f7203f6fbc4208094e4aa30fe358ccdade0ee1d08226881e74afc80c49a97e43d44894688377a62c5b3c18afe1de22d6070c8c73fa4d4e34e413a9a1a589535467ba768619831e0a7e8f1bb5d21990f174c485dc2ef71b32cfa94d1c4ba5a5681621795bd358b84489b5b05c522885488d9e4f652319f95abbdd373f3bfd2d254ec62391b12ec9451934aff9e55857609f18aaed7af586bcffc92bb52ee446438ff078ccd6325b946c45676a78eed8049f8578b0985101d7aa4b46acfcb1dba679c358c4e1c457bc2ccf29ad292116ce25d088235d8a0ec153c2b7d63d2ac1e351a594c24180f9b6a567a6161371ebee49464f1c1a9448e0b450b5ccdef1859612ba9ba524978c799247df235d0b85e53d5e6f93592bf7802ebce6e7bda102573b3624245813fb8bc5c73b2bd4d9f62080db15a144d94dcec9e33f2ca345f4b3a8214ba2a2621311399bfeecce8aa564c831dab93f665b735241abf3bd51f6c2eebeb380190c52592e1246fb322bb535d34a552b4bd02a222d69ff02fdad0756e1fb325633d55501ef046c690c946bfc96f94c5f93c26c9ed52b24c1cdb1348213fea84733096244cc67deda78c6bfc387f613e3cb74459a6b4b71df42646baf3c00000000000000c81c93d2db6773b36fcadc873d92a17c404251bce8d03684cc8cd6a02c3b1966fc85398effb7446d1df85deb88ee7d29ea60170c0fc1c28575a52dfa3c000000000000003ce59d240ec340754fc58da887a9b157db67104c7a9a4230940e3de83af1282bf450828ebbb64c5b7b3d6ae38c14a9dfe153689ba0533e64589a95e48c000000000000004987707ea403d031874abb93fe10fba43a9084e2ccc3fb75b5ad2392e13766962be11483098f9f78196f77fc39b71ab8d253b7c01f0358cf9a32c059dcd74c8fa2760e7cbaa7d65173383f0782946be96187684a4e7567116d02a40733c373ee5b47cab5e3ea915dc870948464517e085cf98a7a7c853eb4c423a841e173f2dec2d8a76b6f46d13fcbdea30a400400000000000055282b136c0a9dbe9f53698c93cc90dd226fc457871af8c1a1e36f1b70437afce5d3186ba90de39651bc399d6f5695cc1daad9843e6f46fe1b5b777e2f8daf6fe36d9f1cb8c7b4559830c31de4323281c03c9279676e6f2bea4cd7ad59a0c8e7ce3843b746ec0bda10646cb5f1588dd96f95d2f8251a9d263c544d1c97f0913c09a77d775f97cecc0c4a1f7024d3c6175090c9b72cf2ee4a411b2402ffccfe8573d5c1b382820ebc782b4d24b37241e63a5847e83ea9d874d4814e6b5820f1c52779294ef810b61f8220c342d100b5b81692f64700af5d94e42c7abc757d5a535588a58ff0e6edfd923079da6bae6f604b3eca49345ae5b27f418b093e719d0021e991398b1e07fb91de57d0d9b5b581198ae235113a4301d4ec3d028d8bf0099c951241b9874a8d8a3633b4c62dab2805096fbb40444769cb7dfb1c5779b2115fd4ad41386f09c0922042fb3c6983e46f6310e5ae18aab31ea4694b5cf863add3592eafffcbdbc44608ab6ab7dbda2857cdc57e252df5fe1f72921ec3165072ae7203253a28e2d624da4fad54fe09084de29d101d79192159a847dd56755cecc3baebe4178e4244a8db384543e22c0c127f01ac85401f247947157ce18c73199c6833fbab1aa6661c9acd0854dda976df6f06f88e0f453ddbfe14d116ca198a786208238813c4b82a344af404f44032e321614251bf7e51b321da906ab3a293ca8989e220850bf8d44bdb4a2600da89be1f4ad17800282e906609d6b3079c02407d7a7d6ab1c7b65328ee055109ec94df3663a39ffd527f962346525cd6dd20720290d48d05973a63997cb1db7819ec9c2bcee028922709efc21bb4938379e59cbf9ad4ab3cf83213649f6669569262e7e4ac970faea192279e2b4c314344da222f28ed3f58621d424f07fb3f60069fa5ff0c0f1caff97e59cddea6233ffc5b412c0f819490e53c785f331ac74d31e09c30c50c0c5f8ac0202cd3bf63568401fdefe68b78a7138cf473d519fce7622958d198aff553da3186c8b6cbd8f0460226a88b055e37c8e1775e20a7fad64558b9e2cbad67f2d623ddf73c08cf4c47023a4ed82186d40858dbe4152cad4e9c516175361acb259d5d57543e54f6debb29931df3d5cd77c7fffd0c1484740fc0841b83c3b46a0028da83e209fdf2c6b33ae0998383da2bd03d30a405b24a92c04b87f8ed1a2289b316b83c6f37609e1658efdfe27501a9a86814df6e215329359660fa37bb7819f2f683a165555febb0ba7e696a32ea8b5fd2d26c66726946fe15328c386be3cd208ed39228f4e5dbb13527cd67cb33b718ededd80c347f1c2cd14678623dd47f791be0188f741d31788be8c615d6a534b710b1e0fea64247c7e46f504420fe069bb94c801525f944276177134e17345c3f23c43beae38ccc35d776e244671d3649b0ca68db6e40ad435ad50cc8905f8d04fe63a096061577f7f9339570ac715b0c931410a4405e66b62c326a2e466e02c5ce7db8600d158ec46e349c091b70476f9acd831e79ec92f2123c00000000000000c41ade1dcace89b791a12f833efdc5fa650af429aeb3a05366ade6d956f9dc4ad553363de749aadb64aaa927f59685c0c1596c5f32413a506ffd132e3c0000000000000001e6ece317f3b1d5985f688dec576bf9125286034ee3a2c661916e9705b4d555448f2e8ccf39a5f152e21cf70494213b00c4a5334a98622d8761d40b8c00000000000000ca5e720d114cf8ac811457183515c0086f0e8402b97bfed86ead1de2ee5d3ca666a1b1713d07378a74d2d0b37eaa1e1cca73d8afaaa338438ff0f3432a7e909d5917862bfbd60cf6cc5c7c10555ed5754e234cdb0aeacd366ac8bb0f065afc94ef28f705baf255437f32565286abdba009827ac6e471477faab620161fe59302a217c85f013da403449de78b0000000000000000b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d000000000000000000000000", + "bundle_words": "3822abcbec267554d28606330659ff767cd767d8b1965d11b3caace518f9e23c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000997b28d2a7b0fa066064d604337518e9181210268caa463e28acea369295c22500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e80300003d9274b62a6f53504642981a1fdd6888fdc896395ea25326f8e0d305daee275c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f071a76de0b56dc8edf1b1d3cccdb0b3bc71501c5ac584b30b5610d661a4dfa600ba1dd20500000000000000f4010000c054af19598cb19d1b273daa509b828e136f0626839f1c1cf85aab87bad1e07200000000000000000000000000000000000000000000000000000000000000007bdc90ef399c7006dac704a4acd5060199b9d499aa8f08291998692a45582eff639c1575e56c03b18e299d663bb9464791e74677c47db01418470b1e4ce96278fe2b7aedc33f3617885d90c3d83d08fa25bde683497eb7fcd8c1053fb75482e86969a710a8734a9705aed616715faae2db2c694d866fcd39a789b0c0d242780ea4f169f2d91c1b5df06fafa156bdc0fa75d777b28afff0daa0d89bc05aed31a463acc2f0ab015ffbeb26f94b6c3549c9139ace12e05a344fbb9c7e4865482f6e1cf3cc5dda4eb8a2feadeadbeed617418882a86ddbbc6040f5c270b87ef57d67757da9773620f29a3d59efe67cbc00cc4fc368a0ec1ec0ea75571ddfedb3fd717a0c8c469452b17c6d946e497d1380870d1a2fb0b33a05e2c97cdf7660c2a5c181215389d4e6fe3fc2749148e25d731e34b9dd9b406cd4f3feb936f22bc91f97b1e87eb5045ec65857ae048d3f3734fabcf40cb445554c000c3cc4806c207daea6e71133f9a400f2c3fc1c13ccd3db02aa39ac0468034c1be19ab3b8f1b08dd0d2071a6b1547b063e3e7d73347f3bed086508356fb037b435b3707c286e849d886f56d86bd70b90cc7dca1638b76bd157e9cf51a4a68241f6919dfe7ebf10f485fa9d1b76d7a1764f88b3bf42b57a20a31807a383f7317b262db9367fafa6d2c1e429cf9f37a674235d708b9a71b051d1ccd1c082739f5ae165ff7f1241e4957fd3f055ae631fb93b171710764d35e26cc564daa1d8fdf81813c127dd42878f4921a210d149a03234823ffe9729749a6488647cc16d38d02548cde33d562e45958e6013c63350469c7b327f4a6ca45305ef37acbec21a1376097dd49faf7d07fc694cf43d354c7b36783403684e16f026e5502c0ae2ec6e576c0204990bf05b4b6cb81e347bc857029e796435c173fb74ce980a7a11b45b53bce76f721f62aaa8b5710e91a19122daaf4dbfaa7a2df55abe2345d9271dbe1ee834ceac3553b11714d987d38a560b261a0312b1a08d23fd5d78698d3cbe3aeded4147251a5fa9f2965b05f1a6cff629dea4c5a78e1f36cfbc62b51860ead9565560dd3ca3bf016993a36085da7f248d502074efb82e2163fe64f385c2ffb7f543bdee775080b1df0ffbe146cfc5c4d6ae65b70f5b47906574bc576baafdbbfa792bbbbb371c50cff2c9bd499f6cf551bd42c7581ff5ca8a1cf750160c6360809ea30d764cdf8c567da5edc0c69e3ac30ec2671eaa0f8fdf36e04eb3082a99cb49f9512a25526105a7d6f5f2ee2dde7585a0b26f1040cbec38290cfc58a1230c0c1854c4b5ff1e616d78f574a32d4f6efe3c83a0179ed37ba279720a373d3dc0359dc9591fd9750e7bcc84e572b7e78d4748f77529ef982bdfa2cfd7f359133167097a814a07ad0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e8030000581906e92917b38e00337c6ae6d51f3520bc5d58132b41f91178fe84fac484020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68aef9d1737d036382d191a1ee2eef6da67d880a18a41501ea8a1d7594b824eb2b749a9f3838472389920d8d8b917ac9a824f3f13f32a0589e6102c67aabe410000000000000000543a32a39a762a9473f34fa9d075f971f8064902de05754b6c543ec0b0224885863d15e8cce5a58d9f838dc0758c03d2fb6b4ba924fd0d6f3595af6ea037c0ce00000000000000003677194c5ab29d09d1d9e4964456b03b0cd2e013c34f3caefa7ceb83d927ceb97eff2b796da80ab44adcfbb0b24d77442a85a9978671ff6e23c21861378b0f3500e40b54020000004e51cdd702ce6fb93f35890e3908ee07ade0e78f0e4022356d110aecebbe87c682ffffc5b73aad49187760cd2e1b112442dfcf63824401f157dffe38abb1ee3bc093027e03000000d1f0fcc68d4dda579e6de1e551c40c8cb18fe992f419fa5e3ee61205d742d75f40420f00000000000000000000000000000000000000000000000000e80300009139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "expected": "ad62481fd7436c15bb645e3f71f1c9bed2365607371e578fe2f2736630a52fc5", + "salt": "9139f8939573c135b2bc6fac6e0ac0a7155de159b92fa8e8564af38ae7bd4644", + "auth_commit": "b76df5eb1804034d1ae65ba9fd1ce7e6395bc675c59f1d8f47a966c9da69814d", + "binding": "4e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "signing_message": "72616e642d6d73617574682d314e84cffe66625c9f6f9b315d7f48d66c361ebebabbdf8942694a98e2a500c8f5", + "expires_at": 2024, + "summary": [ + "pay 10 RAND to rand17gXwSDLFKubgL1XyThxChyBmpPiSqbaWjqeKVn5K8MfyUMe6fCCtZ52Q2ekUuAfoED4i5JGJ2nTtnpgM9kcGNMyjX42RzZvuqoSyJQXgf9oiB6MMb8AZVizwiGdAuWutF1VvKjQMD8StkUuPznta5xDrJ5xK9F2AM6p31VuLXb6ocHLXp2E6RXD3ThcYcemr7YWTBRENEDNoqBoSpskUkEeuc63ChrwTJ7NYFRSzZnRoyckuHmRvbxrSFT2ezsrDnwo7x3LaSggY7cQFMQZgvUkrK6FkXTK9eVLTEQG6a1trLBNYwza5w1W2UW4yBnQWgdqWcEsGHWaEkXPsX3bYxgUZuuNxCnCXRBX8HduDNBELv54karp6QYgQN9DPbJAok3mZKJpesmY2MuJRkcRF65rcZDKrrz446gKzJNSwigGagDxQ89N5QKFS2bWhukdm8ehCrHEWBcy7jATeY79aQRM5dxz4ckxPCuX8ogrBJeGvKMxtXSFcrgdQ4Wb8U2gwXmthaLgKrfkMM4rjduYBKQ5GXRXKJMfxLqDXcAqMmDecGDKRYk29FjyUPV3PeXtQ8Dzp8sUp2o9brJz7prpzs3UEBHXB7Ha6GZb4HQ2zkDc4Hkg5K3pgdM3z7JbuXUxX5fduk1jvLbh2JBtyLtW7KJw6scBmYwumo8KwGExpvAVMTqVe8rPiwtSs3nvERGzoGs3cAZUGdXPW8NBdadNdW42QiccH9VLCkRNVq3x67jz951Kg63rMk1oVEQKVvLqJPgraGmyWY2DEAweCwTanM5hWGy2e86Z1PQmDnADgWY1nMowkNSHFDm2UYxkwmdr5oXAJeTwn6h6EqMPPADrVXk5zrhSG8HkVXLeBNuXyXC2C1U2JPVwHL145AfkGWT5z7ug8bU4G3Zn1G81cP3VQ1tQgGXppA3ew2o1NBa6tBUka8HtsLKjMJ293JyywDfzKNULEgdJgArwy61LZeWG3UVrt2ZbbS3FbCRnywvdd2AFHUjcqPND4qfw2SKXAZpvba1kDHAY7ZfFmSHx9722NSg7RyYCnb1ALCmFeudfD39iQcqoKTeWTUwMiQz1XfALZYSNFGrwwkurCjB95p372baMxfuPK3sEzyKYNWtWSvvRXSbp5zgQpV6eFGuUjEnUMYPzr9PY3KyMD4ojgMKAi7VtvuWpLvFFTbWJXnNoBbox5V2gb3PfuHTkmF7VHxfFVaAx2Mbhg16LjTakxTKfpMErFrxq9qoRUb5ArUrgsh25H8eifUzjgALpRMakLcAhtPaVCdtZpreQuvFi74Z9o8Ya35qBafAL4dirheTwX8Xv2nppk2zgUeV9KkbytKHd1NSR53654rkmXbiqEBG5d2zv3kZUtjUYQc71DFee615aR9oTrkfcbcdBsDiM7LSHfYdqx4ythkFKMSFT7jfwqF2wUJ2G5WscqXTKsgPAvehHanS4zAzkytkaiF2wmxj378sJkniMfRsxSdovTkfHZYHob4etYZkQtUNswtSCganEBnaYsbrZXeSqzEdmXcWPL2cegBYB5cnS816Kfw8TqaadikmzNGq3LGLHLEgG7QVt4nHHD9ruKrsTg2tkZaF3jXRpyJbwkuq1DG3JwdAT1kysoQjbz7BGV7BTSNd4edsX2nyFFZjeTSuuPP8AtT", + "fee 0.001 RAND" + ] +} diff --git a/ui/test/fixtures/msauth/signer-seeds.json b/ui/test/fixtures/msauth/signer-seeds.json new file mode 100644 index 0000000..dcb2700 --- /dev/null +++ b/ui/test/fixtures/msauth/signer-seeds.json @@ -0,0 +1,8 @@ +{ + "members": [ + "f4684f5b1ba352c74cb2a9798df9778392344484733bf25aab90253b431bf805", + "eb4470ed866f18cbac9fecc22c3930b7ce1c212b6a2198633554db88f16f5a17", + "a879bc5fb3b03f8e496cb7b9bfc86267b5287fc13f05d6995440d04e7325cde3" + ], + "non_member": "441eb45be075624d18d9b6e616e7e8f7ceaa30b1d2dcfe2adbf4c55fea769940" +} diff --git a/ui/test/fixtures/vault-pre-task5.json b/ui/test/fixtures/vault-pre-task5.json new file mode 100644 index 0000000..3470d00 --- /dev/null +++ b/ui/test/fixtures/vault-pre-task5.json @@ -0,0 +1,13 @@ +{ + "note": "A vault record written by ui/engine/crypto.js as of 395707d (before the Task 5 encryptBytes refactor). Frozen: never regenerate.", + "password": "frozen-vector-password-1", + "plaintext": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90", + "record": { + "v": 1, + "kdf": "pbkdf2-sha256", + "iter": 600000, + "salt": "HkH71A7frq+ixxH+laeS2A==", + "iv": "zCl144Ia6SkdeZ6y", + "ct": "sk9on3psNXaLJdV2tEcHbJqV0hBLGVEfd7hvjQx/sOEUqTb0TFcvipz1kMHQsDnjx8X5Qi3gGuLq9BjdbThtDvvM+/s5whVMY7KBBvYRyfo=" + } +} diff --git a/ui/test/i18n.test.mjs b/ui/test/i18n.test.mjs index 482e889..0564b53 100644 --- a/ui/test/i18n.test.mjs +++ b/ui/test/i18n.test.mjs @@ -124,6 +124,8 @@ const ALLOWED = new Set([ // Settings → Prover: the `randprover:` link's scheme (the pairing field's placeholder), and the // fake prover pool's member fingerprints (Crockford digits, "6DYD-YDEN-6XYX-YDY5" and the like). 'randprover', 'DYD', 'YDEN', 'XYX', 'YDY', 'FD', 'ZD', 'GHWN', 'SMN', 'HC', + // Multisig signers: the fake policy fingerprint ("NFYW-K8E9-E5K9-AAK3"), shown as data. + 'NFYW', 'AAK', // Settings → Appearance → Language: each language listed under its own name (LOCALES[].name in // ui/i18n.js), never translated, so a reader who cannot read the one in force can find theirs. // The walk's words are ASCII runs, so Español, Français and Português arrive split at the accent. @@ -140,6 +142,8 @@ const SCREENS = [ ['activity', () => unlockedBackend()], ['explore', () => unlockedBackend()], ['settings', () => unlockedBackend()], + ['signers', () => unlockedBackend()], + ['signer/s1', () => { const b = unlockedBackend(); b.signers.add({ label: 'x', password: 'unlocked-password-1' }); return b; }], ['send', () => unlockedBackend()], ['receive', () => unlockedBackend()], ['withdraw', () => unlockedBackend()], diff --git a/ui/test/msauth-fixtures.test.mjs b/ui/test/msauth-fixtures.test.mjs new file mode 100644 index 0000000..c1bf695 --- /dev/null +++ b/ui/test/msauth-fixtures.test.mjs @@ -0,0 +1,21 @@ +// The private-multisig vectors every app's tests read (iOS bundles the directory, Android adds it to +// its test resources, wallet-core reads it): fullnode's crates/randprotocol-msig/tests/fixtures, +// byte for byte. The hashes live in ONE list, ui/test/fixtures/msauth.sha256 (`shasum -a 256` +// format), which a re-vendor rewrites; wallet-core's `the_fixture_copies_are_the_vendored_vectors` +// holds the copies to the submodule. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFileSync, readdirSync } from 'node:fs'; + +const DIR = new URL('./fixtures/msauth/', import.meta.url); +const SUMS = readFileSync(new URL('./fixtures/msauth.sha256', import.meta.url), 'utf8') + .split('\n').filter(Boolean).map((l) => l.split(/\s+/)).map(([hash, name]) => [name, hash]); + +test('every msauth fixture is listed once, and hashes to its listed SHA-256', () => { + assert.ok(SUMS.length >= 11, `the list names ${SUMS.length} fixtures`); + assert.deepEqual(readdirSync(DIR).sort(), SUMS.map(([n]) => n).sort()); + for (const [name, want] of SUMS) { + assert.equal(createHash('sha256').update(readFileSync(new URL(name, DIR))).digest('hex'), want, name); + } +}); diff --git a/ui/test/signers-engine.test.mjs b/ui/test/signers-engine.test.mjs new file mode 100644 index 0000000..b7dafb1 --- /dev/null +++ b/ui/test/signers-engine.test.mjs @@ -0,0 +1,361 @@ +// The engine's signer profiles (spec 2026-10-11-msauth-signing-design.md §3): kept in the vault, +// policies opened at unlock, seeds only under the password, nothing in plain storage. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { makeWasmBackend } from '../engine/backend-wasm.js'; +import { PASSWORD, mapStorage, stubCore, stubFetch, stubPlatform, GENESIS } from './backend-fixtures.mjs'; + +export const POLICY = '{"version":1,"threshold":2,"signers":["aa","bb","cc"],"msk":"' + '11'.repeat(32) + '"}'; +export const KEY = '{"seed":"' + '22'.repeat(32) + '","address":"A","public_key":"bb"}'; +export const SEED_HEX = '22'.repeat(32); +export function msauthCore(extra = {}) { + return stubCore({ + msauth_policy_info: () => ({ fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(60), policy_hash: '33'.repeat(32) }), + msauth_signer_import: ({ input, policy }) => { + if (!String(input).includes(SEED_HEX)) throw new Error('this key is not one of the policy\'s signers'); + return { seed: SEED_HEX, public_key: 'bb', address: 'A', index: policy ? 1 : null }; + }, + ...extra, + }); +} +export async function unlockedWithSigner(opts = {}) { + const storage = opts.storage || mapStorage(); + const core = opts.core || msauthCore(); + const fetch = opts.fetch || stubFetch(); + const backend = makeWasmBackend({ core, storage, platform: stubPlatform(), fetch, locks: null, broadcast: null }); + await backend.wallet.create(PASSWORD); + const [row] = await backend.signers.add({ label: 'treasury', policy: POLICY, key: KEY, password: PASSWORD }); + return { backend, storage, core, fetch, row }; +} + +test('adding a signer keeps policy and seed only in vault records', async () => { + const { storage, row } = await unlockedWithSigner(); + assert.deepEqual({ ...row, id: 'x' }, { id: 'x', label: 'treasury', index: 1, fingerprint: 'NFYW-K8E9-E5K9-AAK3', threshold: 2, signers: 3, address: 'rand1' + 'm'.repeat(60) }); + const everything = JSON.stringify([...storage.local.entries()]); + assert.ok(!everything.includes(SEED_HEX), 'no seed in plain storage'); + assert.ok(!everything.includes('11'.repeat(32)), 'no msk in plain storage'); + assert.ok(!everything.includes('treasury'), 'no label in plain storage'); + assert.ok(storage.local.get('signerPolicies').ct && storage.local.get('signerSeeds')[row.id].ct); + const session = JSON.stringify([...storage.sessionMap.entries()]); + assert.ok(!session.includes(SEED_HEX), 'the seed is never in the session'); +}); + +test('profiles survive a lock and come back with the normal unlock', async () => { + const { backend, row } = await unlockedWithSigner(); + await backend.wallet.lock(); + await assert.rejects(backend.signers.list()); + await backend.wallet.unlock(PASSWORD); + assert.deepEqual((await backend.signers.list()).map((r) => r.id), [row.id]); +}); + +test('add refuses a wrong password, a key outside the policy and a duplicate', async () => { + const { backend } = await unlockedWithSigner(); + await assert.rejects(backend.signers.add({ label: 'x', policy: POLICY, key: KEY, password: 'wrong-password-here' }), /already saved as treasury/); + await assert.rejects(backend.signers.add({ label: 'y', policy: POLICY, key: '{"seed":"' + '44'.repeat(32) + '"}', password: PASSWORD }), /not one of the policy's signers/); + await assert.rejects(backend.signers.add({ label: '', policy: POLICY, key: KEY, password: PASSWORD }), /1-64 characters/); + const fresh = await unlockedWithSigner({ core: msauthCore() }).then(async ({ backend: b, row }) => { await b.signers.remove(row.id, PASSWORD); return b; }); + await assert.rejects(fresh.signers.add({ label: 'z', policy: POLICY, key: KEY, password: 'wrong-password-here' }), /wrong password/); +}); + +test('remove deletes both halves, and only with the password', async () => { + const { backend, storage, row } = await unlockedWithSigner(); + await assert.rejects(backend.signers.remove(row.id, 'wrong-password-here'), /wrong password/); + assert.ok(storage.local.get('signerSeeds')[row.id].ct, 'the seed record survived the wrong password'); + assert.deepEqual(await backend.signers.remove(row.id, PASSWORD), []); + assert.equal(storage.local.get('signerSeeds')[row.id], undefined); +}); + +test('a BOM-prefixed policy or key file is read like any other', async () => { + const { backend } = await unlockedWithSigner(); + const p = await backend.signers.preview({ policy: `${POLICY}\n`, key: `${KEY}\r\n` }); + assert.equal(p.index, 1); +}); + +test('a wipe removes every profile', async () => { + const { backend, storage } = await unlockedWithSigner(); + await backend.wallet.wipe(); + assert.equal(storage.local.get('signerPolicies'), undefined); + assert.equal(storage.local.get('signerSeeds'), undefined); +}); + +test('a damaged policies record is never overwritten: unlock works, list/add/remove refuse', async () => { + const { backend, storage, row } = await unlockedWithSigner(); + await backend.wallet.lock(); + const rec = storage.local.get('signerPolicies'); + storage.local.set('signerPolicies', { ...rec, ct: 'AAAA' }); + const before = JSON.stringify(storage.local.get('signerPolicies')); + const seedsBefore = JSON.stringify(storage.local.get('signerSeeds')); + await backend.wallet.unlock(PASSWORD); // unlock itself does not fail + await assert.rejects(backend.signers.list(), /could not be opened/); + await assert.rejects(backend.signers.add({ label: 'again', policy: POLICY, key: KEY, password: PASSWORD }), /could not be opened/); + await assert.rejects(backend.signers.remove(row.id, PASSWORD), /could not be opened/); + assert.equal(JSON.stringify(storage.local.get('signerPolicies')), before, 'record byte-identical'); + assert.equal(JSON.stringify(storage.local.get('signerSeeds')), seedsBefore, 'seeds untouched'); +}); + +import { verdict, EXPIRY_MARGIN } from '../engine/signers.js'; + +const SUMMARY = { expires_at: 200, chain_id: 13, genesis: GENESIS, binding_domain: 1, lines: ['x'], policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', recipients_checked: true, + outputs: [{ to_account: false, amount: '5', needs_payee: true, matches: 'FP' }] }; +const signingCore = (extra = {}) => msauthCore({ + msauth_check: () => ({ ...SUMMARY }), + msauth_sign: ({ seed }) => { if (seed !== SEED_HEX) throw new Error('bad seed'); return '1:' + 'ab'.repeat(8); }, + msauth_verify_share: ({ share }) => { if (!share.startsWith('0:')) throw new Error('signer 1\'s signature does not verify over this proposal'); return true; }, + ...extra, +}); +const atHead = (height, table = {}) => stubFetch({ rand_getHead: () => ({ height, hash: 'ff'.repeat(32), view: 1 }), ...table }); + +test('an unmatched or unchecked payee refuses signing, before anything else', async () => { + const unchecked = { ...SUMMARY, recipients_checked: false, outputs: [{ to_account: false, amount: '5', needs_payee: true, matches: null }] }; + assert.equal(verdict({ summary: unchecked, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'unchecked-recipient'); + assert.equal(verdict({ summary: unchecked, head: null, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'unchecked-recipient'); + const seen = []; + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: ({ expected_recipients }) => { seen.push(expected_recipients); return unchecked; } }), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'unchecked-recipient'); + assert.deepEqual(seen, [['rand1payee:5']], 'the payees reach the core'); +}); + +test('the expiry rule is the CLI\'s: refuse at head + 16 >= expires_at', () => { + assert.equal(EXPIRY_MARGIN, 16); + assert.equal(verdict({ summary: SUMMARY, head: 183, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true); + assert.equal(verdict({ summary: SUMMARY, head: 184, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'expired'); +}); + +test('a proposal for another chain or genesis is refused', async () => { + assert.equal(verdict({ summary: { ...SUMMARY, chain_id: 21 }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'wrong-chain'); + assert.equal(verdict({ summary: { ...SUMMARY, genesis: 'bb'.repeat(32) }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'wrong-genesis'); + assert.equal(verdict({ summary: { ...SUMMARY, binding_domain: 0, genesis: 'bb'.repeat(32) }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true, 'a chain-id-bound proposal binds no genesis'); + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => ({ ...SUMMARY, chain_id: 21 }) }), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'wrong-chain'); +}); + +test('with no node the proposal is shown but not signed, naming the node', async () => { + const dead = async () => { throw new TypeError('fetch failed'); }; + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: dead }); + const r = await backend.signers.check(row.id, 'P', ['rand1payee:5']); + assert.equal(r.signable, false); + assert.equal(r.refusal.code, 'no-node'); + assert.match(r.refusal.message, /rpc\.randprotocol\.org/); + assert.deepEqual(r.summary.lines, ['x'], 'the review still shows what was checked'); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'no-node'); +}); + +test('a node that cannot give a head is no node', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: stubFetch({ rand_getHead: () => { throw new Error('busy'); } }) }); + assert.equal((await backend.signers.check(row.id, 'P', ['rand1payee:5'])).refusal.code, 'no-node'); +}); + +test('every signature asks for the password and caches nothing', async () => { + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + const before = JSON.stringify([...storage.sessionMap.entries()]); + assert.deepEqual(await backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), { share: '1:' + 'ab'.repeat(8), index: 1 }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], ''), /wrong password/, 'the second signature asks again'); + assert.equal(JSON.stringify([...storage.sessionMap.entries()]), before, 'the session gained nothing'); + assert.ok(!JSON.stringify([...storage.local.entries()]).includes(SEED_HEX)); + assert.ok(!JSON.stringify([...storage.local.entries()]).includes('"P"'), 'the proposal is never stored'); +}); + +test('a wrong password at signing is throttled like an unlock', async () => { + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], 'wrong-password-here'), /wrong password/); + assert.equal(storage.local.get('unlockFailures').count, 1); + await backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD); + assert.equal(storage.local.get('unlockFailures'), undefined, 'a right password clears the count'); +}); + +test('verifyShare names the position and trims a saved file\'s newline', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100) }); + assert.deepEqual(await backend.signers.verifyShare(row.id, 'P', '0:abcd\n'), { index: 0 }); + await assert.rejects(backend.signers.verifyShare(row.id, 'P', '1:abcd'), /does not verify/); +}); + +// ---- fix round 1: fail closed on malformed summaries, re-check after the backoff ---- +const V = (summary) => verdict({ summary, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }); +test('a malformed summary field is refused, never signable', () => { + for (const bad of [ + { expires_at: undefined }, { expires_at: 'soon' }, { expires_at: -1 }, { expires_at: 1.5 }, { expires_at: null }, + { chain_id: undefined }, { chain_id: 'x' }, + { binding_domain: undefined }, { binding_domain: 'x' }, { binding_domain: 1.5 }, + { genesis: undefined }, { genesis: '' }, { genesis: 'zz' }, + { outputs: undefined }, + ]) { + const r = V({ ...SUMMARY, ...bad }); + assert.equal(r.signable, false, JSON.stringify(bad)); + assert.equal(r.refusal.code, 'malformed-summary', JSON.stringify(bad)); + } + assert.equal(V({ ...SUMMARY }).signable, true); +}); + +test('an output that pays without a match is unchecked even when recipients_checked is true', () => { + const o = (x) => V({ ...SUMMARY, recipients_checked: true, outputs: [x] }); + assert.equal(o({ to_account: false, amount: '5', needs_payee: true, matches: null }).refusal.code, 'unchecked-recipient'); + assert.equal(o({ to_account: false, amount: '5', needs_payee: true }).refusal.code, 'unchecked-recipient'); + assert.equal(o({ to_account: true, amount: '5', matches: null }).signable, true, 'change needs no payee'); + assert.equal(o({ to_account: false, amount: '0', matches: null }).signable, true, 'a zero amount pays nothing'); +}); + +test('the rule is re-run after the password backoff, and a refusal there signs nothing', async () => { + let heads = [100, 190]; let signed = 0; + const fetch = stubFetch({ rand_getHead: () => ({ height: heads.length > 1 ? heads.shift() : heads[0], hash: 'ff'.repeat(32), view: 1 }) }); + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore({ msauth_sign: () => { signed += 1; return '1:ab'; } }), fetch }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'expired'); + assert.equal(signed, 0); + assert.equal(storage.local.get('unlockFailures'), undefined); +}); + +test('a node on another chain is no node, naming the URL', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore(), fetch: atHead(100, { rand_chainId: () => 21 }) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'no-node' && /rpc\.randprotocol\.org/.test(e.message)); +}); + +test('a proposal bound to another genesis is refused through sign()', async () => { + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => ({ ...SUMMARY, genesis: 'bb'.repeat(32) }) }), fetch: atHead(100) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], PASSWORD), (e) => e.code === 'wrong-genesis'); +}); + +test('BOM-prefixed proposal and share reach the core trimmed', async () => { + const seen = []; + const core = signingCore({ + msauth_check: ({ proposal }) => { seen.push(['check', proposal]); return { ...SUMMARY }; }, + msauth_sign: ({ proposal }) => { seen.push(['sign', proposal]); return '1:ab'; }, + msauth_verify_share: ({ proposal, share }) => { seen.push(['verify', proposal, share]); return true; }, + }); + const { backend, row } = await unlockedWithSigner({ core, fetch: atHead(100) }); + await backend.signers.sign(row.id, 'PROP\n', ['rand1payee:5'], PASSWORD); + assert.deepEqual(await backend.signers.verifyShare(row.id, 'PROP\n', '0:abcd\n'), { index: 0 }); + assert.deepEqual(seen, [['check', 'PROP'], ['sign', 'PROP'], ['verify', 'PROP', '0:abcd']]); +}); + +test('a refused sign leaves the failure count alone and never calls msauth_sign', async () => { + let signed = 0; + const { backend, storage, row } = await unlockedWithSigner({ core: signingCore({ msauth_sign: () => { signed += 1; return '1:ab'; } }), fetch: atHead(190) }); + await assert.rejects(backend.signers.sign(row.id, 'P', ['rand1payee:5'], 'wrong-password-here'), (e) => e.code === 'expired'); + assert.equal(signed, 0); + assert.equal(storage.local.get('unlockFailures'), undefined); +}); + +// ---- fix round 2: outputs and the summary itself fail closed ---- +test('a malformed output or summary is malformed-summary', () => { + const code = (summary) => verdict({ summary, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code; + const out = (o) => ({ ...SUMMARY, outputs: [o] }); + assert.equal(code(out(null)), 'malformed-summary'); + assert.equal(code(out({ to_account: false, matches: null })), 'malformed-summary', 'no amount'); + assert.equal(code(out({ to_account: false, amount: '', matches: null })), 'malformed-summary'); + assert.equal(code(out({ to_account: false, amount: 'abc', matches: null })), 'malformed-summary'); + assert.equal(code(out({ amount: '5', matches: null })), 'malformed-summary', 'to_account missing'); + assert.equal(code(null), 'malformed-summary'); + assert.equal(code(undefined), 'malformed-summary'); + for (const amount of ['0', '00', ' 0', 0]) { + assert.equal(verdict({ summary: out({ to_account: false, amount, matches: null }), head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true, `${JSON.stringify(amount)} is zero`); + } +}); + +test('only a non-empty string match counts as matched', () => { + for (const matches of [false, '', ' ', 0, {}, null, undefined, true]) { + const r = verdict({ summary: { ...SUMMARY, outputs: [{ to_account: false, amount: '5', matches }] }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }); + assert.equal(r.refusal && r.refusal.code, 'unchecked-recipient', JSON.stringify(matches)); + } +}); + +test('a blank expected recipient is refused by check and sign, and never reaches the core', async () => { + const seen = []; + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: ({ expected_recipients }) => { seen.push(expected_recipients); return { ...SUMMARY }; } }), fetch: atHead(100) }); + const r = await backend.signers.check(row.id, 'P', ['rand1a:1', ' ']); + assert.equal(r.signable, false); + assert.equal(r.refusal.code, 'unchecked-recipient'); + await assert.rejects(backend.signers.sign(row.id, 'P', ['', 'rand1b:1'], PASSWORD), (e) => e.code === 'unchecked-recipient'); + assert.deepEqual(seen, [], 'nothing was passed on with the blanks removed'); +}); + +test('an empty intended-payments list with a paying output, or an entry without an amount, is refused', async () => { + const paying = { ...SUMMARY, recipients_checked: true, outputs: [{ to_account: false, amount: '5', matches: 'ABCD' }] }; + const seen = []; + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: ({ expected_recipients }) => { seen.push(expected_recipients); return paying; } }), fetch: atHead(100) }); + const r = await backend.signers.check(row.id, 'P', []); + assert.equal(r.refusal.code, 'unchecked-recipient', 'a matches string alone does not make an empty list pass'); + await assert.rejects(backend.signers.sign(row.id, 'P', [], PASSWORD), (e) => e.code === 'unchecked-recipient'); + const noAmount = await backend.signers.check(row.id, 'P', ['rand1a']); + assert.equal(noAmount.refusal.code, 'unchecked-recipient', 'a RAND-only summary needs the amount on every entry'); + assert.equal((await backend.signers.check(row.id, 'P', ['rand1a:'])).refusal.code, 'unchecked-recipient'); + assert.equal((await backend.signers.check(row.id, 'P', [':5'])).refusal.code, 'unchecked-recipient'); + assert.deepEqual(seen, [[], [], ['rand1a'], ['rand1a:']], 'an empty address never reaches the core'); + const ok = await backend.signers.check(row.id, 'P', [' rand1a:5 ']); + assert.deepEqual(seen.at(-1), ['rand1a:5']); +}); + +test('an address-only entry is accepted when a paying output is not RAND', async () => { + const token = { ...SUMMARY, recipients_checked: true, outputs: [{ to_account: false, amount: '5', asset: 7, owner_fingerprint: 'F', matches: 'ABCD' }], expected: [{ entry: 'rand1a', fingerprint: 'F' }] }; + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => token }), fetch: atHead(100) }); + const r = await backend.signers.check(row.id, 'P', ['rand1a']); + assert.equal(r.signable, true); + const rand = { ...token, outputs: [{ ...token.outputs[0], asset: 0 }] }; + const b2 = await unlockedWithSigner({ core: signingCore({ msauth_check: () => rand }), fetch: atHead(100) }); + assert.equal((await b2.backend.signers.check(b2.row.id, 'P', ['rand1a'])).refusal.code, 'unchecked-recipient'); +}); + +test('the amountless exception is per row: only the owner of a non-RAND output may leave the amount out', async () => { + const mixed = (expected) => ({ ...SUMMARY, recipients_checked: true, ...(expected ? { expected } : {}), + outputs: [{ to_account: false, amount: '5', asset: 7, owner_fingerprint: 'F', matches: 'ABCD' }, { to_account: false, amount: '9', asset: 0, owner_fingerprint: 'G', matches: 'EFGH' }] }); + const run = async (summary, entries) => { + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => summary }), fetch: atHead(100) }); + return backend.signers.check(row.id, 'P', entries); + }; + const exp = [{ entry: 'rand1f', fingerprint: 'F' }, { entry: 'rand1g', fingerprint: 'G' }]; + assert.equal((await run(mixed(exp), ['rand1f', 'rand1g:9'])).signable, true, 'the non-RAND owner may go unpinned'); + assert.equal((await run(mixed(exp), ['rand1f:5', 'rand1g'])).refusal.code, 'unchecked-recipient', 'a RAND payee may not'); + assert.equal((await run(mixed(undefined), ['rand1f'])).refusal.code, 'unchecked-recipient', 'expected missing: fail closed'); + assert.equal((await run(mixed('nope'), ['rand1f'])).refusal.code, 'unchecked-recipient'); + assert.equal((await run(mixed([{ entry: 'rand1f' }]), ['rand1f'])).refusal.code, 'unchecked-recipient', 'no fingerprint: fail closed'); +}); + +test('the core\'s payee sentences reach the caller verbatim', async () => { + for (const msg of ['expected recipient AB12-CD34 is paid 3, not 5', 'expected recipient AB12-CD34 is not paid', 'output 2 pays AB12-CD34, not an expected recipient']) { + const { backend, row } = await unlockedWithSigner({ core: signingCore({ msauth_check: () => { throw new Error(msg); } }), fetch: atHead(100) }); + await assert.rejects(backend.signers.check(row.id, 'P', ['rand1a:5']), (e) => e.message === msg); + } +}); + +test('sign hands msauth_sign the same expected-recipient list the check used', async () => { + const seen = []; + const core = signingCore({ + msauth_check: ({ expected_recipients }) => { seen.push(['check', expected_recipients]); return { ...SUMMARY }; }, + msauth_sign: ({ expected_recipients }) => { seen.push(['sign', expected_recipients]); return '1:ab'; }, + }); + const { backend, row } = await unlockedWithSigner({ core, fetch: atHead(100) }); + await backend.signers.sign(row.id, 'P', [' rand1payee:5 ', 'rand1other:2\n'], PASSWORD); + assert.deepEqual(seen, [['check', ['rand1payee:5', 'rand1other:2']], ['sign', ['rand1payee:5', 'rand1other:2']]]); +}); + +// ---- final fix wave ---- +// The reviewer's probe: X is paid 900 RAND beside 1 unit of asset 5, and the signer names X with no +// amount. The token output must not unpin X's RAND, so the verdict refuses. +test('a dust token beside a RAND output to the same payee does not unpin that payee', () => { + const G = '0'.repeat(64); + const summary = { + recipients_checked: true, expires_at: 1000, chain_id: 21, binding_domain: 1, genesis: G, lines: ['x'], + outputs: [ + { to_account: false, amount: '900000000000', asset: 0, owner_fingerprint: 'X', matches: 'AAAA (900 RAND)' }, + { to_account: false, amount: '1', asset: 5, owner_fingerprint: 'X', matches: 'AAAA (0.000000001 RAND)' }, + { to_account: true, amount: '5', asset: 0, owner_fingerprint: 'self', matches: null }, + ], + expected: [{ entry: 'rand1payee', fingerprint: 'X' }], + }; + const r = verdict({ summary, head: 10, chainId: 21, genesis: G, url: 'u', recipients: ['rand1payee'] }); + assert.equal(r.signable, false); + assert.equal(r.refusal.code, 'unchecked-recipient'); + // An output with no asset field counts as RAND too (fails closed). + const noAsset = { ...summary, outputs: [{ ...summary.outputs[0], asset: undefined }, summary.outputs[1]] }; + assert.equal(verdict({ summary: noAsset, head: 10, chainId: 21, genesis: G, url: 'u', recipients: ['rand1payee'] }).refusal.code, 'unchecked-recipient'); + // The same payee paid the token alone may still go unpinned. + const tokenOnly = { ...summary, outputs: [summary.outputs[1], summary.outputs[2]] }; + assert.equal(verdict({ summary: tokenOnly, head: 10, chainId: 21, genesis: G, url: 'u', recipients: ['rand1payee'] }).signable, true); +}); + +test('a summary whose lines are missing or not all strings is malformed-summary', () => { + for (const lines of [undefined, null, 'x', {}, [1], ['x', null], ['x', ['y']]]) { + const summary = { ...SUMMARY }; + if (lines === undefined) delete summary.lines; else summary.lines = lines; + assert.equal(verdict({ summary, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).refusal.code, 'malformed-summary', JSON.stringify(lines)); + } + assert.equal(verdict({ summary: { ...SUMMARY, lines: [] }, head: 1, chainId: 13, genesis: GENESIS, url: 'u' }).signable, true, 'an empty array of lines is well formed'); +}); diff --git a/ui/test/signers-screens.test.mjs b/ui/test/signers-screens.test.mjs new file mode 100644 index 0000000..b7bb9a4 --- /dev/null +++ b/ui/test/signers-screens.test.mjs @@ -0,0 +1,426 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import './dom-env.mjs'; +import { unlockedBackend } from './fake-backend.mjs'; +import { mountApp } from './helpers.mjs'; +import { confirmsFingerprint, lastFourOf, MAX_FILE_BYTES } from '../screens/signers.js'; + +const tick = () => new Promise((r) => setTimeout(r, 0)); +const signerBackend = (over = {}) => { + const b = unlockedBackend(over); + return b; +}; + +test('confirmation is the fingerprint\'s last four, without case or dashes', () => { + assert.equal(lastFourOf('NFYW-K8E9-E5K9-AAK3'), 'aak3'); + assert.ok(confirmsFingerprint('NFYW-K8E9-E5K9-AAK3', ' aak3 ')); + assert.ok(!confirmsFingerprint('NFYW-K8E9-E5K9-AAK3', 'AAK')); + assert.ok(!confirmsFingerprint('', '')); +}); + +test('settings offers the signers screen only where the backend has the group', async (t) => { + const { app, root } = await mountApp(t, signerBackend(), { hash: '#settings' }); + await app.idle(); + assert.ok(root.querySelector('[data-go="signers"]')); +}); + +test('a proposal\'s lines are shown verbatim, as text, with the caveat', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const lines = [...root.querySelectorAll('[data-role="line"]')].map((n) => n.textContent); + assert.equal(lines.length, 6); + assert.ok(lines.slice(0, 4).every((l) => l.endsWith('(owner key verified; delivery key NOT verifiable from this proposal)'))); +}); + +test('sign stays disabled until the last four are typed, and asks for the password each time', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const sign = root.querySelector('[data-role="sign"]'); + assert.equal(sign.disabled, true); + const confirm = root.querySelector('input[data-role="confirm"]'); + confirm.value = 'aak3'; confirm.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(sign.disabled, false); + root.querySelector('input[data-role="password"]').value = 'unlocked-password-1'; + sign.click(); + await app.idle(); await tick(); + assert.equal(root.querySelector('[data-role="share"]').value, '1:' + 'ab'.repeat(8)); + assert.equal(root.querySelector('input[data-role="password"]').value, '', 'the password field is emptied'); +}); + +test('every paying output asks for its payee, and the typed addresses reach check and sign', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const seen = []; + const original = b.signers.check; + b.signers.check = async (id, proposal, payees) => { seen.push(payees); return original(id, proposal, payees); }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const inputs = root.querySelectorAll('input[data-role="payee"]'); + assert.equal(inputs.length, 1, 'the list starts with one empty row'); + inputs[0].value = 'rand1payee'; inputs[0].dispatchEvent(new Event('input', { bubbles: true })); + const amt = root.querySelector('input[data-role="payee-amount"]'); + amt.value = ' 10.5 '; amt.dispatchEvent(new Event('input', { bubbles: true })); + root.querySelector('[data-role="check-payees"]').click(); + await app.idle(); await tick(); + assert.deepEqual(seen.at(-1), ['rand1payee:10.5']); +}); + +test('a refused check names the node and offers Retry, and nothing can be signed', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + b.signers.check = async () => ({ summary: { outputs: [], recipients_checked: true, lines: ['fee 0.001 RAND'], policy_fingerprint: 'NFYW-K8E9-E5K9-AAK3', expires_at: 2024, fee: '1000000', burns: { burn_a: '0', burn_asset: 0, burn_r: '0' } }, + signable: false, refusal: { code: 'no-node', node: 'https://rpc.example', message: 'No node answered at https://rpc.example, so the expiry cannot be checked and this proposal is not signed. Retry when the node is reachable.' } }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + assert.match(root.querySelector('[data-role="refusal"]').textContent, /rpc\.example/); + assert.ok(root.querySelector('[data-role="retry"]')); + assert.equal(root.querySelector('[data-role="sign"]'), null); +}); + +test('the popup offers the side panel instead of a file picker', async (t) => { + const b = signerBackend(); + b.platform.openSidebar = async () => {}; + const { app, root } = await mountApp(t, b, { hash: '#signers', mode: 'popup' }); + await app.idle(); + assert.equal(root.querySelector('input[type=file]'), null); + assert.ok(root.querySelector('[data-role="to-sidebar"]')); +}); + +test('a profile label from storage is text, never markup', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: '', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const { app, root } = await mountApp(t, b, { hash: '#signers' }); + await app.idle(); + assert.equal(root.querySelector('img'), null); +}); + +test('every refusal code shows its message and nothing can be signed', async (t) => { + for (const code of ['malformed-summary', 'unchecked-recipient', 'no-node', 'wrong-chain', 'wrong-genesis', 'expired']) { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const base = (await b.signers.check('s1', '{}', [])).summary; + b.signers.check = async () => ({ summary: base, signable: false, refusal: { code, message: `message for ${code}` } }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + assert.equal(root.querySelector('[data-role="refusal"]').textContent, `message for ${code}`); + assert.equal(root.querySelector('[data-role="sign"]'), null); + app.destroy(); root.remove(); + } +}); + +test('an unreadable profiles record shows its message instead of a blank screen', async (t) => { + const b = signerBackend(); + b.signers.list = async () => { throw Object.assign(new Error('the saved signer profiles could not be opened'), { code: 'signers-unreadable' }); }; + const { app, root } = await mountApp(t, b, { hash: '#signers' }); + await app.idle(); await tick(); + assert.match(root.querySelector('[data-role="error"]').textContent, /could not be opened/); +}); + +test('a share can be saved as a file where the platform can', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const saved = []; + b.platform.saveText = async (f) => { saved.push(f); }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{"x":1}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const confirm = root.querySelector('input[data-role="confirm"]'); + confirm.value = 'AAK3'; confirm.dispatchEvent(new Event('input', { bubbles: true })); + root.querySelector('input[data-role="password"]').value = 'unlocked-password-1'; + root.querySelector('[data-role="sign"]').click(); + await app.idle(); await tick(); + root.querySelector('[data-role="save-share"]').click(); + assert.deepEqual(saved, [{ name: 'share-1.txt', text: '1:' + 'ab'.repeat(8) + '\n' }]); +}); + +test('a file over the cap is refused, not read', async (t) => { + const b = signerBackend(); + const { app, root } = await mountApp(t, b, { hash: '#signers' }); + await app.idle(); + const input = root.querySelector('input[data-file="policy"]'); + const big = { size: MAX_FILE_BYTES + 1, text: async () => { throw new Error('read'); } }; + Object.defineProperty(input, 'files', { value: [big] }); + input.dispatchEvent(new Event('change', { bubbles: true })); + await tick(); + assert.match(root.querySelector('[data-role="error"]').textContent, /larger/); + assert.equal(root.querySelector('textarea[data-role="policy"]').value, ''); +}); + +for (const [name, mutate] of [['a null summary', () => null], ['non-array outputs and lines', (s) => ({ ...s, outputs: 'x', lines: {} })], ['lines that are not all strings', (s) => ({ ...s, lines: ['ok', 1] })]]) { + test(`${name} shows the malformed-summary refusal, with no Sign button`, async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const base = (await b.signers.check('s1', '{}', [])).summary; + b.signers.check = async () => ({ summary: mutate(base), signable: false, refusal: { code: 'malformed-summary', message: 'incomplete answer' } }); + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + assert.equal(root.querySelector('[data-role="refusal"]').textContent, 'incomplete answer'); + assert.equal(root.querySelector('[data-role="sign"]'), null); + }); +} + +test('a blank payee field is refused on the screen and nothing is sent to check', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const original = b.signers.check; + let calls = 0; + b.signers.check = async (...a) => { calls += 1; return original(...a); }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + assert.equal(calls, 1); + root.querySelector('[data-role="check-payees"]').click(); + await app.idle(); await tick(); + assert.equal(calls, 1, 'a blank payee never reaches check'); + assert.ok(root.querySelector('[data-role="payee-error"]').textContent.length > 0); + assert.equal(root.querySelector('[data-role="payee-error"]').hasAttribute('hidden'), false); +}); + +test('a failing save shows the error and a good one shows the path', async (t) => { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + let fail = true; + b.platform.saveText = async () => { if (fail) throw new Error('disk full'); return '/Downloads/share-1-2.txt'; }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const confirm = root.querySelector('input[data-role="confirm"]'); + confirm.value = 'aak3'; confirm.dispatchEvent(new Event('input', { bubbles: true })); + root.querySelector('input[data-role="password"]').value = 'unlocked-password-1'; + root.querySelector('[data-role="sign"]').click(); + await app.idle(); await tick(); + root.querySelector('[data-role="save-share"]').click(); + await tick(); + assert.match(root.querySelector('[data-role="share-note"]').textContent, /disk full/); + fail = false; + root.querySelector('[data-role="save-share"]').click(); + await tick(); + assert.match(root.querySelector('[data-role="share-note"]').textContent, /share-1-2\.txt/); +}); + +async function checked(t, summaryOver, checkFn) { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const original = b.signers.check; + const seen = []; + b.signers.check = async (id, proposal, payees) => { + seen.push(payees); + if (checkFn) return checkFn(payees, () => original(id, proposal, payees)); + const r = await original(id, proposal, payees); + return summaryOver ? { ...r, summary: summaryOver(r.summary) } : r; + }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + return { app, root, seen }; +} +const type = (el, v) => { el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); }; +const submit = async (app, root) => { root.querySelector('[data-role="check-payees"]').click(); await app.idle(); await tick(); }; + +test('two rows send two addr:amount entries, rows can be added and removed', async (t) => { + const { app, root, seen } = await checked(t); + root.querySelector('[data-role="add-payment"]').click(); + root.querySelector('[data-role="add-payment"]').click(); + assert.equal(root.querySelectorAll('input[data-role="payee"]').length, 3); + root.querySelectorAll('[data-role="remove-payment"]')[2].click(); + const [a, b] = root.querySelectorAll('input[data-role="payee"]'); + const [x, y] = root.querySelectorAll('input[data-role="payee-amount"]'); + type(a, 'rand1a'); type(x, '1'); type(b, ' rand1b '); type(y, '2.25'); + await submit(app, root); + assert.deepEqual(seen.at(-1), ['rand1a:1', 'rand1b:2.25']); +}); + +test('an empty row, a row without an amount or a bad amount refuses on the screen', async (t) => { + const { app, root, seen } = await checked(t); + const n = seen.length; + await submit(app, root); + const err = () => root.querySelector('[data-role="payee-error"]'); + assert.ok(err().textContent && !err().hasAttribute('hidden')); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + await submit(app, root); + assert.match(err().textContent, /amount/i); + type(root.querySelector('input[data-role="payee-amount"]'), 'abc'); + await submit(app, root); + assert.ok(!err().hasAttribute('hidden')); + assert.equal(seen.length, n, 'nothing reached check'); +}); + +test('with a non-RAND paying output a row may leave its amount empty, and a note says why', async (t) => { + const { app, root, seen } = await checked(t, (s) => ({ ...s, outputs: s.outputs.map((o) => (o.amount === '10000000000' ? { ...o, asset: 7 } : o)) })); + assert.ok(root.querySelector('[data-role="nonrand-note"]')); + type(root.querySelector('input[data-role="payee"]'), 'rand1tok'); + await submit(app, root); + assert.deepEqual(seen.at(-1), ['rand1tok']); +}); + +test('a RAND-only summary shows no non-RAND note and needs the amount', async (t) => { + const { app, root, seen } = await checked(t); + assert.equal(root.querySelector('[data-role="nonrand-note"]'), null); + const n = seen.length; + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + await submit(app, root); + assert.equal(seen.length, n); +}); + +test('the core\'s three payee sentences reach the screen verbatim', async (t) => { + for (const msg of ['expected recipient AB12-CD34 is paid 3, not 5', 'expected recipient AB12-CD34 is not paid', 'output 2 pays AB12-CD34, not an expected recipient']) { + const b = signerBackend(); + await b.signers.add({ label: 'treasury', policy: 'P', key: 'K', password: 'unlocked-password-1' }); + const original = b.signers.check; + let first = true; + b.signers.check = async (...a) => { if (first) { first = false; return original(...a); } throw new Error(msg); }; + const { app, root } = await mountApp(t, b, { hash: '#signer/s1' }); + await app.idle(); + root.querySelector('textarea[data-role="proposal"]').value = '{}'; + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + type(root.querySelector('input[data-role="payee-amount"]'), '5'); + await submit(app, root); + assert.equal(root.querySelector('[data-role="error"]').textContent, msg); + app.destroy(); root.remove(); + } +}); + +test('the optional-amount exception is per row: only a non-RAND owner\'s row may go unpinned', async (t) => { + const fps = { rand1tok: 'F', rand1r: 'G' }; + const { app, root, seen } = await checked(t, null, async (payees, base) => { + const r = await base(); + const outputs = [{ to_account: false, amount: '5', asset: 7, owner_fingerprint: 'F', matches: 'x' }, { to_account: false, amount: '9', asset: 0, owner_fingerprint: 'G', matches: 'y' }]; + return { ...r, summary: { ...r.summary, outputs, expected: payees.map((e) => ({ entry: e, fingerprint: fps[e.split(':')[0]] || null })) } }; + }); + root.querySelector('[data-role="add-payment"]').click(); + const [a, b] = root.querySelectorAll('input[data-role="payee"]'); + type(a, 'rand1tok'); type(b, 'rand1r'); + await submit(app, root); + assert.deepEqual(seen.at(-1), ['rand1tok', 'rand1r'], 'unfingerprinted rows may be sent address-only to find out'); + assert.equal(root.querySelectorAll('[data-role="nonrand-note"]').length, 1, 'the note stays on the non-RAND row alone'); + const n = seen.length; + await submit(app, root); + assert.equal(seen.length, n, 'the RAND row now needs its amount'); + assert.match(root.querySelector('[data-role="payee-error"]').textContent, /amount/i); + type(root.querySelectorAll('input[data-role="payee-amount"]')[1], '9'); + await submit(app, root); + assert.deepEqual(seen.at(-1), ['rand1tok', 'rand1r:9']); +}); + +test('editing a payment after a check disarms Sign until the payments are checked again', async (t) => { + const { app, root } = await checked(t); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + type(root.querySelector('input[data-role="payee-amount"]'), '1'); + await submit(app, root); + const arm = () => { const c = root.querySelector('input[data-role="confirm"]'); c.value = 'aak3'; c.dispatchEvent(new Event('input', { bubbles: true })); }; + arm(); + assert.equal(root.querySelector('[data-role="sign"]').disabled, false); + assert.equal(root.querySelector('[data-role="stale-note"]').hasAttribute('hidden'), true); + type(root.querySelector('input[data-role="payee-amount"]'), '2'); + assert.equal(root.querySelector('[data-role="sign"]').disabled, true); + assert.equal(root.querySelector('[data-role="stale-note"]').hasAttribute('hidden'), false); + await submit(app, root); + arm(); + assert.equal(root.querySelector('[data-role="sign"]').disabled, false); + root.querySelector('[data-role="add-payment"]').click(); + assert.equal(root.querySelector('[data-role="sign"]').disabled, true, 'add disarms'); + await submit(app, root); + assert.ok(root.querySelector('[data-role="payee-error"]').textContent, 'the blank new row is refused, so Sign stays off'); + assert.equal(root.querySelector('[data-role="sign"]').disabled, true); +}); + +test('the top Check does not re-arm Sign after a row was edited', async (t) => { + const { app, root } = await checked(t); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + type(root.querySelector('input[data-role="payee-amount"]'), '1'); + await submit(app, root); + type(root.querySelector('input[data-role="payee-amount"]'), '2'); + root.querySelector('[data-role="check"]').click(); + await app.idle(); await tick(); + const c = root.querySelector('input[data-role="confirm"]'); c.value = 'aak3'; c.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(root.querySelector('[data-role="sign"]').disabled, true); + assert.equal(root.querySelector('[data-role="stale-note"]').hasAttribute('hidden'), false); +}); + +test('an edit while a check is in flight leaves Sign disabled when it lands', async (t) => { + let release; + const gate = new Promise((r) => { release = r; }); + let n = 0; + const { app, root } = await checked(t, null, async (payees, base) => { n += 1; if (n > 1) await gate; return base(); }); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + type(root.querySelector('input[data-role="payee-amount"]'), '1'); + root.querySelector('[data-role="check-payees"]').click(); + await tick(); + type(root.querySelector('input[data-role="payee-amount"]'), '2'); + release(); + await app.idle(); await tick(); + const c = root.querySelector('input[data-role="confirm"]'); c.value = 'aak3'; c.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(root.querySelector('[data-role="sign"]').disabled, true); +}); + +// ---- final fix wave ---- +test('a payee paid both RAND and a token keeps its amount required, and the screen says why', async (t) => { + const fps = { rand1payee: 'X' }; + const { app, root, seen } = await checked(t, null, async (payees, base) => { + const r = await base(); + const outputs = [{ to_account: false, amount: '900000000000', asset: 0, owner_fingerprint: 'X', matches: 'a' }, { to_account: false, amount: '1', asset: 5, owner_fingerprint: 'X', matches: 'b' }]; + return { ...r, summary: { ...r.summary, outputs, expected: payees.map((e) => ({ entry: e, fingerprint: fps[e.split(':')[0]] || null })) } }; + }); + type(root.querySelector('input[data-role="payee"]'), 'rand1payee'); + await submit(app, root); + assert.deepEqual(seen.at(-1), ['rand1payee'], 'an unfingerprinted row may be sent address-only to find out'); + assert.equal(root.querySelector('[data-role="nonrand-note"]'), null, 'no exception offered to a RAND payee'); + assert.ok(root.querySelector('[data-role="mixed-note"]'), 'the both-RAND-and-token sentence is shown'); + const n = seen.length; + await submit(app, root); + assert.equal(seen.length, n, 'the amount is now required'); + assert.match(root.querySelector('[data-role="payee-error"]').textContent, /amount/i); +}); + +test('editing the proposal after a check clears the review and disarms Sign', async (t) => { + const { app, root } = await checked(t); + type(root.querySelector('input[data-role="payee"]'), 'rand1a'); + type(root.querySelector('input[data-role="payee-amount"]'), '1'); + await submit(app, root); + const c = root.querySelector('input[data-role="confirm"]'); c.value = 'aak3'; c.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(root.querySelector('[data-role="sign"]').disabled, false); + const ta = root.querySelector('textarea[data-role="proposal"]'); + ta.value = `${ta.value} `; ta.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(root.querySelector('[data-role="sign"]'), null, 'the review is cleared'); + assert.equal(root.querySelector('[data-role="line"]'), null); +}); + +test('typing the same proposal text back leaves a fresh review alone', async (t) => { + const { app, root } = await checked(t); + const ta = root.querySelector('textarea[data-role="proposal"]'); + const before = root.querySelectorAll('[data-role="line"]').length; + ta.dispatchEvent(new Event('input', { bubbles: true })); + assert.equal(root.querySelectorAll('[data-role="line"]').length, before); +}); diff --git a/ui/test/vault-frozen.test.mjs b/ui/test/vault-frozen.test.mjs new file mode 100644 index 0000000..0fdd810 --- /dev/null +++ b/ui/test/vault-frozen.test.mjs @@ -0,0 +1,21 @@ +// A vault record written by the pre-Task-5 ui/engine/crypto.js (commit 395707d), frozen as a +// fixture: the refactored decryptSecret/decryptBytes must still open every record already on disk. +// The fixture was made by running `git show 395707d:ui/engine/crypto.js` under node; never regenerate it. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { decryptSecret, decryptBytes } from '../engine/crypto.js'; + +const vector = JSON.parse(readFileSync(new URL('./fixtures/vault-pre-task5.json', import.meta.url), 'utf8')); + +test('a pre-refactor vault record opens through decryptSecret to the exact plaintext', async () => { + assert.equal(await decryptSecret(vector.password, vector.record), vector.plaintext); +}); + +test('a pre-refactor vault record opens through decryptBytes to the plaintext\'s exact bytes', async () => { + assert.deepEqual(await decryptBytes(vector.password, vector.record), new TextEncoder().encode(vector.plaintext)); +}); + +test('the frozen record still refuses the wrong password', async () => { + await assert.rejects(decryptSecret(`${vector.password}x`, vector.record), /wrong password/); +}); diff --git a/web/wallet/main.js b/web/wallet/main.js index da7c472..9e771d8 100644 --- a/web/wallet/main.js +++ b/web/wallet/main.js @@ -67,6 +67,14 @@ function makePlatform(version) { // this one — the wallet's window is not something an explorer should be able to reach into. openExternal: (url) => { window.open(url, '_blank', 'noopener,noreferrer'); }, copy: (text) => navigator.clipboard.writeText(String(text ?? '')), + // A share (`i:hex`) or other small text as a downloaded file. Not secret. + saveText: ({ name, text }) => { + const url = URL.createObjectURL(new Blob([String(text)], { type: 'text/plain' })); + const a = document.createElement('a'); + a.href = url; a.download = String(name); a.rel = 'noopener'; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }, }; if (version) platform.version = version; // Optional in the contract: where reading the clipboard is not available, the send screen offers diff --git a/web/wallet/test/core.integration.test.mjs b/web/wallet/test/core.integration.test.mjs index f269bf2..f1aa5bf 100644 --- a/web/wallet/test/core.integration.test.mjs +++ b/web/wallet/test/core.integration.test.mjs @@ -238,18 +238,18 @@ test('BUNDLE_INPUTS matches what the real core will actually select', { skip }, // ------------------------------------------------------------------------- the chain-14 core --- -test('the artefact this wallet loads is built for chain 20 on the hidden-asset bundle', { skip }, async () => { +test('the artefact this wallet loads is built for chain 21 on the hidden-asset bundle', { skip }, async () => { const { PROVER_WARNING } = await import('../../../ui/screens/settings.js'); // The wasm is git-ignored build output, so "is it the current one" is a real question: a stale // artefact would answer every offline call above perfectly while proving against a guest the // chain no longer runs. These are the constants that moved, and a stale build fails here. const core = await realCore(); const k = await core.call('version'); - assert.equal(k.default_chain_id, 20, 'the core has described chain 20 since 2026-10-01 (19 from 2026-09-30, 18 from e830acc, 16 from d3e3ada before that)'); + assert.equal(k.default_chain_id, 21, 'chain 21 from e34117b (constraint set 9); 20 from 2026-10-01 before that'); // Fullnode v0.6.7 (constraint set 8 and split authorisation — the build chains 18 and 19 run): // every bundle proof declares the guest's ceiling, which the genesis pins; the chains on which a // memo claim is never believed; the two guests chain 18's `rand_status` names (2026-09-30). - assert.equal(k.chain_build, '37c4fdd'); + assert.equal(k.chain_build, 'c0878c3'); assert.equal(k.bundle_gas_limit, 20479); assert.equal(k.hc_bundle, '60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce', 'bundle guest v3'); assert.equal(k.hc_auth, '1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1', 'the auth guest'); @@ -435,3 +435,32 @@ test('prover: the real core parses a pairing link and fingerprints its key', { s await assert.rejects(core.call('parse_prover_link', { link: 'https://127.0.0.1:8546' })); assert.equal((await core.call('version')).prover_wire, 1); }); + +test('the wasm core checks and signs fullnode\'s private-multisig vectors exactly', { skip }, async () => { + const core = await realCore(); + const fx = (n) => readFileSync(new URL(`../../../ui/test/fixtures/msauth/${n}.json`, import.meta.url), 'utf8'); + const expected = JSON.parse(fx('expected')); + const seeds = JSON.parse(fx('signer-seeds')); + const [proposal, policy] = [fx('proposal-valid'), fx('policy')]; + // The real payee, as fullnode's generator wrote it into the informational summary. + const payee = JSON.parse(proposal).summary[0].split(' ').pop(); + assert.equal(payee, expected.recipient); + const expected_recipients = [`${payee}:10`]; + const s = await core.call('msauth_check', { proposal, policy, expected_recipients }); + assert.equal(s.recipients_checked, true); + assert.equal(s.policy_fingerprint, 'NFYW-K8E9-E5K9-AAK3'); + assert.equal(s.outputs[2].amount, '10000000000'); + assert.deepEqual(s.lines, expected.lines_expecting_recipient); + for (const [i, want] of [[0, expected.share_0], [2, expected.share_2]]) { + assert.equal(await core.call('msauth_sign', { proposal, policy, seed: seeds.members[i], expected_recipients }), want); + assert.equal(await core.call('msauth_verify_share', { proposal, policy, share: want }), true); + } + await assert.rejects(core.call('msauth_sign', { proposal, policy, seed: seeds.members[0] }), + /^Error: no expected recipient given: the payee was NOT checked$/); + await assert.rejects(core.call('msauth_sign', { proposal: fx('proposal-substituted-recipient'), policy, seed: seeds.members[0], expected_recipients: [payee] }), + (e) => e.message === expected.substituted_refusal); + await assert.rejects(core.call('msauth_sign', { proposal, policy, seed: seeds.non_member, expected_recipients }), /not one of the policy's signers/); + await assert.rejects(core.call('msauth_check', { proposal: fx('proposal-unbound'), policy }), /does not hash to the proposal's binding/); + const imported = await core.call('msauth_signer_import', { input: seeds.members[2], policy }); + assert.equal(imported.index, 2); +}); diff --git a/web/wallet/test/msauth.devnet.e2e.test.mjs b/web/wallet/test/msauth.devnet.e2e.test.mjs new file mode 100644 index 0000000..1d27eca --- /dev/null +++ b/web/wallet/test/msauth.devnet.e2e.test.mjs @@ -0,0 +1,68 @@ +// One private-multisig signature from the web wallet's engine (ui/engine/signers.js) over the real +// wasm core, against the devnet scripts/e2e-msauth.sh runs: the signer profile is added from the +// policy and key files, the proposal is checked with no payee (refused), with the payee's address +// but no amount (refused), then with `addr:AMOUNT` (signable), and signed; the share is written +// to the work directory for the CLI's prove. +// +// Skipped unless MSAUTH_E2E_DIR and MSAUTH_E2E_RPC are set and the wasm core is built. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { makeWasmBackend } from '../../../ui/engine/backend-wasm.js'; +import { mapStorage } from '../../../ui/test/backend-fixtures.mjs'; + +const CORE_JS = new URL('../../../extension/shared/core/rand_wallet.js', import.meta.url); +const CORE_WASM = new URL('../../../extension/shared/core/rand_wallet_bg.wasm', import.meta.url); +const DIR = process.env.MSAUTH_E2E_DIR || ''; +const RPC = process.env.MSAUTH_E2E_RPC || ''; +const SIGNER = process.env.MSAUTH_E2E_SIGNER ?? '0'; +const PASSWORD = 'an-e2e-password-for-a-real-vault'; +const skip = !DIR || !RPC || !existsSync(CORE_WASM) ? 'run scripts/e2e-msauth.sh' : false; +const read = (n) => readFileSync(join(DIR, n), 'utf8'); + +test('a web wallet signs a private-multisig proposal on a devnet', { skip, timeout: 120_000 }, async () => { + const mod = await import(CORE_JS.href); + mod.initSync({ module: readFileSync(CORE_WASM) }); + const core = { + async call(m, p = {}) { + const r = JSON.parse(mod.call(m, JSON.stringify(p))); + if (!r.ok) throw new Error(r.error); + return r.value; + }, + }; + const backend = makeWasmBackend({ + core, storage: mapStorage(), fetch, + platform: { name: 'node-e2e', openExternal() {}, copy() {} }, + locks: null, broadcast: null, + }); + await backend.wallet.create(PASSWORD); + await backend.settings.set({ rpcUrl: RPC }); + const rows = await backend.signers.add({ label: 'web signer', policy: read('policy.json'), key: read(`signer${SIGNER}.key.json`), password: PASSWORD }); + assert.equal(rows.length, 1); + const [row] = rows; + assert.equal(row.index, Number(SIGNER)); + const proposal = read('proposal.json'); + // payee.txt: `rand1…:AMOUNT`, what the payee told each signer out of band. + const payment = read('payee.txt').trim(); + const address = payment.slice(0, payment.lastIndexOf(':')); + + const unchecked = await backend.signers.check(row.id, proposal); + assert.equal(unchecked.signable, false); + assert.equal(unchecked.refusal.code, 'unchecked-recipient', 'no payee entered: nothing signable'); + const amountless = await backend.signers.check(row.id, proposal, [address]); + assert.equal(amountless.signable, false); + assert.equal(amountless.refusal.code, 'unchecked-recipient', 'a RAND payee without its amount: nothing signable'); + + const checked = await backend.signers.check(row.id, proposal, [payment]); + assert.equal(checked.signable, true, JSON.stringify(checked.refusal)); + assert.equal(checked.summary.recipients_checked, true); + assert.equal(checked.summary.chain_id, 21); + + const { share, index } = await backend.signers.sign(row.id, proposal, [payment], PASSWORD); + assert.equal(index, Number(SIGNER)); + assert.match(share, new RegExp(`^${SIGNER}:[0-9a-f]+$`)); + assert.deepEqual(await backend.signers.verifyShare(row.id, proposal, share), { index }); + writeFileSync(join(DIR, `share${SIGNER}.txt`), `${share}\n`); + console.log(`web signer ${index}: share written (${share.length} chars)`); +}); diff --git a/web/wallet/test/node.e2e.test.mjs b/web/wallet/test/node.e2e.test.mjs index 93bcf3d..cc3159f 100644 --- a/web/wallet/test/node.e2e.test.mjs +++ b/web/wallet/test/node.e2e.test.mjs @@ -54,7 +54,7 @@ import { const NODE_BIN = process.env.RAND_NODE_BIN || ''; const PROVER_BIN = process.env.RAND_PROVER_BIN || ''; const PASSWORD = 'an-e2e-password-for-a-real-vault'; -const CHAIN_ID = 20; // the chain this build's core was made for (`version.default_chain_id`) +const CHAIN_ID = 21; // the chain this build's core was made for (`version.default_chain_id`) const FAUCET_UNITS = 100_000_000_000n; // one `rand_mint`: 100 RAND const SEND_UNITS = '2000000000'; // 2 RAND out of the faucet's 100 const ZERO32 = '00'.repeat(32); @@ -182,7 +182,7 @@ before(async () => { env.genesis = join(env.dir, 'genesis.json'); env.datadir = join(env.dir, 'data'); const help = execFileSync(NODE_BIN, ['genesis', '--help'], { stdio: 'pipe' }).toString(); - // A memo chain, as chain 20 is: every envelope is exactly 1860 bytes, so `rand_getLimits` names it. + // A memo chain, as chains 20 and 21 are: every envelope is exactly 1860 bytes, so `rand_getLimits` names it. env.envelopeBytes = help.includes('--envelope-bytes') ? 1860 : null; execFileSync(NODE_BIN, ['keygen', '--out', env.key], { stdio: 'pipe' }); const cut = execFileSync(NODE_BIN, [ diff --git a/web/wallet/test/prover.e2e.test.mjs b/web/wallet/test/prover.e2e.test.mjs index 6a29cb6..a6ad5d9 100644 --- a/web/wallet/test/prover.e2e.test.mjs +++ b/web/wallet/test/prover.e2e.test.mjs @@ -33,7 +33,7 @@ import { const NODE_BIN = process.env.RAND_NODE_BIN || ''; const PROVER_BIN = process.env.RAND_PROVER_BIN || ''; const PASSWORD = 'an-e2e-password-for-a-real-vault'; -const CHAIN_ID = 20; // the chain this build's core was made for (`version.default_chain_id`) +const CHAIN_ID = 21; // the chain this build's core was made for (`version.default_chain_id`) const SEND_UNITS = '2000000000'; // 2 RAND out of the faucet's 100 const missing = [ @@ -153,6 +153,12 @@ test('a wasm wallet pairs a rand-prover that is not its own, sends through it, a // The chain, as the engine reads it: both guests, so the core makes the auth proof here. const status = await rpc(env.rpcUrl, 'rand_status'); if (env.v3) assert.match(String(status.hc_auth), /^[0-9a-f]{64}$/, 'the node names its auth guest'); + // A chain-21 shape (RAND_E2E_GENESIS_ARGS carries --auth-multisig-guest): the node names the + // msauth guest and loaded its pinned verifier key, and this wallet's single-key spend still lands. + if ((process.env.RAND_E2E_GENESIS_ARGS || '').includes('--auth-multisig-guest')) { + assert.match(String(status.hc_auth_multisig), /^[0-9a-f]{64}$/, 'the node names its multisig auth guest'); + assert.equal(status.msauth_key, 'loaded', 'the node loaded the Test msauth key (RAND_MSAUTH_VK)'); + } // Faucet the sender through the engine, and scan until the note is spendable. await env.a.backend.faucet.request(); diff --git a/web/wallet/test/withdraw.e2e.test.mjs b/web/wallet/test/withdraw.e2e.test.mjs index a4d042e..6093de8 100644 --- a/web/wallet/test/withdraw.e2e.test.mjs +++ b/web/wallet/test/withdraw.e2e.test.mjs @@ -72,7 +72,7 @@ const PROVER_BIN = process.env.RAND_PROVER_BIN || ''; const CLI_BIN = process.env.RAND_CLI_BIN || ''; const FIXTURES_BIN = process.env.RAND_FIXTURES_BIN || ''; const PASSWORD = 'an-e2e-password-for-a-real-vault'; -const CHAIN_ID = 20; // the chain this build's core was made for (`version.default_chain_id`) +const CHAIN_ID = 21; // the chain this build's core was made for (`version.default_chain_id`) const SOURCE_CHAIN = 2; // Ethereum's bridge chain id // Chain 2 USDT, the mainnet wire address: twelve zero bytes then the contract. The genesis // `e2e-fixtures` writes lists zUSD backed by exactly this coin.