Skip to content

nightly-labs/nightly-mobile2mobile

Repository files navigation

Nightly Mobile2Mobile

Nightly_Read_Me

Nightly Mobile2Mobile is a React Native reference dapp that exercises mobile-to-mobile deep-link flows with the Nightly wallet across pluggable blockchain networks. It showcases the encrypted handshake, message signing, and transaction building needed to hand transactions off to the wallet.

Table of Contents

Prerequisites

  • Node.js 20 or newer.
  • Yarn 1.x (recommended) or npm that ships with Node 20.
  • A working React Native CLI environment (Xcode + CocoaPods for iOS, Android Studio/SDK + Java for Android). Follow the official setup guide.
  • Ruby >= 2.6 with Bundler for managing the CocoaPods toolchain (Gemfile).
  • A Nightly mobile wallet build installed on the test device/emulator to consume the nightly:// scheme.

Installation

  1. Install JavaScript dependencies:
    yarn install
    # or
    npm install
  2. (iOS only) Install Ruby gems and CocoaPods:
    bundle install
    (cd ios && bundle exec pod install)

Running the app

This project reserves Metro port 8083 to avoid clashes with other React Native apps.

  1. Start Metro

    yarn start:8083
    # or
    npm run start:8083
  2. Launch a platform target (use a second terminal):

    • Android: yarn android or npm run android
    • iOS: yarn ios or npm run ios

    For iOS on Apple Silicon, ensure you have installed pods with arch -x86_64 bundle exec pod install if the simulator target still requires Rosetta.

When everything is set up correctly you will see the Network selector screen, allowing you to jump into whichever network-specific tooling is available.

Deep-link workflow

Ephemeral dapp key

Every session starts by minting a fresh X25519 keypair so the dapp and wallet can agree on an encrypted channel.

  • generateKeypair() from src/utils/encryption.ts wraps tweetnacl.box.keyPair(), returning a base58 public key and a raw secret key kept only in memory.
  • The public half is passed to the wallet as dappEncryptionPublicKey; the secret half never leaves the app but is used after connect to derive the shared secret.

Example from src/screens/AptosScreen.tsx:

const [initialKeys] = useState(() => generateKeypair());
const [dappPublicKey, setDappPublicKey] = useState(initialKeys.publicKey58);
const [dappSecretKey, setDappSecretKey] = useState(initialKeys.secretKey);

// After the wallet replies we turn the two keys into a shared secret:
const shared = deriveSharedSecret(response.walletPub, dappSecretKey);
setWalletSharedKey(shared);

Call generateKeypair() again whenever you want to reset the session (the sample does this on screen mount and when you tap Disconnect).

Connect

Tap Connect to start the Nightly handshake; the app pushes a nightly://v1/direct/connect URL to the wallet. For a full implementation, see the openWalletConnect helper in the network screen modules under src/screens/.

Payload → Wallet (IDappConnectRequest)

{
  "network": "aptos",
  "cluster": "mainnet",
  "responseRoute": "nightlydapp:///api/v1/connect",
  "appInfo": {
    "name": "NightlyMobile2Mobile",
    "icon": "https://placehold.co/64x64",
    "url": "https://example.local"
  },
  "dappEncryptionPublicKey": "<dapp-public-key-base58>"
}
  • network (required): one of aptos, movement, sui, or iota (defined in types.ts).
  • cluster (required): mainnet, testnet, or devnet; skip combos the target chain does not support.
  • responseRoute (required): any deep link owned by your app; use your own scheme/host if you fork the sample.
  • appInfo (required): wallet-facing metadata rendered in the approval sheet (see AppInfoMeta).
  • dappEncryptionPublicKey (required): base58 public key generated per session to derive the shared secret.

Response → App (WalletEncryptedResponse) Wallet returns to the responseRoute with a base64 data param containing the WalletEncryptedResponse envelope:

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": "<ciphertext-base64>"
}

This envelope shape is identical across every supported network.

  • success: boolean indicating whether the wallet completed the flow.
  • walletPub + the stored secret key allow the dapp to derive the shared key.
  • nonce: base58 nonce needed when decrypting the payload.
  • payload: base64 ciphertext encrypted with the shared key.
  • Persist the shared key for subsequent encrypted requests.

Decrypted payload example (placeholders):

{
  "activeAccount": {
    "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "publicKey": "5oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
  }
}
  • address: wallet address selected inside Nightly for the current network.
  • publicKey: associated base58-encoded public key the dapp can use for verification.

Sign Message

Once connected, tapping Sign Message sends an encrypted signMessage request. Refer to the openWalletSignMessage helper in the network screen modules under src/screens/ for the full flow, including payload encryption and deep-link construction.

Payload → Wallet (IDappSignMessageRequest)

{
  "network": "aptos",
  "cluster": "mainnet",
  "responseRoute": "nightlydapp:///api/v1/response",
  "payload": "<ciphertext-base64>",
  "nonce": "<nonce-base58>",
  "dappEncryptionPublicKey": "<dapp-public-key-base58>",
  "address": "<wallet-address>",
  "appInfo": {
    "name": "NightlyMobile2Mobile",
    "icon": "https://placehold.co/64x64",
    "url": "https://example.local"
  }
}
  • network (required): one of the NETWORK enum values (aptos, movement, sui, iota).
  • cluster (required): choose a supported cluster for the selected network (mainnet, testnet, devnet).
  • responseRoute (required): deep link Nightly should call after signing (sample config uses nightlydapp:///api/v1/response).
  • dappEncryptionPublicKey (required): session key used to encrypt the inner payload.
  • payload (required): encrypted message payload for the target network (IAptosDappSignMessageInnerPayload, IMovementDappSignMessageInnerPayload, etc.; see src/types/wallet.ts).
  • nonce (required): base58 nonce used when encrypting payload.
  • address (required): wallet account the dapp expects the signature from.
  • appInfo (required): conforms to AppInfoMeta.

Response → App (WalletEncryptedResponse) Wallet responds through the same envelope (success, walletPub, nonce, payload).

Decoded envelope example (placeholders):

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": {
    "signature": "0xaaaaaaaa…"
  }
}

Return an error payload instead if the user rejects or the wallet cannot sign.

Sign Transaction

The Transfer button builds network-specific transactions and requests a signature bundle. Refer to the openWalletSignTransactions helper in the network screen modules under src/screens/ for the end-to-end link construction.

Payload → Wallet (IDappSignTransactionsRequest)

{
  "network": "aptos",
  "cluster": "mainnet",
  "responseRoute": "nightlydapp:///api/v1/response",
  "payload": "<ciphertext-base64>",
  "nonce": "<nonce-base58>",
  "dappEncryptionPublicKey": "<dapp-public-key-base58>",
  "address": "<wallet-address>",
  "appInfo": {
    "name": "NightlyMobile2Mobile",
    "icon": "https://placehold.co/64x64",
    "url": "https://example.local"
  }
}
  • network (required): one of the NETWORK enum values.
  • cluster (required): target cluster (mainnet, testnet, devnet) supported by the network.
  • responseRoute (required): deep link to receive the signed results (sample config uses nightlydapp:///api/v1/response).
  • dappEncryptionPublicKey (required): session key used to encrypt the transaction payload.
  • payload (required): encrypted transaction payload for the selected network (see IAptosDappSignTransactionsInnerPayload, IMovementDappSignTransactionsInnerPayload, etc. in src/types/wallet.ts).
  • options (optional): behaviour flags that adjust wallet-side handling.
    • submit (optional; defaults to false): set to true to have the wallet broadcast on success (Aptos, Movement, Sui, Iota).
  • nonce (required): base58 nonce paired with the encrypted payload.
  • address (required): wallet account expected to sign.
  • appInfo (required): conforms to AppInfoMeta.

Response → App (WalletEncryptedResponse) Wallet responds through the same envelope (success, walletPub, nonce, payload). The decoded payload differs between networks and whether options.submit was set.

Submit enabled (placeholders):

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": {
    "hashes": "[\"<transaction-hash>\"]"
  }
}

Applies to Aptos, Movement, Sui, and IOTA when options.submit is true.

Sign only examples (placeholders):

Aptos / Movement

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": {
    "senderAuthenticator": "0xbbbbbbbb…",
    "network": "movement"
  }
}

Sui

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": {
    "signedTransaction": {
      "signature": "APWPNC8o…",
      "transactionBlockBytes": "AAACAAhA…"
    }
  }
}

IOTA

{
  "success": true,
  "walletPub": "<wallet-public-key-base58>",
  "nonce": "<nonce-base58>",
  "payload": {
    "signedTransaction": {
      "signature": "ACPOCq6BPFI…",
      "bytes": "AAACAAhA…"
    }
  }
}

Regardless of flow, the UI surfaces the last outgoing URL, the callback URL, and the decoded payload so you can validate each step. Use Disconnect to throw away the shared key and restart the handshake.

Configuration

  • App identity – Update APP_INFO in src/utils/deeplink.ts with your app name, URL, and icon. The wallet surfaces this information during connection prompts.
  • URL scheme – Change the nightlydapp scheme if you fork the project. Update:
    • iOS: ios/NightlyMobile2Mobile/Info.plist (CFBundleURLSchemes)
    • Android: android/app/src/main/AndroidManifest.xml <data android:scheme>
    • JavaScript constants: DAPP_LINK_BASE in each screen module (e.g. Aptos, Movement, or Sui)
  • Network/cluster defaults – Edit types.ts to change supported clusters, default network enumeration values, or initial addresses.

Troubleshooting

  • If tapping Connect does nothing, confirm the Nightly wallet is installed and registered for the nightly:// scheme on the device/emulator.
  • Incoming URLs are rendered in the debug panel; if the data field is missing, verify the wallet returned base64 payloads and that the shared secret was derived (watch the Metro logs for warnings).
  • Transaction building requires network access to the configured fullnode. Ensure your simulator/device can reach the endpoint for the network you are testing.
  • Clear Metro cache (yarn start:8083 --reset-cache) if you change native deep-link configuration and the app does not pick it up immediately.

About

Nightly mobile2mobile example app

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors