Skip to content

Instantly share code, notes, and snippets.

@thuykaka
Last active June 3, 2025 13:50
Show Gist options
  • Select an option

  • Save thuykaka/24a195ea4ec770ae298e5326e3b1bac9 to your computer and use it in GitHub Desktop.

Select an option

Save thuykaka/24a195ea4ec770ae298e5326e3b1bac9 to your computer and use it in GitHub Desktop.
Tạo Bank QR (giống VietQR)
/*
https://vietqr.net/portal-service/download/documents/QR_Format_T&C_v1.0_VN_092021.pdf
Nội dung của mã QR động sẽ có các phần ghép lại tạo thành 1 nội dung hoàn chỉnh (mỗi phần bao gồm mã ID, độ dài của giá trị và giá trị):
1. Payload Format Indicator
Ví dụ: 000210 -> ID: 00, Độ dài: 02, Giá trị: 01.
2. Point of Initiation Method
Ví dụ: 010212 -> ID: 01, Độ dài: 02, Giá trị: 12.
Trong đó:
- 11 = QR tĩnh – áp dụng khi mã QR cho phép thực hiện nhiều lần giao dịch
- 12 = QR động – áp dụng khi mã QR chỉ cho phép thực hiện một lần giao dịch
3. Consumer Account Information
Ví dụ: 38530010A000000727012300069704230109mynamebvh0208QRIBFTTA -> ID: 38, Độ dài: 53, Giá trị: 0010A000000727012300069704230109mynamebvh0208QRIBFTTA
Giá trị gồm 3 thành phần:
- Định danh toàn cầu (GUID): 0010A000000727 -> ID: 00, Độ dài: 10, Giá trị: A000000727
- Tổ chức ngân hàng thụ hưởng: 012300069704230109mynamebvh -> ID: 01, Độ dài: 23, Giá trị: 00069704230109mynamebvh.
Giá trị này lại chia làm 2 phần:
- Acquier ID/BNB ID: 0006970423 -> ID: 00, Độ dài: 06, Giá trị: 970423 (970423 chính là mã BIN TPBank).
- Merchant ID/Consumer ID: 0109mynamebvh, ID: 01, Độ dài: 09, Giá trị: mynamebvh (Chính là số tài khoản).
- Mã dịch vụ: 0208QRIBFTTA -> ID: 02, Độ dài: 08, Giá trị: QRIBFTTA (QRIBFTTA: dịch vụ chuyển nhanh NAPAS247 bằng mã QR đến Tài khoản)
4. Transaction Currency
Ví dụ: 5303704 -> ID: 53, Độ dài: 03, Giá trị: 704 (704 mã tiền tệ VN)
5. Transaction Amount
Ví dụ: 5406180000 -> ID: 54, Độ dài: 06, Giá trị: 180000 (số tiền chuyển khoản)
6. Country Code
Ví dụ: 5802VN -> ID: 58, Độ dài: 02, Giá trị: VN (mã quốc gia)
7. Additional Data Field Template
Ví dụ: 62080804test -> ID: 62, Độ dài: 08, Giá trị: 0804test
Giá trị gồm: ID: 08, Độ dài 04, Giá trị: test (là nội dung chuyển khoản)
8. CRC
6304AB76 -> ID: 63, Độ dài: 04, Giá trị: AB76
Đây là một phương pháp kiểm tra tính toàn vẹn dữ liệu trong việc truyền tải thông tin.
Ở đây họ sử dụng CRC 0xFFFF.
Cách tính là lấy nội dung của QRCode như ví dụ trên sẽ là 00020101021238530010A000000727012300069704230109mynamebvh0208QRIBFTTA53037045405500005802VN62080804test6304
rồi cho qua hàm tính CRC sẽ ra kết quả: AB76.
*/
import qrcode, { type QRCodeToBufferOptions, type QRCodeToDataURLOptions } from 'qrcode';
import { z } from 'zod';
import { Jimp } from 'jimp';
type PointOfInitMethod = 'static' | 'dynamic';
type QrCodeLogoOptions = {
src: string;
text?: string;
backgroundColor?: string;
size?: {
w?: number;
h?: number;
};
position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
};
// Zod schema for validation
const VietQrSchema = z.object({
bankBin: z
.string()
.min(1, 'Bank BIN or name is required')
.refine(val => /^[0-9]{6}$/.test(val) || Object.keys(BANK_BINS).includes(val.toLowerCase()), {
message: 'Invalid bank BIN code or bank name',
}),
accNo: z
.string()
.min(1, 'Account number is required')
.max(19, 'Account number must be at most 19 digits')
.regex(/^[0-9]+$/, 'Account number must contain only digits'),
amt: z
.string()
.min(1, 'Amount is required')
.max(13, 'Amount must be at most 13 digits')
.regex(/^[0-9]+$/, 'Amount must contain only digits')
.refine(val => parseInt(val) > 0, {
message: 'Amount must be a positive number',
}),
desc: z
.string()
.min(1, 'Description is required')
.max(50, 'Description must be at most 50 characters')
.regex(/^[a-zA-Z0-9\s]+$/, 'Description must contain only letters, numbers and spaces')
.transform(val => val.trim())
.refine(val => val.length <= 50, {
message: 'Description must be at most 50 characters after trimming',
}),
initMethod: z.enum(['static', 'dynamic']).optional(),
});
type VietQrConfig = z.infer<typeof VietQrSchema>;
// Bank BIN codes mapping
const BANK_BINS: Record<string, string> = {
// Ngân hàng TMCP Sài Gòn Công Thương
saigonbank: '970400',
sgb: '970400',
'ngan hang sai gon cong thuong': '970400',
// Ngân hàng TMCP Sài Gòn Thương Tín
sacombank: '970403',
stb: '970403',
'ngan hang sai gon thuong tin': '970403',
// Ngân hàng Nông nghiệp và Phát triển Nông thôn Việt Nam
agribank: '970405',
'ngan hang nong nghiep va phat trien nong thon viet nam': '970405',
// Ngân hàng TMCP Đông Á
dongabank: '970406',
'ngan hang dong a': '970406',
// Ngân hàng TMCP Kỹ Thương Việt Nam
techcombank: '970407',
tcb: '970407',
'ngan hang ky thuong viet nam': '970407',
// Ngân hàng TNHH Một Thành Viên Dầu Khí Toàn Cầu
gpbank: '970408',
'ngan hang dau khi toan cau': '970408',
// Ngân hàng TMCP Bắc Á
bab: '970409',
'ngan hang bac a': '970409',
// Ngân hàng TNHH Một Thành Viên Standard Chartered
stanchart: '970410',
scvn: '970410',
'ngan hang standard chartered viet nam': '970410',
// Ngân hàng TMCP Đại Chúng Việt Nam
pvcombank: '970412',
'ngan hang dai chung viet nam': '970412',
// Ngân hàng TNHH Một Thành Viên Đại Dương
oceanbank: '970414',
ocean: '970414',
// Ngân hàng TMCP Công Thương Việt Nam
vietinbank: '970415',
'ngan hang cong thuong viet nam': '970415',
// Ngân hàng TMCP Á Châu
acb: '970416',
'ngan hang a chau': '970416',
// Ngân hàng Đầu tư và Phát triển Việt Nam
bidv: '970418',
'ngan hang dau tu va phat trien viet nam': '970418',
// Ngân hàng TMCP Quốc Dân
ncb: '970419',
'ngan hang quoc dan': '970419',
// Ngân hàng Liên doanh Việt Nga
vrb: '970421',
'ngan hang lien doanh viet nga': '970421',
// Ngân hàng TMCP Quân Đội
mb: '970422',
'ngan hang quan doi': '970422',
// Ngân hàng TMCP Tiên Phong
tpbank: '970423',
'ngan hang tien phong': '970423',
// Ngân hàng TNHH Một Thành Viên Shinhan Việt Nam
shinhan: '970424',
'ngan hang shinhan viet nam': '970424',
// Ngân hàng TMCP An Bình
abbank: '970425',
'ngan hang an binh': '970425',
// Ngân hàng TMCP Hàng Hải
msb: '970426',
'ngan hang hang hai': '970426',
// Ngân hàng TMCP Việt Á
vietabank: '970427',
'ngan hang viet a': '970427',
// Ngân hàng TMCP Nam Á
nama: '970428',
'ngan hang nam a': '970428',
// Ngân hàng TMCP Xăng dầu Petrolimex
pgbank: '970430',
'ngan hang thinh vuong va phat trien': '970430',
// Ngân hàng TMCP Xuất Nhập khẩu Việt Nam
eximbank: '970431',
'ngan hang xuat nhap khau viet nam': '970431',
// Ngân hàng TMCP Việt Nam Thịnh Vượng
vpbank: '970432',
'ngan hang viet nam thinh vuong': '970432',
// Ngân hàng TMCP Việt Nam Thương Tín
vietbank: '970433',
'ngan hang viet nam thuong tin': '970433',
// Ngân hàng TNHH Indovina
indovina: '970434',
'ngan hang indovina': '970434',
// Ngân hàng TMCP Ngoại thương Việt Nam
vietcombank: '970436',
vcb: '970436',
'ngan hang ngoai thuong viet nam': '970436',
// Ngân hàng TMCP Phát triển TP.HCM
hdbank: '970437',
'ngan hang phat trien tp hcm': '970437',
// Ngân hàng TMCP Bảo Việt
bvb: '970438',
baovietbank: '970438',
'ngan hang bao viet': '970438',
// Ngân hàng TNHH Một Thành Viên Public Việt Nam
publicbank: '970439',
pbvn: '970439',
'ngan hang public viet nam': '970439',
// Ngân hàng TMCP Đông Nam Á
seabank: '970440',
'ngan hang dong nam a': '970440',
// Ngân hàng TMCP Quốc Tế Việt Nam
vib: '970441',
'ngan hang quoc te viet nam': '970441',
// Ngân hàng TNHH Một Thành Viên Hong Leong Việt Nam
hongleong: '970442',
hlbvn: '970442',
'ngan hang hong leong viet nam': '970442',
// Ngân hàng TMCP Sài Gòn - Hà Nội
shb: '970443',
'ngan hang sai gon ha noi': '970443',
// Ngân hàng TNHH Một Thành Viên Xây Dựng Việt Nam
cbbank: '970444',
'ngan hang xay dung viet nam': '970444',
// Ngân hàng Hợp Tác Xã Việt Nam
coopbank: '970446',
// Ngân hàng TMCP Phương Đông
ocb: '970448',
// Ngân hàng TMCP Kiên Long
kienlong: '970452',
klb: '970452',
// Ngân hàng TMCP Bản Việt
bvbank: '970454',
// Ngân hàng Công nghiệp Hàn Quốc - Chi nhánh Hà Nội
ibk_hanoi: '970455',
// Ngân hàng Industrial Bank of Korea - Chi nhánh Hồ Chí Minh
ibk_hcm: '970456',
// Ngân hàng TNHH Một Thành Viên Woori Bank Việt Nam
wooribank: '970457',
// Ngân hàng TNHH Một Thành Viên UOB Việt Nam
uob: '970458',
// Ngân hàng TNHH Một Thành Viên CIMB Việt Nam
cimb: '970459',
};
/**
* Get bank BIN code from bank name
* @param bankName - Bank name or code (case insensitive)
* @returns Bank BIN code or undefined if not found
*/
function getBankBin(bankName: string): string | undefined {
return BANK_BINS[bankName.toLowerCase()];
}
/**
* Check if a string is a valid bank BIN code
* @param bin - String to check
* @returns boolean indicating if the string is a valid BIN code
*/
function isValidBin(bin: string): boolean {
return /^[0-9]{6}$/.test(bin);
}
/**
* Get bank BIN code from either bank name or direct BIN code
* @param input - Bank name or BIN code
* @returns Bank BIN code or undefined if not found
* @throws Error if input is invalid
*/
function resolveBankBin(input: string): string {
// If input is a valid BIN code, use it directly
if (isValidBin(input)) return input;
// Try to get BIN code from bank name
const bin = getBankBin(input);
if (bin) return bin;
throw new Error(`Invalid bank name or BIN code: ${input}, refer: https://www.sbv.gov.vn/webcenter/portal/vi/menu/trangchu/ttvnq/htmtcqht`);
}
class VietQr {
private static readonly PFI = '000210'; // PAYLOAD_FORMAT_INDICATOR
private static readonly GUID = '0010A000000727';
private static readonly SVC = '0208QRIBFTTA'; // SERVICE_CODE
private static readonly CURR = '5303704'; // CURRENCY
private static readonly CC = '5802VN'; // COUNTRY_CODE
private readonly initMethod: string;
private readonly accInfo: string;
private readonly txAmt: string;
private readonly addData: string;
/**
* Creates a new VietQR code generator instance
* @param cfg Configuration object containing all required parameters
* @param cfg.bankBin - Bank BIN code or bank name from https://www.sbv.gov.vn/webcenter/portal/vi/menu/trangchu/ttvnq/htmtcqht
* @param cfg.accNo - Recipient's bank account number
* @param cfg.amt - Transfer amount (positive number, max 13 digits)
* @param cfg.desc - Transfer description (max 50 characters, no special characters)
* @param cfg.initMethod - QR type: 'static' or 'dynamic'
* @throws Error if validation fails
*/
constructor(cfg: VietQrConfig) {
// Validate input using Zod schema
const validatedConfig = VietQrSchema.parse(cfg);
const bankBin = resolveBankBin(validatedConfig.bankBin);
this.initMethod = this.buildInitMethod(validatedConfig.initMethod);
this.accInfo = this.buildAccInfo(bankBin, validatedConfig.accNo);
this.txAmt = this.buildTxAmt(validatedConfig.amt);
this.addData = this.buildAddData(validatedConfig.desc);
}
private static getSize(txt: string): string {
return txt.length.toString().padStart(2, '0');
}
private buildInitMethod(method?: PointOfInitMethod): string {
return method === 'static' ? '010211' : '010212';
}
private buildAccInfo(bin: string, acc: string): string {
const binSize = VietQr.getSize(bin);
const binPart = `00${binSize}${bin}`;
const accSize = VietQr.getSize(acc);
const accPart = `01${accSize}${acc}`;
const org = `01${VietQr.getSize(`${binPart}${accPart}`)}${binPart}${accPart}`;
const info = `${VietQr.GUID}${org}${VietQr.SVC}`;
return `38${VietQr.getSize(info)}${info}`;
}
private buildTxAmt(amt: string): string {
return `54${VietQr.getSize(amt)}${amt}`;
}
private buildAddData(desc: string): string {
const val = `08${VietQr.getSize(desc)}${desc}`;
return `62${VietQr.getSize(val)}${val}`;
}
private static calcCrc(txt: string): string {
let crc = 0xffff;
for (let i = 0; i < txt.length; i++) {
crc ^= txt.charCodeAt(i) << 8;
for (let j = 0; j < 8; j++) {
crc = (crc & 0x8000) !== 0 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc.toString(16).toUpperCase().padStart(4, '0');
}
/**
* Generates the QR code string
* @returns The complete QR code string
*/
public getQrContent(): string {
const content = [VietQr.PFI, this.initMethod, this.accInfo, VietQr.CURR, this.txAmt, VietQr.CC, this.addData, '6304'].join('');
return `${content}${VietQr.calcCrc(content)}`;
}
public async getQrDataUrl(options: QRCodeToDataURLOptions): Promise<string> {
return qrcode.toDataURL(this.getQrContent(), { width: 250, ...options });
}
private detectUrlType(url: string): 'url' | 'base64' | undefined {
if (/^data:image\/png;base64,/.test(url)) return 'base64';
if (z.string().url().safeParse(url).success) return 'url';
return undefined;
}
public async getQrWithLogo(qrCodeOptions: QRCodeToBufferOptions, logoOptions: QrCodeLogoOptions): Promise<string> {
const qrImage = await Jimp.read(await qrcode.toBuffer(this.getQrContent(), { width: 250, ...qrCodeOptions }));
const logoSrc = logoOptions.src;
const logoType = this.detectUrlType(logoSrc);
if (!logoType) throw new Error('Invalid logo source');
const logoImage = await Jimp.read(logoType === 'url' ? logoSrc : Buffer.from(logoSrc.split(',')[1]!, 'base64'));
const logoRatio = logoImage.bitmap.height / logoImage.bitmap.width;
const logoWidth = Math.min(logoOptions.size?.w ?? qrImage.bitmap.width * 0.25, qrImage.bitmap.width * 0.25);
const logoHeight = logoOptions.size?.h ?? Math.round(logoWidth * logoRatio);
logoImage.resize({ w: logoWidth, h: logoHeight });
// Center the logo in the QR code
const x = qrImage.bitmap.width / 2 - logoWidth / 2;
const y = qrImage.bitmap.height / 2 - logoHeight / 2;
if (!logoOptions.backgroundColor) {
qrImage.composite(logoImage, x, y);
} else {
const backgroundImage = new Jimp({ width: logoWidth, height: logoHeight, color: logoOptions.backgroundColor });
backgroundImage.composite(logoImage, 0, 0);
qrImage.composite(backgroundImage, x, y);
}
return qrImage.getBase64('image/png');
}
}
export { VietQr, type VietQrConfig };
const vietQr = new VietQr({ bankBin: '970415', accNo: '0982447171', amt: '50000', desc: 'thuyck' });
const qrOptions = {};
const logoOptions = {
src: `https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png`,
backgroundColor: '#ffffff',
};
const url = await vietQr.getQrWithLogo(qrOptions, logoOptions);
console.log('url:', url);
// data:image/png;base64,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JImabVokUbPNVJKYyjbLMhArRxJvunaN66+cccs9u9x1bJcTJ815e8XVzxWvv2aN555fOEt793H++z7I5s2/xtbtn2a+eZL9r38La2/5Hobnv4AztOf5zL7js+z+xVH86KdhbnThWynPuxFtXMgZ33K48KEf2svH7tjhT7+0y0XfXPiHV834tx8/zUzwzfuP8L1Xvp5LDl6E9u7jgv/4Xzj1ax9j687bmZ86xf7XvIE9//ifUC66mFgtA7GyDh8o3HB94Ybr1/jbaGMPe996A3vfegNPReuHGa58D0/loUfnfP7YnBtft4a0zhl33bfLGRceEpcdeh5vv/YHOUt79rLvhrex74a3EattIOJrPnzraW79kzn3PjjwhmsGvvI4fOCWTWYFrr9qIJ65BiK+5p+/dg9fObHJr965w6/eucMZazP4ides8fzDhXjmGoj4mvP2ivd93wZ33z/nC4/MOTQT11w+46ILCvHMNrBAkqjZZirbjCWJFtvUJDGWJMayTYskarZpkUSLbWqSGMs2X29tKHzHFYWXXSlWlW1qkhhLEi22qUmixTYtkqjZpkUSNdssQiEiuleIiO4VIqJ7hYjonmybJZLEIthmUSQxlm2mkMRUthlLEi22qUliVdmmRRI120wlibFsc64VIqJ7hYjoXiEiuleIiO4VIqJ7sm06JokW29Qk0WKbmiTGss2iSGIs29QkMZZtWiRRs02LJFpsM4UkprLNIkiixTbLUoiI7hUionuFiOheISK6N7BAkhjLNotgmxZJTGGbFklMIYkW27TYZhFsU5PEVLYZSxKrShIttllFhYjoXiEiuleIiO4VIqJ7hYjo3sCS2WYsSbTYZixJtNimJokWSdRsswi2WSbbtEiiZpsWSYwliRbbjGWbZZHEVLZpkUTNNotQiIjuFSKie4WI6F4hIrpXiIjuybZZIkm02GYKSbTYZpkksUy2qUmixTY1SSyTbVokUbNNiyRqtmmRRM02Y0ni6bBNTRIttlmWQkR0rxAR3StERPcKEdE92TYLIomabcaSRIttxpLEVLapSWIs2yyTJBbBNmNJ4umwTU0SY9lmmSQxlm1aJFGzzSIUIqJ7hYjoXiEiuleIiO4VIqJ7sm2WSBKLYJtnGklMZZuaJMayTYskxrJNTRIttmmRRM02LZKo2aZFEstkm1VUiIjuFSKie4WI6F4hIron22ZBJFGzzTJJosU2NUlMZZspJNFim6kksQi2GUsSLbapSaLFNjVJ9MA2i1CIiO4VIqJ7hYjoXiEiuleIiO4NLJBtzjXbnGuSaLHNWJJosU1NEotgm7Ek0WKbqSQxlm2mkESLbcaSxLlWiIjuFSKie4WI6F4hIron22ZBJLGqbDOFJMayzaJIomabFknUbNMiibFsM5YkWmwzliTGsk1NEi22qUlimWyzCIWI6F4hIrpXiIjuFSKie4WI6N7AktlmmSQxlSRqtlkFtqlJYixJtNhmCklMJYlFsM2i2KYmiRbbLEshIrpXiIjuFSKie4WI6N7AipDEFLaZShJTSaJmmxZJjGWbFknUbNMiiZptVoEkFkESyySJVVSIiO4VIqJ7hYjoXiEiuleIiO4NxJNs0yKJmiTGkkSLbcaSxFiSGEsSLbapSaLFNotgmxZJ1GzTIomabaaSRIttapI41woR0b1CRHSvEBHdK0RE9woR0b2BeJIkWmwzliRqtmmRxCLYpkUSNdssgm1aJNFim5okWmxTk8RYkmixTU0SLbZpkUTNNi2SqNlmEQoR0b1CRHSvEBHdK0RE9wZWhG3ONduMJYmxJNFim5okWmzTIomaJFpsU5NEi22mkMTTIYmxJFGzTYskarZpkUTNNi2SaLHNWLZZlkJEdK8QEd0rRET3ChHRvUJEdE+2zYJIYlXZpiaJc802LZJosc25JolFsM1YkpjKNlNJomabFknUbLMIhYjoXiEiuleIiO4VIqJ7sm0iomuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOheISK6V4iI7hUionuFiOje/wVMuTioRy1+8AAAAABJRU5ErkJggg==
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment