DashPay Contact Requests
DashPay contact requests are contactRequest documents in the DashPay data
contract. The document links the sender identity to toUserId and carries the
sender's DIP-15 contact payment public key encrypted for the recipient.
The Evo SDK exposes the DIP-15 derivation primitive through
wallet.deriveDashpayContactKey. Applications still need to encrypt the
derived contact xpub before submitting the document.
Document fields
The contactRequest document requires these fields:
type DashpayContactRequestDocument = {
$createdAt: number;
$createdAtCoreBlockHeight: number;
toUserId: Uint8Array;
encryptedPublicKey: Uint8Array;
senderKeyIndex: number;
recipientKeyIndex: number;
accountReference: number;
};
The current DashPay contract schema requires the system field
$createdAtCoreBlockHeight. Older external references may use
coreHeightCreatedAt; do not submit that name to the current contract.
encryptedPublicKey is exactly 96 bytes:
- 16 bytes: AES-CBC initialization vector
- 80 bytes: AES-CBC ciphertext for the sender's 69-byte compact contact xpub
The compact xpub is the parent fingerprint (4 bytes), chain code (32 bytes) and public key (33 bytes), without the version, depth and child number of a full 78-byte BIP32 serialization. Receivers, including the reference mobile wallets and the Rust, Swift and Kotlin SDKs, refuse any other plaintext length. Both 69 and 78 bytes pad to the same 80-byte ciphertext, so the contract cannot catch the mistake: a request that encrypts the full 78 bytes is stored on chain, and the recipient's wallet then drops it.
The sender derives the contact xpub from the sender identity, recipient identity, account, and address index. The sender then encrypts that xpub with an ECDH shared secret from the sender's identity encryption private key and the recipient's identity decryption public key.
Derive the contact payment xpub
import { wallet } from '@dashevo/evo-sdk';
const senderKeyIndex = 0;
const recipientKeyIndex = 0;
const addressIndex = 0;
const contactKey = await wallet.deriveDashpayContactKey({
mnemonic: senderMnemonic,
network: 'testnet',
senderIdentityId,
receiverIdentityId: recipientIdentityId,
account: 0,
addressIndex,
});
// contactKey.xpub is encrypted into contactRequest.encryptedPublicKey.
senderKeyIndex and recipientKeyIndex identify the identity public keys used
for ECDH. addressIndex is the DIP-15 child index used for contact payment key
derivation and is independent from those identity key indexes.
Build the encrypted public key
The following helper shows the byte-level encryption shape. It uses the same
secp256k1 primitives exposed by @dashevo/dashcore-lib and Node.js crypto
for AES-CBC.
import crypto from 'node:crypto';
import dashcore from '@dashevo/dashcore-lib';
function fixed32(value): Buffer {
return Buffer.from(value.toArray('be', 32));
}
function deriveSharedKey({
privateKeyWif,
publicKeyBytes,
}: {
privateKeyWif: string;
publicKeyBytes: Uint8Array;
}): Buffer {
const privateKey = dashcore.PrivateKey.fromWIF(privateKeyWif);
const publicKey = dashcore.PublicKey.fromBuffer(Buffer.from(publicKeyBytes));
const sharedPoint = publicKey.point.mul(privateKey.toBigNumber());
if (sharedPoint.isInfinity()) {
throw new Error('ECDH shared point is invalid');
}
const x = fixed32(sharedPoint.getX());
const y = fixed32(sharedPoint.getY());
const compressedPrefix = Buffer.from([2 | (y[31] & 1)]);
return crypto.createHash('sha256').update(Buffer.concat([compressedPrefix, x])).digest();
}
function compactXpubPayload(xpub: string): Buffer {
const payload = dashcore.encoding.Base58Check.decode(xpub);
if (payload.length !== 78) {
throw new Error(`Invalid DashPay contact xpub length: ${payload.length}`);
}
// BIP32 layout: version(4) depth(1) parentFingerprint(4) childNumber(4)
// chainCode(32) publicKey(33). DIP-15 encrypts only the parent
// fingerprint, chain code and public key: 69 bytes.
return Buffer.concat([payload.subarray(5, 9), payload.subarray(13, 78)]);
}
function encryptContactXpub({
contactXpub,
senderEncryptionPrivateKeyWif,
recipientDecryptionPublicKeyBytes,
}: {
contactXpub: string;
senderEncryptionPrivateKeyWif: string;
recipientDecryptionPublicKeyBytes: Uint8Array;
}): Uint8Array {
const aesKey = deriveSharedKey({
privateKeyWif: senderEncryptionPrivateKeyWif,
publicKeyBytes: recipientDecryptionPublicKeyBytes,
});
const payload = compactXpubPayload(contactXpub);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
const encrypted = Buffer.concat([
cipher.update(payload),
cipher.final(),
]);
const encryptedPublicKey = Buffer.concat([iv, encrypted]);
if (encryptedPublicKey.length !== 96) {
throw new Error(`DashPay encryptedPublicKey must be 96 bytes, got ${encryptedPublicKey.length}`);
}
return encryptedPublicKey;
}
Submit the document
const encryptedPublicKey = encryptContactXpub({
contactXpub: contactKey.xpub,
senderEncryptionPrivateKeyWif,
recipientDecryptionPublicKeyBytes,
});
const accountReference = 0;
const document = {
$createdAt: Date.now(),
$createdAtCoreBlockHeight: platformCoreHeight,
toUserId: recipientIdentityIdBytes,
encryptedPublicKey,
senderKeyIndex,
recipientKeyIndex,
accountReference,
};
accountReference above is a placeholder for the current Platform field
accepted by the contactRequest schema. It is not a complete implementation of
any ASK/HMAC-based account-reference obfuscation described in older DIP text.
When querying received requests through the JavaScript SDK, pass identity IDs in
the representation expected by the SDK call being used. The contract stores
toUserId as a 32-byte identifier, while some high-level JavaScript query
helpers accept the base58 identity string and perform the conversion
internally.
Current SDK boundary
wallet.deriveDashpayContactKey handles DIP-15 path derivation. It does not
currently submit DashPay documents or encrypt/decrypt encryptedPublicKey.
Applications need to combine the wallet helper with identity encryption keys
until a higher-level DashPay contact request helper is added to the JavaScript
SDK.
Treat the example as a byte-level reference. A production application should add contract validation, decrypt round-trip tests, and checks that the selected identity keys are active secp256k1 keys bounded for DashPay contact requests.