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 78-byte serialized contact xpub
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 serializedXpubPayload(xpub: string): Buffer {
const payload = dashcore.encoding.Base58Check.decode(xpub);
if (payload.length !== 78) {
throw new Error(`Invalid DashPay contact xpub length: ${payload.length}`);
}
return payload;
}
function encryptContactXpub({
contactXpub,
senderEncryptionPrivateKeyWif,
recipientDecryptionPublicKeyBytes,
}: {
contactXpub: string;
senderEncryptionPrivateKeyWif: string;
recipientDecryptionPublicKeyBytes: Uint8Array;
}): Uint8Array {
const aesKey = deriveSharedKey({
privateKeyWif: senderEncryptionPrivateKeyWif,
publicKeyBytes: recipientDecryptionPublicKeyBytes,
});
const payload = serializedXpubPayload(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.